7.1 파티셔닝 개요와 결정 기준
테이블 한 개가 수억~수십억 row로 자라면 단일 테이블로는 운영이 어려워집니다. 인덱스 BLOAT, VACUUM 시간, 백업·복구, 보존 정책 적용 — 모두 비용이 폭증합니다. 파티셔닝(partitioning)은 큰 테이블을 작은 자식 테이블 여러 개로 물리적으로 나누고, 사용자에게는 하나의 테이블처럼 보이게 합니다. 파티셔닝의 개념과 도입할지 말지의 결정 기준을 정리합니다.
개념
flowchart TD
P["events (parent)<br/>partitioned by RANGE(created_at)"]
C1["events_2025_q4<br/>(2025-10 ~ 2026-01)"]
C2["events_2026_q1<br/>(2026-01 ~ 2026-04)"]
C3["events_2026_q2<br/>(2026-04 ~ 2026-07)"]
P --> C1
P --> C2
P --> C3
classDef parent fill:#ede9fe,stroke:#6d28d9,color:#3b0764,stroke-width:2px
classDef child fill:#d1fae5,stroke:#047857,color:#064e3b
class P parent
class C1,C2,C3 child
- 부모 테이블
events는 레코드를 직접 저장하지 않는 경우가 많다 (declarative partition) - INSERT/SELECT는 부모 이름으로 — PostgreSQL이 알아서 자식 테이블로 라우팅
- 각 자식은 독립적인 테이블 — 자체 인덱스, 자체 VACUUM, 자체 통계
무엇이 좋아지나
| 영역 | 효과 |
|---|---|
| 쿼리 성능 | WHERE created_at >= '2026-04-01'은 한 파티션만 스캔 (partition pruning) |
| VACUUM·인덱스 | 파티션 단위로 vacuum·reindex 가능. 한 파티션 BLOAT가 다른 파티션 영향 없음 |
| 데이터 보존 정책 | 오래된 파티션을 DETACH PARTITION + DROP으로 한 번에 제거 |
| 백업 | 파티션 단위로 백업·복원 가능 (특히 cold 파티션은 read-only로 분리) |
| 인덱스 빌드 | 큰 인덱스를 파티션 단위로 작은 인덱스 여러 개로 → 더 빠르고 BLOAT 덜 위험 |
| 통계 | 파티션별 통계로 옵티마이저 정확도 ↑ |
무엇이 안 좋아지나
| 측면 | 단점 |
|---|---|
| 글로벌 unique constraint | 파티션 키를 포함해야 unique 가능. (id, partition_key)로 보내는 식 |
| 외래 키 | 파티션된 부모를 가리키는 FK는 PG 12+에서 지원 — 그 전엔 불가 |
| partition pruning이 안 되는 쿼리 | WHERE func(created_at) > ... 같은 표현식이면 모든 파티션 스캔 |
| 파티션 수가 너무 많음 | 100+ 파티션이면 plan 시간이 커지고 카탈로그 비대 |
| 운영 복잡도 | 파티션 생성·정리·이름 규칙·자동화 필요 |
| 변경 SQL의 락 | 부모에 ALTER TABLE은 모든 자식에 영향 |
도입할까 말까 — 결정 기준
| 조건 | 판단 |
|---|---|
| 테이블 크기 < 50GB | 파티셔닝 안 함 — 단일 테이블 + 인덱스로 충분 |
| 50GB ~ 200GB | 보존 정책·인덱스 관리 부담이 있으면 검토 |
| 200GB+ | 거의 항상 파티셔닝 |
| 보존 기간이 명확 (90일·1년 등) | 파티셔닝이 정석 — 만료 데이터 한 번에 제거 |
| 시간 기반 쿼리가 대부분 | range 파티션 |
| tenant 별 격리 + 쿼리 | list 또는 hash 파티션 |
| 단순 분산 부하 | hash 파티션 |
| 매우 random insert + lookup | 파티셔닝 효과 작음 |
| 글로벌 unique 제약 필수 | 신중히 검토 — 파티션 키 추가 가능한지 |
파티션 키 선택
키 선택은 한 번 정하면 바꾸기 어렵다(파티션 키 변경 = 테이블 재구성). 신중히 진행합니다.
좋은 키의 조건:
- 쿼리 WHERE 절에 자주 등장
- 시간순 등 자연 분포가 있음
- NOT NULL (NULL은 DEFAULT 파티션이 필요)
- 변경되지 않음 (UPDATE로 파티션 이동 가능하지만 비용 큼)
시계열 데이터는 created_at 또는 event_date 가 표준입니다. 멀티 테넌트는 tenant_id, 분산은 id % N(hash).
파티션 타입 미리보기
세 종류 — 자세한 동작은 다음 절.
| 타입 | 분할 기준 | 사용처 |
|---|---|---|
| RANGE | 값의 범위 | 시계열·연속값 |
| LIST | 값의 명시적 목록 | tenant·국가·카테고리 |
| HASH | 해시값 모듈로 | 부하 분산 |
파티션 단위 결정 — 너무 많지도 적지도
| 단위 | 권장 시나리오 |
|---|---|
| 일 | 매일 보존·삭제. 데이터량 작거나 1년치만 보유 |
| 주 | 1~2년치 데이터 |
| 월 | 가장 흔한 — 1~5년 데이터 |
| 분기 | 5년+ 장기 보유 |
| 년 | 매우 cold한 데이터 |
너무 잘게 자르면 파티션 수 폭증 → 플래너 비용·카탈로그 비대. 너무 듬성하면 보존 단위가 크고 한 파티션이 너무 큽니다.
경험치: 한 테이블에 활성 파티션 50개 이하가 가장 운영하기 좋습니다. 데이터 보유 5년 + 월 파티션 = 60개로 약간 넘는다 — 오래된 건 분기로 합치는 패턴(rolling).
운영 자동화의 필수성
파티션 자체를 만드는 건 쉽지만, 매일·매월 새 파티션을 만들고 오래된 파티션을 정리하는 일을 사람이 할 수는 없습니다. 자동화 도구:
pg_partman: 표준입니다. 시간 기반 자동 생성·삭제·archive. 7.5에서 자세히- 자체 cron + SQL: 작은 시스템에서 가능
- Citus/TimescaleDB: 시계열 특화 확장이 자체 관리
정리
- 파티셔닝 = 큰 테이블을 작은 자식 여러 개로 물리 분할
- 50GB 이하면 불필요, 200GB+면 거의 필수
- 시계열·보존 정책·VACUUM 부담이 자주 도입 사유
- 파티션 키는 NOT NULL · 자주 WHERE에 등장 · 변경 없음 조건
- 단위는 보통 월. 활성 파티션 50개 이하 권장
- 자동화(pg_partman) 없이는 운영 부담이 커서 사실상 도입 안 됨
다음 절(7.2)에서는 세 가지 파티션 타입 — RANGE / LIST / HASH — 의 동작을 봅니다.