Theme:

같은 데이터를 저장하더라도 타입 선택에 따라 저장 공간이 2배 이상 차이날 수 있다면, 데이터 타입을 어떻게 골라야 할까요?

MySQL에서 데이터 타입 선택은 단순한 문법 문제가 아닙니다. 저장 공간, 인덱스 효율, 쿼리 성능에 직접적인 영향을 미칩니다. 이 글에서는 각 타입의 특성과 올바른 사용법을 정리하겠습니다.

숫자 타입

정수 타입

타입저장 크기범위 (SIGNED)범위 (UNSIGNED)
TINYINT1바이트-128 ~ 1270 ~ 255
SMALLINT2바이트-32,768 ~ 32,7670 ~ 65,535
MEDIUMINT3바이트-8M ~ 8M0 ~ 16M
INT4바이트-2.1B ~ 2.1B0 ~ 4.2B
BIGINT8바이트-9.2E ~ 9.2E0 ~ 18.4E

선택 기준:

  • ID 컬럼: INT UNSIGNED(42억)로 충분한 경우가 대부분입니다
  • 대규모 시스템: BIGINT UNSIGNED를 사용합니다
  • 상태값, 플래그: TINYINT로 충분합니다
  • INT(11)의 11은 저장 크기와 무관합니다 — 단순히 ZEROFILL 시 표시 너비일 뿐입니다
SQL
-- INT(11)이든 INT(4)든 저장 크기는 동일하게 4바이트
CREATE TABLE example (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,  -- 4바이트, 0~42억
    status TINYINT NOT NULL DEFAULT 0,            -- 1바이트
    count BIGINT UNSIGNED                         -- 8바이트
);

실수 타입

타입저장 크기정밀도
FLOAT4바이트~7자리
DOUBLE8바이트~15자리
DECIMAL(M,D)가변정확한 소수점
SQL
-- 금액 계산에는 반드시 DECIMAL을 사용합니다
-- FLOAT/DOUBLE은 부동소수점 오차가 발생합니다
CREATE TABLE products (
    price DECIMAL(10,2) NOT NULL,    -- 최대 99999999.99
    weight DOUBLE                     -- 정확도가 덜 중요한 경우
);

-- 부동소수점 오차 예시
SELECT 0.1 + 0.2;  -- 0.30000000000000004 (DOUBLE)
SELECT CAST(0.1 AS DECIMAL(10,1)) + CAST(0.2 AS DECIMAL(10,1));  -- 0.3 (정확)

금액, 환율 등 정확한 계산이 필요한 경우 반드시 DECIMAL을 사용해야 합니다.

문자열 타입

CHAR vs VARCHAR

특성CHAR(n)VARCHAR(n)
저장 방식고정 길이가변 길이
공간항상 n바이트데이터 + 1~2바이트(길이)
패딩공백으로 채움없음
성능고정 크기로 약간 빠름가변이라 약간 느림
SQL
-- CHAR: 길이가 일정한 데이터에 적합
CREATE TABLE codes (
    country_code CHAR(2) NOT NULL,        -- 'KR', 'US' 등 항상 2자
    postal_code CHAR(5) NOT NULL,         -- '06100' 항상 5자
    uuid CHAR(36) NOT NULL                -- UUID는 항상 36자
);

-- VARCHAR: 길이가 가변적인 데이터에 적합
CREATE TABLE users (
    name VARCHAR(50) NOT NULL,            -- 이름: 길이 다양
    email VARCHAR(255) NOT NULL,          -- 이메일: 길이 다양
    bio VARCHAR(500)                      -- 자기소개: 길이 다양
);

VARCHAR의 길이 정보

  • 255바이트 이하: 길이 정보 1바이트
  • 256바이트 이상: 길이 정보 2바이트
  • 이 차이는 메모리 할당에도 영향을 미칩니다 (임시 테이블, 정렬 버퍼)

TEXT vs VARCHAR

특성VARCHARTEXT
최대 크기65,535바이트 (행 전체)TINYTEXT~LONGTEXT (4GB)
인덱스전체 가능접두사만 가능
기본값설정 가능설정 불가
저장 위치행 내부 (짧을 때)외부 페이지 (overflow)
SQL
-- 짧은 텍스트는 VARCHAR
CREATE TABLE posts (
    title VARCHAR(200) NOT NULL,
    -- 긴 텍스트는 TEXT
    content TEXT NOT NULL,
    -- 매우 긴 텍스트는 MEDIUMTEXT
    full_html MEDIUMTEXT
);

-- TEXT에 인덱스를 걸려면 접두사 길이를 지정해야 합니다
CREATE INDEX idx_content ON posts (content(100));

TEXT 사용 시 주의점:

  • 임시 테이블이 디스크에 생성될 수 있어 정렬 성능이 떨어집니다
  • SELECT *를 하면 대량의 데이터가 전송됩니다
  • 필요한 컬럼만 명시적으로 SELECT하는 것이 좋습니다

날짜와 시간 타입

DATETIME vs TIMESTAMP

