CHAR와 VARCHAR는 같은거다.

728x90

DB를 쓰다보니 구분해서 쓰는데 최근에 오라클 디비를 건드릴 일이 있어서 공부한 자료

일단 필자는 RDB는 MYSQL 기반을 쓰고, 오라클은 최근에 써보는중이다.

일단 이글을 보는사람은 한국인일 가능성이 높으니까

한글 유니코드를 쓴다고 가정하고 시작한다. (utf8mb4) (매우중요)

CHAR와 VARCHAR

일단, CHAR는 고정길이고 VARCHAR는 가변길이다.

CHAR에 10만큼 넣었으면 구조상 10만큼의 크기를 무조건 먹기 때문에 리소스 낭비가 발생할 수 있다

반대로 VARCHAR의 경우에는 가변크기기 때문에 10을 넘으면 안되는거라고 보면 된다.

 

실제로 3만큼만 넣었으면 3만큼의 크기만 할당하여 사용한다.

일단 utf8mb4에서는 한글을 3바이트로 쓰는데 영어로 인코딩하는경우 euckr로 하면 2바이트다.

따라서 설정을 하고, CHAR(10)으로 하면 30바이트짜리인거고

이거는 그냥 고정해서 30바이트를 쓴다.

 

이게 예전에는 물리엔진 자체, 하드웨어 자체가 느리다보니 이렇게 데이터를 고정시켜서 딱

넣어두면 검색하는데 약간의 이점이 있었다고 한다.

 

다만, 현재는 차이가 없으므로 명목상 이거는 절대 10글자만 쓸거야 하지않으면 걍 VARCHAR를 쓰면되는데

예를들어 오라클에서는 BOOLEAN 형이 없어서 CHAR(1)로 해서 Y/N으로 볼린을 처리한다.

이런경우에 쓰면되고, 아니면 뭐 핸드폰번호나 이렇게 고정된 글자일때 쓰면 매우 미미한 효과를 얻을 수 있다고한다.

 

나도 이걸 해보기 전에는 이렇다고 배웠고 알고 있었다.


일단 테스트용 디비를 하나 만들고 아래처럼 쓴다

DROP TABLE IF EXISTS test_char_varchar;

CREATE TABLE test_char_varchar (
    id INT AUTO_INCREMENT PRIMARY KEY,
    v VARCHAR(10),
    c CHAR(10)
);

그리고 직접 입력하는 멍청한짓은 하지않을 거기 때문에 랜덤 함수하나를 만든다

#!/bin/bash

MYSQL_USER="root"
MYSQL_PASS="your_password"
MYSQL_DB="test_db"

# 테이블 생성
mysql --default-character-set=utf8mb4 -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB <<EOF
DROP TABLE IF EXISTS test_char_varchar;

