본문으로 건너뛰기
7.2 range / list / hash 파티션

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+의 선언적 파티션의 차이·마이그레이션을 봅니다.