특성DATETIMETIMESTAMP
저장 크기8바이트4바이트
범위1000-01-01 ~ 9999-12-311970-01-01 ~ 2038-01-19
타임존저장된 그대로 반환UTC 변환 자동 처리
기본값NULLCURRENT_TIMESTAMP 가능
SQL
CREATE TABLE events (
    -- DATETIME: 타임존과 무관한 날짜 (생일, 기념일)
    birthday DATETIME NOT NULL,

    -- TIMESTAMP: 타임존 변환이 필요한 시점 (생성일, 수정일)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 타임존 변환 확인
SET time_zone = '+09:00';
INSERT INTO events (birthday, created_at) VALUES ('2000-01-01 00:00:00', NOW());

SET time_zone = '+00:00';
SELECT * FROM events;
-- birthday: 2000-01-01 00:00:00 (변하지 않음)
-- created_at: 시간이 9시간 빠르게 표시됨 (UTC 변환)

2038년 문제

TIMESTAMP는 Unix 타임스탬프(4바이트 정수)로 저장되므로 2038-01-19 03:14:07 UTC까지만 표현 가능합니다. 장기 보관 데이터에는 DATETIME을 사용하는 것이 안전합니다.

DATE, TIME, YEAR

SQL
CREATE TABLE schedules (
    event_date DATE NOT NULL,         -- 3바이트, '2026-03-19'
    start_time TIME NOT NULL,         -- 3바이트, '14:30:00'
    birth_year YEAR NOT NULL          -- 1바이트, 1901~2155
);

ENUM과 SET

ENUM

SQL
CREATE TABLE shirts (
    size ENUM('XS', 'S', 'M', 'L', 'XL') NOT NULL
);

-- 내부적으로 1, 2, 3, 4, 5로 저장됩니다 (1~2바이트)
INSERT INTO shirts VALUES ('M');  -- 내부적으로 3 저장

ENUM의 장점:

  • 저장 공간 절약 (VARCHAR 대비)
  • 유효하지 않은 값 자동 거부

ENUM의 단점:

  • 값 추가/삭제 시 ALTER TABLE 필요
  • 정렬이 문자열 순서가 아닌 정의 순서
  • ORM과 호환성 문제가 발생할 수 있음

값이 자주 변경되는 경우 별도의 참조 테이블을 사용하는 것이 실무적으로 더 유연합니다.

SET

SQL
CREATE TABLE permissions (
    flags SET('READ', 'WRITE', 'DELETE', 'ADMIN')
);

INSERT INTO permissions VALUES ('READ,WRITE');  -- 비트마스크로 저장

JSON 타입 (MySQL 5.7.8+)

SQL
CREATE TABLE configs (
    id INT PRIMARY KEY,
    settings JSON NOT NULL
);

INSERT INTO configs VALUES (1, '{"theme": "dark", "lang": "ko"}');

-- JSON 함수로 특정 키 조회
SELECT settings->>'$.theme' AS theme FROM configs WHERE id = 1;

-- JSON 키에 인덱스 생성 (가상 컬럼 활용)
ALTER TABLE configs
ADD COLUMN theme VARCHAR(20) GENERATED ALWAYS AS (settings->>'$.theme') STORED,
ADD INDEX idx_theme (theme);

JSON 사용 시 주의점:

  • 스키마가 없으므로 데이터 정합성 관리가 어렵습니다
  • 부분 업데이트가 비효율적일 수 있습니다 (MySQL 8.0에서 개선)
  • 자주 검색하는 키는 가상 컬럼 + 인덱스를 사용합니다

실무에서 자주 하는 실수

1. 필요 이상으로 큰 타입 사용

SQL
-- 나쁜 예: 상태값에 VARCHAR 사용
status VARCHAR(20)  -- 'active', 'inactive' 저장

-- 좋은 예: TINYINT 사용
status TINYINT NOT NULL DEFAULT 1  -- 1=active, 0=inactive

2. IP 주소를 VARCHAR로 저장

SQL
-- 나쁜 예: VARCHAR(15) — 최대 15바이트
ip_address VARCHAR(15)

-- 좋은 예: INT UNSIGNED — 4바이트
ip_address INT UNSIGNED
-- INET_ATON('192.168.1.1') → 3232235777
-- INET_NTOA(3232235777) → '192.168.1.1'

3. 모든 문자열에 VARCHAR(255) 사용

SQL
-- 나쁜 예: 모두 255
name VARCHAR(255),
phone VARCHAR(255),
zip_code VARCHAR(255)

-- 좋은 예: 적절한 크기 지정
name VARCHAR(50),
phone VARCHAR(20),
zip_code CHAR(5)

VARCHAR(255)가 직접 디스크 공간에서 손해를 보지는 않지만, 메모리 할당(임시 테이블, 정렬)에서는 최대 크기 기준으로 할당되므로 성능에 영향을 줄 수 있습니다.

정리

  • 정수는 필요한 범위에 맞는 가장 작은 타입을 선택합니다
  • 금액 계산에는 반드시 DECIMAL을 사용합니다 (FLOAT/DOUBLE 금지)
  • 고정 길이는 CHAR, 가변 길이는 VARCHAR, 대용량 텍스트는 TEXT를 사용합니다
  • 타임존 변환이 필요하면 TIMESTAMP, 순수 날짜/시간이면 DATETIME을 사용합니다
  • ENUM은 값이 고정적일 때만 사용하고, 자주 변하면 참조 테이블을 고려합니다
  • SELECT *를 지양하고, 특히 TEXT/BLOB 컬럼이 있는 테이블에서는 필요한 컬럼만 선택합니다
댓글 로딩 중...