CREATE TABLE test_char_varchar (
    id INT AUTO_INCREMENT PRIMARY KEY,
    v VARCHAR(10),
    c CHAR(10)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
EOF

echo "테이블 생성 완료"

# UTF-8 한글 랜덤 생성 함수
rand_korean() {
    LEN=$((1 + RANDOM % 10))
    OUT=""
    KOREAN_LIST=("가" "나" "다" "라" "마" "바" "사" "아" "자" "차" "카" "타" "파" "하"
                 "거" "너" "더" "러" "머" "버" "서" "어" "저" "처" "커" "터" "퍼" "허")

    for ((i=1; i<=LEN; i++)); do
        RAND_IDX=$((RANDOM % ${#KOREAN_LIST[@]}))
        OUT="${OUT}${KOREAN_LIST[$RAND_IDX]}"
    done

    echo "$OUT"
}

echo "10만건 INSERT 시작..."

for ((i=1; i<=100000; i++)); do
    V=$(rand_korean)
    C=$(rand_korean)

    mysql --default-character-set=utf8mb4 -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB \\
        -e "INSERT INTO test_char_varchar (v, c) VALUES ('$V', '$C');"

    if (( $i % 5000 == 0 )); then
        echo "$i 건 입력됨"
    fi
done

echo "완료! 총 100,000건 입력됨."

그럼 보안알림 뜨는데 나는 로컬에서돌려서 신경안썻지만 혹시 바꾸려면 cnf에서 설정해서 위에 부분 바꿔서

주입시켜주면된다.

 

원래 10만개할려고했는데 귀찮아서 중간에 그만하고 있는걸로만 테스트해본다.

mysql> SELECT
    ->     COUNT(*) AS total_rows,
    ->     SUM(LENGTH(v)) AS total_v_bytes,
    ->     SUM(LENGTH(c)) AS total_c_bytes,
    ->     AVG(LENGTH(v)) AS avg_v_bytes,
    ->     AVG(LENGTH(c)) AS avg_c_bytes
    -> FROM test_char_varchar;
+------------+---------------+---------------+-------------+-------------+
| total_rows | total_v_bytes | total_c_bytes | avg_v_bytes | avg_c_bytes |
+------------+---------------+---------------+-------------+-------------+
|      21356 |        352362 |        352674 |     16.4994 |     16.5140 |
+------------+---------------+---------------+-------------+-------------+
1 row in set (0.018 sec)

근데 실제로 해보니까 차이가 거의 안난다.

그리고 가장중요한건 3바이트니까 대략 6만3천 이상이 C 바이트로 잡혀야 내가 알고 있는건데 아니였다.

 

이상한데 싶어서 안에 데이터를 보니까 잘못 짰다

같은 데이터를 넣어야 의미가 있지 싶어서 다시만들었다

#!/bin/bash

MYSQL_USER="root"
MYSQL_PASS="your_password"
MYSQL_DB="test_db"

# 테이블 생성
mysql --default-character-set=utf8mb4 -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB <<EOF
DROP TABLE IF EXISTS test_char_varchar;

CREATE TABLE test_char_varchar (
    id INT AUTO_INCREMENT PRIMARY KEY,
    v VARCHAR(10),
    c CHAR(10)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
EOF

echo "테이블 생성 완료"

# UTF-8 한글 랜덤 생성 함수
rand_korean() {
    LEN=$((1 + RANDOM % 10))
    OUT=""
    KOREAN_LIST=("가" "나" "다" "라" "마" "바" "사" "아" "자" "차" "카" "타" "파" "하"
                 "거" "너" "더" "러" "머" "버" "서" "어" "저" "처" "커" "터" "퍼" "허")

    for ((i=1; i<=LEN; i++)); do
        RAND_IDX=$((RANDOM % ${#KOREAN_LIST[@]}))
        OUT="${OUT}${KOREAN_LIST[$RAND_IDX]}"
    done

    echo "$OUT"
}

echo "10만건 INSERT 시작..."

for ((i=1; i<=100000; i++)); do
    VALUE=$(rand_korean)   # 💡 한 번만 생성
    V="$VALUE"
    C="$VALUE"             # 💡 동일 값 저장

    mysql --default-character-set=utf8mb4 -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB \\
        -e "INSERT INTO test_char_varchar (v, c) VALUES ('$V', '$C');"

    if (( $i % 5000 == 0 )); then
        echo "$i 건 입력됨"
    fi
done

echo "완료! 총 100,000건 입력됨."

보니까 안다르다는걸 알았다. 우리가 주로 쓰는 utf8 에서는 의미가없다는것을 알 수 있다.

mysql> SELECT
    ->     COUNT(*) AS total_rows,
    ->     SUM(LENGTH(v)) AS total_v_bytes,
    ->     SUM(LENGTH(c)) AS total_c_bytes,
    ->     AVG(LENGTH(v)) AS avg_v_bytes,
    ->     AVG(LENGTH(c)) AS avg_c_bytes
    -> FROM test_char_varchar;
+------------+---------------+---------------+-------------+-------------+
| total_rows | total_v_bytes | total_c_bytes | avg_v_bytes | avg_c_bytes |
+------------+---------------+---------------+-------------+-------------+
|       7105 |        117420 |        117420 |     16.5264 |     16.5264 |
+------------+---------------+---------------+-------------+-------------+
1 row in set (0.004 sec)

현대 MySQL(utf8mb4)에서 CHAR는 실제로 가변 바이트 저장을 한다

정확히 말하면:

  • MySQL에서 CHAR는 내부적으로 길이를 문자의 수로 제한할 뿐
  • 실제 저장되는 바이트는 문자 인코딩에 따라 결정된다.

즉, CHAR(10) = 10문자까지 저장한다는 의미지,

고정된 30바이트를 패딩하는 고정 바이트 타입이 아니다.

그래서 “고정 길이 = CHAR는 항상 공간 낭비”라는 고전적인 논리는

MySQL utf8mb4에서는 사실상 거의 의미가 없다.

인코딩이 ASCII(latin1)이면 바이트 고정이므로 이점 존재

의미있게 다시 해보자

DROP TABLE IF EXISTS test_char_varchar_ascii;

CREATE TABLE test_char_varchar_ascii (
    id INT AUTO_INCREMENT PRIMARY KEY,
    v VARCHAR(10),
    c CHAR(10)
) CHARACTER SET latin1 COLLATE latin1_general_ci;
mysql> SELECT
    ->     COLUMN_NAME,
    ->     CHARACTER_SET_NAME,
    ->     COLLATION_NAME
    -> FROM information_schema.COLUMNS
    -> WHERE TABLE_SCHEMA='test_db'
    ->   AND TABLE_NAME='test_char_varchar_ascii';
+-------------+--------------------+-------------------+
| COLUMN_NAME | CHARACTER_SET_NAME | COLLATION_NAME    |
+-------------+--------------------+-------------------+
| id          | NULL               | NULL              |
| v           | latin1             | latin1_general_ci |
| c           | latin1             | latin1_general_ci |
+-------------+--------------------+-------------------+
3 rows in set (0.005 sec)
#!/bin/bash

MYSQL_USER="root"
MYSQL_PASS="your_password"
MYSQL_DB="test_db"

# 테이블 생성
mysql -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB <<EOF
DROP TABLE IF EXISTS test_char_varchar_ascii;

CREATE TABLE test_char_varchar_ascii (
    id INT AUTO_INCREMENT PRIMARY KEY,
    v VARCHAR(10),
    c CHAR(10)
) CHARACTER SET latin1 COLLATE latin1_general_ci;
EOF

echo "ASCII 테이블 생성 완료"

# 랜덤 영문 문자열 생성 함수
rand_ascii() {
    LEN=$((1 + RANDOM % 10))
    OUT=""
    CHARS=("A" "B" "C" "D" "E" "F" "G" "H" "I" "J"
           "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T"
           "U" "V" "W" "X" "Y" "Z")

    for ((i=1; i<=LEN; i++)); do
        IDX=$((RANDOM % ${#CHARS[@]}))
        OUT="${OUT}${CHARS[$IDX]}"
    done

    echo "$OUT"
}

echo "ASCII 10만건 INSERT 시작..."

START_TS=$(date +%s)

for ((i=1; i<=100000; i++)); do
    VALUE=$(rand_ascii)

    mysql -u$MYSQL_USER -p$MYSQL_PASS $MYSQL_DB \\
        -e "INSERT INTO test_char_varchar_ascii (v, c) VALUES ('$VALUE', '$VALUE');" >/dev/null 2>&1

    if (( $i % 10000 == 0 )); then
        echo "$i 건 입력됨"
    fi
done

END_TS=$(date +%s)
DIFF=$((END_TS - START_TS))

echo "총 100,000건 입력 완료 (소요 시간: ${DIFF}초)"

근데도 동일하다. 분명 gpt가 다르다고 했는데?

mysql>
mysql> SELECT
    ->     COUNT(*) AS total_rows,
    ->     SUM(OCTET_LENGTH(v)) AS v_bytes,
    ->     SUM(OCTET_LENGTH(c)) AS c_bytes,
    ->     AVG(OCTET_LENGTH(v)) AS avg_v_bytes,
    ->     AVG(OCTET_LENGTH(c)) AS avg_c_bytes
    -> FROM test_char_varchar_ascii;
+------------+---------+---------+-------------+-------------+
| total_rows | v_bytes | c_bytes | avg_v_bytes | avg_c_bytes |
+------------+---------+---------+-------------+-------------+
|      15098 |   83137 |   83137 |      5.5065 |      5.5065 |
+------------+---------+---------+-------------+-------------+

SELECT 가 원인이였다

내가 짠 쿼리는 DB에 직접 저장된 물리크기를 제는게 아니라 꺼내온 크기를 재는 방식이였다.

저장할때 CHAR 10 을넣으면 스페이스를 넣어서 10글자 넣는게 맞다!

근데 꺼낼때는 랭스를 찍으면 MYSQL에서 자체적으로 공백을 버려(Trim) 해서준다

그래서 다시 찾아보니니까 이 공백을 제거 하지말게하는 법이 있다.

-- 1. 임시로 SQL 모드 변경 (공백 제거 하지마!)
SET sql_mode = 'PAD_CHAR_TO_FULL_LENGTH';

이렇게해서 공백제거를 없엔다

-- 2. 다시 조회
SELECT
    v,
    c,
    LENGTH(v) AS v_len,
    LENGTH(c) AS c_len
FROM test_char_varchar_ascii
LIMIT 5;
+------------+------------+-------+-------+
| v          | c          | v_len | c_len |
+------------+------------+-------+-------+
| MBGOT      | MBGOT      |     5 |    10 |
| KTP        | KTP        |     3 |    10 |
| HK         | HK         |     2 |    10 |
| ECRMTLQLEJ | ECRMTLQLEJ |    10 |    10 |
| BTAIWYBBX  | BTAIWYBBX  |     9 |    10 |
+------------+------------+-------+-------+
5 rows in set (0.001 sec)

그러면 실제로 다른걸 알 수 있다. 그래서 궁금해졌다 utf는 사실 의미가 있었나?

SELECT
    v,
    c,
    LENGTH(v) AS v_len,
    LENGTH(c) AS c_len
FROM test_char_varchar
LIMIT 5;
+--------------------------------+--------------------------------+-------+-------+
| v                              | c                              | v_len | c_len |
+--------------------------------+--------------------------------+-------+-------+
| 자바                           | 자바                           |     6 |    14 |
| 하                             | 하                             |     3 |    12 |
| 머마라커머버더자나마           | 머마라커머버더자나마           |    30 |    30 |
| 처마파타처터버파               | 처마파타처터버파               |    24 |    26 |
| 가라어바가파너                 | 가라어바가파너                 |    21 |    24 |
+--------------------------------+--------------------------------+-------+-------+

갑자기 값이 이상하게 나온다. 물론 고정크기 30으로 나오는것은 아니지만 값이 들쭉날쭉한데

utf8mb4에서 한글은 3바이트지만, 채워넣는 공백(Padding)은 영어랑 똑같이 1바이트라서 발생하는 현상

1. 첫 번째 줄: '자바' (14바이트)

  • 데이터: '자', '바' (2글자)
  • CHAR(10)의 규칙: 10글자가 될 때까지 나머지를 공백으로 채움.
  • 채워야 할 공백: 10 - 2 = 8개

계산: (한글 2글자 × 3바이트) + (공백 8개 × 1바이트) = 6 + 8 = 14

2. 두 번째 줄: '하' (12바이트)

  • 데이터: '하' (1글자)
  • 채워야 할 공백: 10 - 1 = 9개

계산: (한글 1글자 × 3바이트) + (공백 9개 × 1바이트) = 3 + 9 = 12

3. 세 번째 줄: '머마라...나마' (30바이트)

  • 데이터: 10글자 꽉 참.
  • 채워야 할 공백: 0개

계산: (한글 10글자 × 3바이트) + (공백 0개) = 30 + 0 = 30

정리하면

구분 CHAR(10) VARCHAR(10)

저장 방식 정해진 길이만큼 공백(Space)을 채워서 저장 (Padding) 실제 데이터 + **길이 정보(1~2byte)**만 저장
데이터 예시 '자바' (2글자) '자바' (2글자)
실제 저장(utf8mb4) 자바 (6byte) + 공백 8개 (8byte) = 14byte 길이정보 (1byte) + 자바 (6byte) = 7byte
결과 짧은 문자열 넣으면 오히려 용량 낭비가 심할 수 있음 딱 필요한 만큼만 써서 용량 효율적

조회(SELECT) 할 때의 차이 (가장 위험한 함정)

개발자가 가장 조심해야 할 기능적인 차이입니다. 님이 처음에 헷갈렸던 이유이기도 합니다.

  • CHAR: 꺼낼 때 뒤에 붙은 공백을 무조건 잘라버립니다(Trim).
    • 내가 'A '(뒤에 공백 2개)를 저장해도, 꺼낼 땐 'A'가 나옵니다.
    • 문제점: 의도적으로 공백을 저장해야 하는 데이터라면 CHAR를 쓰면 데이터가 손실됩니다.
  • VARCHAR: 공백도 데이터로 취급해서 그대로 보존합니다.
    • 'A '를 저장하면 꺼낼 때도 'A '입니다.

성능 차이 (InnoDB 엔진 기준)

예전(MyISAM 시절)에는 CHAR가 무조건 빨랐습니다. 하지만 지금(InnoDB)은 다릅니다.

  • 디스크 저장: InnoDB는 내부적으로 CHAR도 가변 길이처럼 최적화해서 저장하려고 노력합니다(ROW_FORMAT=DYNAMIC). 그래서 디스크 용량이나 I/O 속도 차이는 거의 없습니다.
  • 메모리 처리 (중요): 여기서 미세한 차이가 납니다.
    • DB가 데이터를 메모리(Buffer Pool)로 올려서 가공할 때, CHAR는 "무조건 최대 길이"만큼 메모리를 확보하고 시작하는 경향이 있습니다.
    • CHAR(255)를 썼는데 실제론 1글자만 들어있어도, 메모리 상에서는 처리가 비효율적일 수 있습니다.
    • 따라서 엄청나게 긴 컬럼을 CHAR로 잡으면 메모리 낭비가 생길 수 있습니다.

언제 무엇을 써야 할까? (결론)

이제 실무적인 가이드라인입니다.

① 기본적으로는 VARCHAR를 쓰세요.

  • 이름, 이메일, 주소, 제목, 댓글 등 길이가 변하는 모든 데이터.
  • 현대 MySQL에서는 VARCHAR가 표준이고 성능 저하도 없습니다.
  • 특히 한글(utf8mb4) 환경에서는 CHAR가 공백 패딩 때문에 용량을 더 먹는 경우가 많습니다(위의 14byte vs 7byte 예시 참고).

② CHAR는 이럴 때만 쓰세요. (딱 고정된 것)

  • 고정된 코드: 국가 코드('KR', 'US'), 통화 코드('KRW', 'USD')
  • 해시 값: 비밀번호 암호화된 값(BCrypt 등은 길이가 일정함), MD5, SHA-256 값.
  • Y/N 값: 'Y', 'N' 처럼 1글자 고정 플래그.
  • UUID(대시 제외): 32글자로 딱 떨어지는 ID.

오늘도 재밌는 삽질이였다

추가로 자바에서 CHAR랑 String은

  • Java char = MySQL CHAR(1)
    • 무조건 1글자.
    • 가볍고 빠름 (스택 메모리에 저장).
  • Java String = MySQL VARCHAR(N)
    • 길이가 제각각임.
    • 편리한 기능이 많지만, char보다는 무거움 (힙 메모리에 저장).

이렇게 이해할수 있다

728x90

'DB' 카테고리의 다른 글

SQL 문법 정리 - <> ?  (0) 2025.01.21
mysql , mariadb ip 허용하기  (0) 2024.07.15
MariaDB 대소문자 구분  (0) 2024.07.02
MYSQL - MySQL Dump 와 옵션  (0) 2024.06.25