데이터 타입 — 무엇을 골라야 후회가 없나
PostgreSQL 의 핵심 데이터 타입 7묶음과 실전 선택 가이드.
데이터 타입은 한 번 정하면 바꾸기 비싸요. 100만 건 쌓인 뒤 ALTER TABLE 로 타입을 바꾸려면 락이 걸리고 시간이 오래 걸립니다. 그래서 4편은 처음에 잘 고르는 법을 정리합니다. PostgreSQL 의 수십 개 타입 중 실무 90% 를 차지하는 7묶음만 쳐냅니다.
숫자 — integer · numeric
| 타입 | 범위 / 정밀도 | 언제 쓰나 |
|---|---|---|
| smallint | -32K~32K | 매우 작은 enum 등 |
| integer | ±21억 | 대부분의 PK·카운터 |
| bigint | ±9 × 10¹⁸ | 대용량 서비스 PK (권장) |
| numeric(p,s) | 소수 정확 (느림) | 돈·세금·정확 계산 |
| real / double precision | 부동소수 | 측정값·과학 계산 |
| serial / bigserial | 자동 증가 | 레거시 패턴 — 새 코드는 IDENTITY 권장 |
-- 돈은 항상 numeric 으로
price NUMERIC(10, 2) -- 10자리, 소수 2자리: 99999999.99 까지
-- float 의 함정
SELECT 0.1 + 0.2; -- 0.30000000000000004
SELECT 0.1::numeric + 0.2; -- 0.3 (정확)
문자열 — text 만 기억해도 된다
-- 거의 모든 경우 text
description TEXT
-- varchar(N) 도 있지만 PostgreSQL 에선 text 와 성능 차이 거의 없음
-- 길이 제약을 정말 강제하고 싶을 때만
code VARCHAR(8)
-- char(N) 은 피하세요 (남는 자리를 공백으로 채움)
한 줄. "PostgreSQL 에서는 text 만 써도 된다." MySQL 출신은 varchar(N) 에 익숙하지만, Postgres 에서는 길이 제약이 곧 성능이 아닙니다. 사용자 입력 검증은 애플리케이션 레이어에서, 정말 DB 레벨이 필요하면 CHECK (length(x) <= 100) 가 더 유연합니다.
시간 — timestamptz 가 거의 정답
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- 'with time zone' — UTC 로 저장, 클라이언트 시간대로 출력
-- timestamp (without tz) — 권장하지 않음
-- date — 날짜만, time — 시간만, interval — 기간
왜 timestamptz 인가. Postgres 는 timestamptz 를 항상 UTC 로 저장합니다. 클라이언트가 SET TIME ZONE 'Asia/Seoul' 만 하면 자동으로 한국 시간으로 보입니다. timestamp(없는 버전) 은 시간대 정보가 사라져서, 서버 이전·DST·국제 사용자에서 버그의 온상이 됩니다.
boolean · uuid · enum
is_active BOOLEAN NOT NULL DEFAULT true
-- uuid — 분산 환경 PK 로 자주
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
-- 사전: CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- enum (CREATE TYPE)
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'done');
status order_status NOT NULL DEFAULT 'pending';
enum vs check 제약 vs lookup 테이블: enum 은 값 추가가 무겁고(ALTER TYPE … ADD VALUE), 변경이 잦으면 lookup 테이블이 낫습니다. 작고 안정적인 상태값에 enum, 자주 추가될 분류는 별도 테이블.
jsonb — 구조 자유 데이터
attributes JSONB NOT NULL DEFAULT '{}'::jsonb
-- 입력
INSERT INTO products (name, attributes)
VALUES ('티셔츠', '{"size":"L","color":"black","tags":["sale","new"]}');
-- 조회 (12편에서 자세히)
SELECT name, attributes->>'color' AS color
FROM products
WHERE attributes @> '{"size":"L"}';
-- 인덱스
CREATE INDEX ix_products_attrs ON products USING GIN (attributes);
json vs jsonb: 항상 jsonb 입니다. json 은 텍스트 그대로, jsonb 는 파싱된 바이너리 — 저장은 살짝 느리고 조회·연산은 훨씬 빠릅니다.
jsonb 의 함정. 편하다고 모든 걸 jsonb 에 넣지 마세요. 자주 검색·정렬하는 필드는 정식 컬럼으로 빼는 게 거의 항상 좋습니다. jsonb 는 "스키마 변경 비용 없이 가벼운 메타 정보" 용도가 본업입니다.
배열 — 정렬된 같은 타입 묶음
tags TEXT[] -- 문자열 배열
scores INTEGER[] -- 정수 배열
INSERT INTO posts (title, tags) VALUES ('첫글', ARRAY['intro','draft']);
-- 검색
SELECT * FROM posts WHERE 'draft' = ANY(tags);
-- 인덱스
CREATE INDEX ix_posts_tags ON posts USING GIN (tags);
관계형 DB 정통 패턴은 별도 테이블이지만, "태그·역할·플래그 같은 간단한 다중 값" 은 배열이 훨씬 간결합니다. 검색·JOIN 이 복잡해지면 그때 별도 테이블로 분리.
실전 선택 — "처음 만드는 사람" 의 체크리스트
- PK →
BIGINT+GENERATED ALWAYS AS IDENTITY(또는 BIGSERIAL). - 분산·외부 노출 ID →
UUID+gen_random_uuid(). - 문자열 →
TEXT. 길이 검증은 앱에서. - 시간 →
TIMESTAMPTZ항상.NOW()가 기본값. - 돈 →
NUMERIC(p, s). float 금지. - 구조 자유 메타 →
JSONB+ GIN. - 상태값 → 처음엔
TEXT CHECK, 안정되면 enum/lookup.
5편 — 첫 CRUD (INSERT·SELECT·UPDATE·DELETE)
이제 만든 테이블에 데이터를 넣고·읽고·바꾸고·지워봅니다. 안전한 UPDATE/DELETE 의 단 한 가지 규칙도 같이.
이전: 3편 DB·테이블 · 현재: 4편 (입문) · 다음 → 5편 CRUD · 진행: 4/24