13.3 파티셔닝을 활용한 튜닝
Part VII에서 파티셔닝의 기본 동작을 봤다면, 파티셔닝이 성능을 어떻게 만들어내는지와 운영 튜닝 포인트. 큰 테이블의 마지막 무기이지만 잘못 쓰면 오히려 손해.
파티셔닝이 성능에 영향을 주는 4가지
| 영역 | 효과 |
|---|---|
| partition pruning | WHERE에 맞는 파티션만 스캔 — 작은 인덱스·작은 데이터 |
| partition-wise join | 같은 파티션끼리 join — 메모리·CPU 효율 |
| partition-wise aggregate | 파티션 단위 부분 집계 → 합산 |
| 인덱스 분할 | 큰 인덱스 1개 → 작은 인덱스 N개. BLOAT 분리, REINDEX 빠름 |
partition pruning 활용
7.4에서 본 가지치기. 효과를 살리는 쿼리 패턴:
| 좋음 | 나쁨 |
|---|---|
WHERE created_at >= '2026-05-01' | WHERE date_trunc('day', created_at) = ... |
WHERE id BETWEEN 1000 AND 2000 (range 파티션) | WHERE id IN (SELECT id FROM ...) (서브쿼리) |
WHERE country = 'KR' (list 파티션) | WHERE country IN (SELECT ...) |
ORM이 자동 생성하는 쿼리가 cast나 함수 호출을 끼우면 pruning 무력화 — 쿼리 패턴 점검합니다.
plan-time vs execution-time
EXPLAIN <쿼리>
-- "Subplans Removed: N" → execution-time pruning
-- Append 아래 자식 1개만 → plan-time pruning (더 좋음)prepared statement의 generic plan은 모든 파티션을 plan에 포함해 plan time 폭증합니다. 운영 권장:
- 매우 많은 파티션 + 자주 호출되는 쿼리 →
plan_cache_mode = force_custom_plan
partition-wise join
SET enable_partitionwise_join = on; -- PG 14+ 기본 on
SELECT *
FROM orders o
JOIN order_items i ON i.order_id = o.id AND i.created_at = o.created_at;
-- 양쪽이 같은 키로 파티션됐다면 — 파티션 단위 join조건:
- 두 테이블이 같은 키로 파티션
- 파티션 경계 완전히 일치
- enable_partitionwise_join = on
효과: 큰 hash table을 만들지 않고 작은 join 여러 개. 메모리·시간 모두 절약합니다.
partition-wise aggregate
SET enable_partitionwise_aggregate = on;
SELECT user_id, count(*)
FROM events
GROUP BY user_id;
-- 파티션별 GROUP BY → 합산효과: parallel scan과 결합되면 매우 큰 데이터셋에서 강력. PG 14+ 기본 on.
파티션 개수 — 균형
| 파티션 수 | 영향 |
|---|---|
| 적음 (< 20) | plan 비용 작음. 각 파티션이 큼 |
| 중간 (20~100) | 균형. 운영 권장 |
| 많음 (100~500) | plan time ↑. relcache 비대 |
| 매우 많음 (1000+) | plan time 폭증, autovacuum 부담 |
운영 권장: 활성 파티션 50개 이하. 오래된 파티션은 분기·년 단위로 합침(7.5).
파티션 키 선택
좋은 키:
- WHERE에 항상 등장
- selectivity 효과 큼
- NOT NULL
- 변경되지 않음
시계열은 created_at 또는 event_date. 멀티 테넌트는 tenant_id. 분산은 hash.
키가 쿼리 패턴에 없으면 pruning 효과 없음 — 큰 결정 실수.
파티션 인덱스 전략
-- 부모에 만들면 모든 자식에 자동 생성
CREATE INDEX ON events (user_id);
-- 특정 파티션에만 — 일반적이지 않지만 가능
CREATE INDEX ON events_2026_05 (extra_col);| 패턴 | 메모 |
|---|---|
| 부모에 만들기 | 일관성, 자동 |
| 자식별 다른 인덱스 | 분석 파티션만 추가 — 가능 |
| unique 인덱스 | 파티션 키 포함 필수 |
인덱스 빌드 — 파티션 단위로 빠름
큰 인덱스를 한 번에 만들면 수 시간 걸립니다. 파티션 단위는 각 파티션이 작아 빠르고, 병렬도 가능합니다.
# 자식 파티션마다 CONCURRENTLY로 빌드
for part in $(psql -At -c "SELECT inhrelid::regclass FROM pg_inherits WHERE inhparent='events'::regclass"); do
psql -c "CREATE INDEX CONCURRENTLY ON $part (user_id);"
done
# 마지막에 부모에 명시적 — 자식이 이미 있으면 attach만
CREATE INDEX ON events (user_id); -- 일관성 검사 + attachPG 12+의 CREATE INDEX ... ON ONLY parent로 부모만 만들고 자식은 따로 ATTACH도 가능합니다.
VACUUM·ANALYZE 분리
각 파티션은 독립 통계·BLOAT. 자동 autovacuum이 자식별로 동작합니다.
-- 명시 ANALYZE
ANALYZE events_2026_05;
-- 부모 통계 (PG 14+)
ANALYZE ROOT events;큰 부모 ANALYZE는 모든 자식 샘플링으로 시간 큽니다. ETL 직후는 수정된 자식만 명시합니다.
핫·콜드 파티션 분리
-- 오래된 파티션을 느린 디스크 tablespace로
CREATE TABLESPACE cold LOCATION '/mnt/hdd/pgdata';
ALTER TABLE events_2024_01 SET TABLESPACE cold;
ALTER INDEX events_2024_01_pkey SET TABLESPACE cold;운영 패턴: 최근 3개월은 NVMe, 그 이전은 HDD. 자주 안 쓰는 cold 데이터의 디스크 비용 절감합니다.
파티셔닝의 함정
| 함정 | 영향 |
|---|---|
| 파티션 키가 WHERE에 안 들어옴 | pruning 0% — 단순 분할 손해 |
| unique constraint에 파티션 키 추가 안 함 | unique 안 됨 |
| 글로벌 FK 어려움 (PG 12+ 부분 지원) | 데이터 무결성 운영 부담 |
| 매우 많은 파티션 | plan time ↑ |
| DEFAULT 파티션이 큼 | 새 파티션 attach 시 검사 오래 |
| 파티션 단위 transactional DDL | 부모 변경이 자식 모두 lock |
파티셔닝 도입 결정 재점검
7.1에서 본 결정 기준 + 성능 관점:
| 상황 | 결정 |
|---|---|
| 50GB 이하, 단일 인덱스로 가능 | 파티셔닝 안 함 |
| 50~200GB, 보존 정책 명확 | 파티셔닝 검토 |
| 200GB+, range 쿼리 위주 | 거의 필수 |
| random 쿼리 위주 | 파티셔닝 효과 작음 |
| 매우 많은 small tenant | tenant 별 파티션 검토 |
정리
- 파티셔닝의 성능 효과 4가지: pruning, wise join, wise aggregate, 인덱스 분할
- 파티션 키는 쿼리 WHERE에 자주 등장하는 컬럼
- 활성 파티션 50개 이하가 운영 한계
- 큰 인덱스 빌드는 자식별 CONCURRENTLY로
- 핫·콜드 분리로 디스크 비용 절감
- 도입 전 쿼리 패턴 점검 필수 — pruning 안 되면 손해
다음 절(13.4)에서는 수많은 연결의 부담을 해결하는 커넥션 풀링을 봅니다.