7.2 range / list / hash 파티션
PostgreSQL의 declarative partitioning은 세 가지 분할 방식을 지원한다 — RANGE, LIST, HASH. 각각의 동작·사용 사례·문법을 본 절에서 정리합니다.
RANGE — 범위 분할
값을 연속된 범위로 나눕니다. 시계열·연속된 정수·날짜에 표준입니다.
-- 부모 테이블 (스키마만 정의, 데이터 없음)
CREATE TABLE events (
id bigserial,
created_at timestamptz NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (id, created_at) -- 파티션 키 포함 필요
) PARTITION BY RANGE (created_at);
-- 월 단위 파티션
CREATE TABLE events_2026_05
PARTITION OF events
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE events_2026_06
PARTITION OF events
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- DEFAULT 파티션 — 어디에도 안 맞는 row를 받음
CREATE TABLE events_default
PARTITION OF events
DEFAULT;FROM은 포함, TO는 미포함 — half-open interval [from, to).
| 특징 | 메모 |
|---|---|
| 범위 겹침 안 됨 | 한 row가 정확히 한 파티션에 속함 |
| 빈 구간 가능 | DEFAULT 파티션이 받거나 INSERT 실패 |
| 다중 컬럼 키 가능 | PARTITION BY RANGE (year, month) 같은 식 |
사용 사례
- 시계열 (
created_at,event_date) - 정수 ID 범위 (1억 단위로 분할)
- 가격·금액 범위
LIST — 명시적 목록 분할
각 파티션이 허용하는 값 목록을 명시합니다.
CREATE TABLE orders (
id bigserial,
country text NOT NULL,
amount numeric,
PRIMARY KEY (id, country)
) PARTITION BY LIST (country);
CREATE TABLE orders_kr PARTITION OF orders FOR VALUES IN ('KR');
CREATE TABLE orders_jp PARTITION OF orders FOR VALUES IN ('JP');
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US', 'CA'); -- 여러 값 한 파티션
CREATE TABLE orders_default PARTITION OF orders DEFAULT;사용 사례
- 국가·지역·tenant ID 같은 카테고리
- 매우 적은 distinct 값을 갖는 컬럼
LIST는 값 추가 시 ALTER TABLE로 새 파티션을 만들거나 DEFAULT에 들어갑니다.
HASH — 해시 모듈로 분할
값을 해시한 뒤 모듈로 N으로 분할.
CREATE TABLE users (
id bigint PRIMARY KEY,
email text,
...
) PARTITION BY HASH (id);
CREATE TABLE users_p0 PARTITION OF users FOR VALUES WITH (modulus 8, remainder 0);
CREATE TABLE users_p1 PARTITION OF users FOR VALUES WITH (modulus 8, remainder 1);
-- ... users_p7까지
CREATE TABLE users_p7 PARTITION OF users FOR VALUES WITH (modulus 8, remainder 7);| 특징 | 메모 |
|---|---|
| 균등 분포 보장 | 해시이므로 모든 파티션에 비슷한 row 수 |
| 보존 정책에 부적합 | 시간 순이 아니라 오래된 데이터 그룹이 없음 |
| 파티션 수 변경이 어려움 | modulus를 늘리면 모든 파티션 재구성 필요 |
사용 사례
- 부하 분산이 필요한 큰 테이블 (시간 키가 없을 때)
- 멀티 디스크에 데이터 균등 분포 (각 파티션을 다른 tablespace로)
어떤 타입을 고르나
flowchart TD
Q{"파티션 키가 …"}
Q -- "시간/연속값" --> R["RANGE"]
Q -- "카테고리 (tenant, country)" --> L["LIST"]
Q -- "균등 분산이 목표, 의미 없는 키" --> H["HASH"]
Q -- "둘 다 필요 (시간 + tenant)" --> SUB["RANGE → 하위 파티션을 LIST/HASH"]
classDef typ fill:#d1fae5,stroke:#047857,color:#064e3b
classDef sub fill:#fed7aa,stroke:#c2410c,color:#7c2d12
classDef q fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
class R,L,H typ
class SUB sub
class Q q
서브파티셔닝 (2단계)
큰 시계열 + 멀티 테넌트는 RANGE 안에 LIST/HASH를 두 단계로.
CREATE TABLE events (
id bigserial,
tenant_id int NOT NULL,
created_at timestamptz NOT NULL,
PRIMARY KEY (id, tenant_id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_05 PARTITION OF events
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01')
PARTITION BY LIST (tenant_id);
CREATE TABLE events_2026_05_t1 PARTITION OF events_2026_05 FOR VALUES IN (1);
CREATE TABLE events_2026_05_t2 PARTITION OF events_2026_05 FOR VALUES IN (2);
-- ...3단계 이상은 운영 복잡도 폭증합니다. 보통 2단계까지.
인덱스
파티션된 부모에 인덱스를 만들면 모든 자식에 자동 생성된다 (PG 11+).
CREATE INDEX ON events (created_at);
-- 각 events_YYYY_MM에도 인덱스가 자동으로 만들어짐부모 인덱스 자체는 가상이고 실제 데이터는 자식 인덱스에 있습니다. unique 인덱스는 파티션 키를 포함해야 가능:
CREATE UNIQUE INDEX ON events (id, created_at); -- OK
CREATE UNIQUE INDEX ON events (id); -- ERROR: must include partition key파티션 라우팅
INSERT는 부모 이름으로 보내면 PostgreSQL이 알맞은 파티션에 라우팅합니다.
INSERT INTO events (created_at, payload)
VALUES (now(), '{"type":"click"}');
-- 자동으로 events_2026_05에 들어감직접 자식 테이블에 INSERT도 가능 (성능 차이 거의 없음).
DEFAULT 파티션의 함정
DEFAULT 파티션이 있으면 PostgreSQL은 새 파티션을 ATTACH할 때 DEFAULT의 row 중 새 파티션 범위에 속하는 게 없는지 검사합니다. 큰 DEFAULT 파티션이 있으면 이 검사가 매우 오래 걸립니다.
권장: DEFAULT 파티션은 빈 상태로 유지합니다. 새 범위 들어오기 전 미리 파티션 만들어 둡니다.
파티션 정보 확인
-- 부모의 모든 파티션
SELECT inhrelid::regclass AS partition,
pg_get_expr(c.relpartbound, c.oid) AS bound
FROM pg_inherits i
JOIN pg_class c ON i.inhrelid = c.oid
WHERE inhparent = 'events'::regclass;
-- 각 파티션 크기
SELECT child::regclass AS partition,
pg_size_pretty(pg_total_relation_size(child)) AS size
FROM pg_partition_tree('events')
WHERE level > 0;pg_partition_tree는 PG 12+의 편의 함수.
운영 시 주의
| 주의 | 메모 |
|---|---|
| 파티션 키 변경 = 테이블 재구성 | 도입 전 신중히 결정 |
| 파티션 키 컬럼은 NOT NULL 권장 | NULL은 DEFAULT 파티션이 받지만 의도치 않은 라우팅 |
| RANGE 경계 겹침은 syntax error로 막힘 | 안전 |
부모에 TRUNCATE는 모든 자식까지 적용 | 영향 큰 명령 |
pg_dump는 기본적으로 부모만 dump해도 자식 데이터 다 포함 | 자식 단위 dump 옵션 (-t)도 가능 |
정리
- RANGE = 시간·연속값. 시계열의 표준
- LIST = 카테고리·tenant·국가
- HASH = 균등 분포가 필요할 때
- 부모에 인덱스 만들면 자식 자동 생성
- unique 인덱스는 파티션 키 포함 필수
- DEFAULT 파티션은 비워 두기
- 서브파티셔닝 2단계까지가 운영 한계
다음 절(7.3)에서는 PG 9.x 시대의 상속 기반 파티션과 PG 10+의 선언적 파티션의 차이·마이그레이션을 봅니다.