PostgreSQL 실전 강좌 #7 파티셔닝: 거대 테이블을 시간 단위로 나누기
이벤트, 로그, 주문처럼 시간과 함께 무한히 쌓이는 테이블은 언젠가 수억 행이 됩니다. 인덱스(실전 4편의 BRIN까지 포함해)로 조회는 버텨도, 두 가지가 점점 무거워집니다. 오래된 데이터의 삭제와 VACUUM입니다. 이 두 문제의 표준 처방이 파티셔닝, 즉 하나의 논리 테이블을 물리적으로 여러 조각으로 나누는 것입니다.
파티셔닝이 해결하는 것 #
- 오래된 데이터 삭제가 DROP 한 방이 됩니다. 이것이 실무 가치의 절반 이상입니다. “1년 지난 이벤트 삭제"를 DELETE로 하면 수천만 행의 dead tuple이 생기고 VACUUM이 그 뒷수습을 해야 하지만, 월별 파티션이라면
DROP TABLE events_2025_08;한 문장으로 즉시, dead tuple 없이 끝납니다. - 파티션 프루닝: 쿼리 조건에 파티션 키가 있으면 해당 파티션만 스캔합니다. 12개월치 테이블에서 이번 달 조회가 1/12만 읽게 됩니다.
- 관리 단위가 작아집니다: VACUUM·ANALYZE·REINDEX가 파티션 단위로 돌므로, 한 번의 작업이 가볍고 병렬화됩니다.
해결하지 않는 것도 분명히 해 두겠습니다. 파티셔닝은 성능 만능약이 아닙니다. 파티션 키 조건이 없는 쿼리는 모든 파티션을 스캔하므로 오히려 오버헤드가 붙고, 단일 행 조회는 인덱스가 이미 빠르므로 나눈다고 더 빨라지지 않습니다. 목적은 “거대 테이블의 수명 주기 관리"이지 “느린 쿼리 튜닝"이 아닙니다.
선언적 파티셔닝: 월별 RANGE #
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
user_id bigint NOT NULL,
payload jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id, created_at) -- 파티션 키가 PK에 포함되어야 합니다
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_08 PARTITION OF events
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE events_2026_09 PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');애플리케이션은 그냥 events에 넣고 조회합니다. 라우팅은 PostgreSQL이 합니다. 문법상 걸리는 지점은 PK입니다. PK·UNIQUE 제약에는 파티션 키가 포함되어야 합니다(각 파티션이 독립 테이블이라 전역 유일성을 검사할 수 없기 때문입니다). 그래서 (id, created_at) 복합 PK가 되는데, id 자체의 전역 유일성은 IDENTITY 시퀀스가 사실상 보장하지만 DB 제약 차원에서는 느슨해진다는 것, 이것이 파티셔닝의 대표적인 트레이드오프입니다. RANGE 외에 LIST(지역·테넌트별)와 HASH(균등 분산)도 있지만, 실무의 8할은 시간 RANGE입니다.
프루닝이 되는 조건 #
프루닝은 공짜가 아니라 쿼리 조건이 파티션 키를 직접 좁힐 때만 작동합니다.
-- 프루닝 됩니다: created_at 조건이 직접
SELECT * FROM events WHERE created_at >= '2026-08-01' AND user_id = 42;
-- 프루닝 안 됩니다: 키 조건이 없음 → 전 파티션 스캔
SELECT * FROM events WHERE user_id = 42;기초 4편의 “컬럼을 가공하면 인덱스를 사용할 수 없다"와 같은 계열로, date_trunc('month', created_at) = ... 같은 가공 조건도 프루닝을 막습니다. 확인 방법은 언제나처럼 EXPLAIN입니다. 계획에 파티션이 몇 개 나타나는지 보면 됩니다. 따라서 파티셔닝은 도입 전에 쿼리 패턴을 함께 검토해야 합니다. 주요 쿼리에 시간 조건이 없는 테이블은 시간 파티셔닝의 후보가 아닙니다.
운영: 파티션은 계속 만들어야 합니다 #
월별 파티셔닝의 숙제는 “다음 달 파티션을 누가 만드는가"입니다. 만들어 두지 않으면 다음 달 첫 INSERT가 실패합니다(받아 줄 파티션이 없으므로. DEFAULT 파티션을 두면 에러는 피하지만 거기 쌓인 데이터는 나중에 옮기는 비용이 더 큽니다). 손으로 하는 운영은 반드시 잊히므로, 표준은 pg_partman 확장입니다. “미래 몇 개를 미리 만들고, 몇 개월 지난 것은 떼어 낸다"를 선언해 두면 주기적으로 알아서 굴립니다. 관리형 서비스에서도 대부분 지원합니다.
도입 기준을 정리하면 이렇습니다. 시간과 함께 계속 쌓이고, 보존 기한이 있으며, 주요 쿼리에 시간 조건이 있는 테이블이 파티셔닝에 적합합니다. 수천만 행 수준에서 미리 도입하기보다, 이 세 조건이 보이는 테이블이 수억 행에 이르기 전에 도입하는 것이 실무적인 시점입니다.
정리 #
- 파티셔닝의 최대 가치는 삭제입니다. DELETE + VACUUM 지옥이 DROP TABLE 한 방이 됩니다.
- 프루닝은 파티션 키를 직접 좁히는 조건에서만 작동합니다. 키 조건 없는 쿼리는 오히려 전 파티션을 스캔합니다.
- PK에는 파티션 키가 포함되어야 합니다. 전역 유일 제약의 느슨해짐이 대표 트레이드오프입니다.
- 파티션 생성은 자동화가 필수입니다. pg_partman으로 “미리 만들고 기한 지나면 떼기"를 선언합니다.
- 도입 기준은 무한 증가 + 보존 기한 + 시간 조건 쿼리의 3종 세트입니다. 다음 편은 복제와 고가용성입니다.