같은 데이터를 저장하더라도 타입 선택에 따라 저장 공간이 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)가 직접 디스크 공간에서 손해를 보지는 않지만, ** 메모리 할당 **(임시 테이블, 정렬)에서는 최대 크기 기준으로 할당되므로 성능에 영향을 줄 수 있습니다.

주의할 점

FLOAT/DOUBLE로 금액을 계산하면 1원이 사라진다

0.1 + 0.2 = 0.30000000000000004처럼 부동소수점 오차가 발생합니다. 금액, 환율, 세금 등 정확한 계산이 필요한 곳에서는 반드시 DECIMAL을 사용해야 합니다.

TIMESTAMP는 2038년까지만 표현 가능하다

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

VARCHAR(255)를 습관적으로 쓰면 메모리에서 손해를 본다

디스크 저장 공간은 실제 데이터 크기만큼만 사용하지만, 임시 테이블과 정렬 버퍼에서는 최대 크기 기준으로 메모리가 할당됩니다. 적절한 크기를 지정해야 합니다.

정리

항목설명
정수필요한 범위에 맞는 가장 작은 타입 선택
DECIMAL금액/환율에 필수, FLOAT/DOUBLE은 부동소수점 오차
CHAR vs VARCHAR고정 길이 → CHAR, 가변 길이 → VARCHAR
TEXT대용량 텍스트, 인덱스는 접두사만 가능, SELECT * 지양
TIMESTAMP vs DATETIME타임존 변환 → TIMESTAMP(2038년 제한), 순수 날짜 → DATETIME
ENUM값 고정적일 때만, 자주 변하면 참조 테이블 사용
댓글 로딩 중...