백엔드 데이터 인프라 32편. PG 데이터 타입의 큰 그림 — 숫자·문자·날짜·UUID·배열·범위 등 카테고리별 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 32편이에요. 9편 CREATE TABLE 에서 "자주 쓰는 6가지 타입" 을 다뤘으니, 이번 32편은 그 위에 — PG(PostgreSQL의 줄임말) 전체 타입 카테고리.
PG 타입의 카테고리
PG는 40+ 내장 타입 + 사용자 정의 무한. 카테고리별:
| 카테고리 | 예 |
|---|---|
| 숫자 | SMALLINT·INTEGER·BIGINT·NUMERIC·REAL·DOUBLE PRECISION |
| 문자 | CHAR·VARCHAR·TEXT |
| 날짜·시간 | DATE·TIME·TIMESTAMP·TIMESTAMPTZ·INTERVAL |
| 불리언 | BOOLEAN |
| UUID | UUID |
| JSON | JSON·JSONB |
| 배열 | INT[]·TEXT[] 등 |
| 범위 | INT4RANGE·TSTZRANGE 등 |
| 네트워크 | INET·CIDR·MACADDR |
| 기하 | POINT·LINE·POLYGON |
| 이진 | BYTEA |
| 열거형 | ENUM |
| 복합 | Composite Types |
| 도메인 | CREATE DOMAIN |
실제로 자주 쓰는 건 숫자·문자·날짜·BOOLEAN·UUID·JSONB·배열 정도예요.
숫자 타입
| 타입 | 크기 | 범위 |
|---|---|---|
| SMALLINT | 2바이트 | -32768 ~ 32767 |
| INTEGER / INT | 4바이트 | -2.1B ~ 2.1B |
| BIGINT | 8바이트 | -9.2 × 10^18 ~ |
| NUMERIC(p, s) | 가변 | 정확한 소수 |
| REAL | 4바이트 | 부동소수 |
| DOUBLE PRECISION | 8바이트 | 부동소수 |
| SERIAL / BIGSERIAL | 4·8바이트 | 자동 증가 정수 |
| IDENTITY | INTEGER·BIGINT 기반 | 자동 증가 SQL 표준 |
정수 — INTEGER vs BIGINT
- INTEGER = 21억 한계. 작은 카운터·플래그.
- BIGINT = 거의 무한. 기본 키·통계 모두 — 한국 회사 표준.
소수 — NUMERIC vs FLOAT
NUMERIC(10, 2) -- 10자리 중 소수 2자리 (정확)
DOUBLE PRECISION -- 부정확 (부동소수)
금액·통화 = NUMERIC 무조건. 0.1 + 0.2 ≠ 0.3 함정.
SELECT 0.1::DOUBLE PRECISION + 0.2::DOUBLE PRECISION;
-- 0.30000000000000004 (부정확)
SELECT 0.1::NUMERIC + 0.2::NUMERIC;
-- 0.3 (정확)
문자 타입
| 타입 | 의미 |
|---|---|
| CHAR(N) / CHARACTER(N) | 고정 길이, 공백 패딩 |
| VARCHAR(N) / CHARACTER VARYING | 가변 (길이 제한) |
| TEXT | 무제한 가변 |
9편 에서 다뤘듯이 — PG 표준 = TEXT. 길이 제한은 CHECK 제약으로 잡아요.
title TEXT CHECK (char_length(title) <= 200)
성능 차이가 없어서 CHAR·VARCHAR 는 거의 안 써요.
citext — 대소문자 무관
CREATE EXTENSION citext;
CREATE TABLE users (
email CITEXT UNIQUE
);
INSERT INTO users (email) VALUES ('Alice@Example.com');
SELECT * FROM users WHERE email = 'alice@example.com'; -- 매칭!
이메일 대소문자 함정을 피할 수 있어요.
날짜·시간 타입
| 타입 | 의미 | 크기 |
|---|---|---|
| DATE | 날짜만 | 4바이트 |
| TIME | 시간만 (시간대 X) | 8바이트 |
| TIMETZ | 시간 + 시간대 | 12바이트 (안 권장) |
| TIMESTAMP | 날짜·시간 (시간대 X) | 8바이트 |
| TIMESTAMPTZ | 시간대 포함 | 8바이트 |
| INTERVAL | 기간 | 16바이트 |
TIMESTAMP vs TIMESTAMPTZ
거의 항상 TIMESTAMPTZ. "시간대 포함" 이 안전.
-- TIMESTAMP (시간대 X)
'2026-05-17 14:00:00'::TIMESTAMP
-- 어느 시간대인지 모름
-- TIMESTAMPTZ (시간대 포함)
'2026-05-17 14:00:00 KST'::TIMESTAMPTZ
-- 한국 시각 명확
PG 내부는 TIMESTAMPTZ 도 UTC 로 저장해두고, 조회할 때 클라이언트 시간대로 변환해줘요.
INTERVAL — 기간
INTERVAL '7 days'
INTERVAL '1 month'
INTERVAL '2 years 3 months'
INTERVAL '1 hour 30 minutes'
-- 연산
NOW() + INTERVAL '7 days' -- 7일 후
created_at - INTERVAL '30 days' -- 30일 전
AGE(NOW(), birth) -- 나이 계산
기간을 다루기 좋은 강력한 도구예요.
DATE_TRUNC·EXTRACT
DATE_TRUNC('day', created_at) -- 일 단위로 자르기
DATE_TRUNC('month', created_at) -- 월 단위
EXTRACT(YEAR FROM birth) -- 연도만
EXTRACT(EPOCH FROM (NOW() - start)) -- 초로 변환
대시보드·통계에서 매일 쓰게 돼요.
BOOLEAN
is_active BOOLEAN NOT NULL DEFAULT TRUE
값은 TRUE·FALSE·NULL. 단축으로 't'·'f'·'yes'·'no'·1·0 도 받아요.
UUID — 분산 표준
CREATE EXTENSION "uuid-ossp";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
...
);
분산 시스템·마이크로서비스에서는 UUID 가 우세해요. BIGSERIAL 의 "순차 증가 충돌" 도 피할 수 있고요.
장단점: - ✓ 분산 시스템에서 고유 - ✓ 노출돼도 비밀번호 추측 X - ✗ 16바이트 (BIGINT 8 의 2배) - ✗ 정렬 비효율 (시계열 인덱스에 약함)
해결 = UUIDv7 (시간 정렬 UUID, PG 18+ 또는 확장).
JSON·JSONB
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
payload JSONB
);
INSERT INTO events (payload) VALUES ('{"user": 1, "action": "click"}'::JSONB);
차이: - JSON = 텍스트 저장, 원본 포맷 보존 - JSONB = 이진 저장, 인덱스 가능, 키 정렬, 거의 항상 JSONB
연산자:
payload -> 'user' -- JSON 추출
payload ->> 'user' -- 텍스트 추출
payload #> '{a,b}' -- 깊은 JSON
payload @> '{"x": 1}' -- 포함
payload ? 'key' -- 키 존재
33편에서 깊이.
배열 — INT[]·TEXT[] 등
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
tags TEXT[]
);
INSERT INTO posts (tags) VALUES (ARRAY['java', 'spring', 'postgres']);
-- 검색
SELECT * FROM posts WHERE 'java' = ANY(tags);
SELECT * FROM posts WHERE tags @> ARRAY['java'];
SELECT * FROM posts WHERE tags && ARRAY['java', 'python']; -- 겹침
-- GIN(범용 역색인) 인덱스
CREATE INDEX idx_posts_tags ON posts USING GIN (tags);
태그나 작은 컬렉션에 아주 자주 써요.
범위 — RANGE
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
period TSTZRANGE NOT NULL,
EXCLUDE USING GIST (period WITH &&) -- 겹침 방지
);
INSERT INTO bookings (period)
VALUES ('[2026-05-17 10:00 KST, 2026-05-17 12:00 KST)');
타입:
- INT4RANGE — 정수 범위
- INT8RANGE — BIGINT
- NUMRANGE — NUMERIC
- TSRANGE — TIMESTAMP
- TSTZRANGE — TIMESTAMPTZ (자주)
- DATERANGE — DATE
23편 EXCLUDE 의 핵심 도구.
네트워크 — INET·CIDR
ip_address INET -- IP 주소
network CIDR -- 네트워크
-- 연산
SELECT * FROM logs WHERE ip << '192.168.1.0/24'::CIDR; -- 서브넷 포함
CDN·방화벽·접근 제어 로그에서 자주 만나요.
ENUM
CREATE TYPE order_status AS ENUM ('PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELED');
CREATE TABLE orders (
status order_status NOT NULL
);
장단점: - ✓ 타입 안전 - ✗ 값 변경·삭제 어려움 - ✗ JPA(자바 ORM 표준) 매핑 조금 복잡
대안은 TEXT + CHECK 가 더 유연해요.
함정 5가지
(1) VARCHAR(N) 박기
MySQL 습관. PG = TEXT 우세.
(2) FLOAT 로 금액
0.1 + 0.2 ≠ 0.3. NUMERIC 무조건.
(3) TIMESTAMP (without TZ)
시간대 모호. TIMESTAMPTZ 표준.
(4) JSON (not JSONB)
인덱스 X·키 순서 보존 (의미 X). JSONB 표준.
(5) ENUM 값 변경 어려움
한 번 박은 ENUM 값은 거의 못 빼요. "상태 집합이 영구히 변하지 않는다" 는 확신이 없다면 TEXT + CHECK 가 무난해요.
기본 키 = BIGINT IDENTITY 또는 UUID. 문자 = TEXT + CHECK. 시각 = TIMESTAMPTZ. 금액 = NUMERIC. JSON = JSONB. 상태 = TEXT + CHECK (ENUM 신중).
한 줄 정리 — PG 풍부한 타입. 한국 회사 표준 = BIGINT·TEXT·TIMESTAMPTZ·NUMERIC·JSONB·UUID. VARCHAR·FLOAT·TIMESTAMP·JSON·ENUM 은 함정. 배열·범위·네트워크·기하 등 PG 특별 타입은 강력한 도구.
시험 직전 한 번 더 — 데이터 타입 입문자가 매번 헷갈리는 것
- 숫자 = SMALLINT·INTEGER·BIGINT·NUMERIC·REAL·DOUBLE PRECISION
- 한국 표준 = BIGINT 기본 키, NUMERIC 금액
- FLOAT 로 금액 X (
0.1 + 0.2 ≠ 0.3) - TEXT = PG 문자열 표준 (CHAR·VARCHAR 거의 X)
- TEXT 길이 = CHECK 제약
- citext = 대소문자 무관 TEXT (이메일)
- TIMESTAMPTZ = 시간대 포함 (표준)
- TIMESTAMP = 시간대 X (위험)
- INTERVAL = 기간 (
INTERVAL '7 days') - DATE_TRUNC·EXTRACT = 시간 조작
- BOOLEAN = TRUE·FALSE·NULL
- UUID = 분산 시스템 (uuid-ossp)
- UUIDv7 = 시간 정렬 (PG 18+)
- JSONB = 이진 JSON·인덱스·키 정렬
- JSON = 텍스트 보존 (거의 안 씀)
- JSONB 연산자 =
->·->>·@>·? - 배열 =
TEXT[]·INT[] - ANY·@>·&& 연산자
- GIN 인덱스 = 배열·JSONB
- 범위 타입 =
TSTZRANGE·INT4RANGE - EXCLUDE 제약과 짝
- INET·CIDR = IP·네트워크
- ENUM = 안정적 상태만 (값 변경 어려움)
- TEXT + CHECK = 더 유연
- 기하 = POINT·LINE·POLYGON (PostGIS 확장)
- BYTEA = 바이너리 (이미지·파일 — 작은 것만)
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 27편 — DELETE 깊이 bloat·VACUUM·파티션 DROP
- 28편 — 쿼리 개요 SELECT 전체 구조
- 29편 — SELECT 절과 표현식의 깊이
- 30편 — 테이블 표현식 FROM 절의 깊이
- 31편 — 쿼리 종합 WHERE GROUP BY HAVING ORDER BY
다음 글: