- Published on
PostgreSQL Partitioning 실전 가이드
- Authors
- Name
- 1. 파티셔닝이란?
- 2. Range 파티셔닝
- 3. List 파티셔닝
- 4. Hash 파티셔닝
- 5. Partition Pruning 확인
- 6. 파티션 자동 생성 (pg_partman)
- 7. 오래된 파티션 삭제
- 8. 운영 팁
- 9. 퀴즈

1. 파티셔닝이란?
파티셔닝은 하나의 큰 테이블을 여러 개의 물리적 파티션으로 분할하는 기법입니다. 테이블의 데이터가 수억 건 이상일 때 쿼리 성능을 크게 향상시킬 수 있습니다.
파티셔닝의 장점
- 쿼리 성능 향상: Partition pruning으로 필요한 파티션만 스캔
- 대량 데이터 삭제: DROP PARTITION으로 즉시 삭제 (DELETE보다 수백 배 빠름)
- 병렬 처리: 파티션별 병렬 스캔 가능
- 관리 용이: 파티션별 인덱스, VACUUM, 백업 가능
2. Range 파티셔닝
가장 많이 사용되는 방식으로, 날짜/숫자 범위로 분할합니다.
-- 부모 테이블 생성
CREATE TABLE orders (
id BIGSERIAL,
customer_id INTEGER NOT NULL,
order_date DATE NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (order_date);
-- 월별 파티션 생성
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- 파티션별 인덱스 (자동 상속됨)
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status ON orders (status, order_date);
-- 기본 파티션 (범위에 맞지 않는 데이터 수용)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
3. List 파티셔닝
특정 값 목록으로 분할합니다. 지역, 카테고리 등에 적합합니다.
CREATE TABLE events (
id BIGSERIAL,
event_type VARCHAR(50) NOT NULL,
payload JSONB,
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY LIST (event_type);
CREATE TABLE events_user PARTITION OF events
FOR VALUES IN ('user_signup', 'user_login', 'user_logout');
CREATE TABLE events_order PARTITION OF events
FOR VALUES IN ('order_created', 'order_paid', 'order_cancelled');
CREATE TABLE events_system PARTITION OF events
FOR VALUES IN ('health_check', 'deploy', 'config_change');
CREATE TABLE events_default PARTITION OF events DEFAULT;
4. Hash 파티셔닝
해시 함수로 균등 분배합니다. 특정 키의 분포가 고른 경우에 적합합니다.
CREATE TABLE user_sessions (
id BIGSERIAL,
user_id INTEGER NOT NULL,
session_id UUID NOT NULL,
data JSONB,
expires_at TIMESTAMP
) PARTITION BY HASH (user_id);
-- 4개 파티션으로 균등 분배
CREATE TABLE user_sessions_0 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_sessions_1 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_sessions_2 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_sessions_3 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
5. Partition Pruning 확인
-- Partition pruning 활성화 확인
SHOW enable_partition_pruning; -- on
-- EXPLAIN으로 pruning 확인
EXPLAIN (ANALYZE, COSTS, BUFFERS)
SELECT * FROM orders
WHERE order_date >= '2026-03-01'
AND order_date < '2026-04-01';
-- 결과 예시:
-- Append (actual rows=50000)
-- -> Seq Scan on orders_2026_03 (actual rows=50000)
-- Filter: (order_date >= '2026-03-01' AND order_date < '2026-04-01')
-- ✅ orders_2026_01, orders_2026_02는 스캔하지 않음!
6. 파티션 자동 생성 (pg_partman)
-- pg_partman 설치
CREATE EXTENSION pg_partman;
-- 자동 파티션 관리 설정
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'order_date',
p_type => 'native',
p_interval => 'monthly',
p_premake => 3 -- 3개월 미리 생성
);
-- 유지보수 함수 (cron으로 매일 실행)
SELECT partman.run_maintenance();
cron 설정
# pg_partman 유지보수 (매일 새벽 2시)
0 2 * * * psql -U postgres -d mydb \
-c "SELECT partman.run_maintenance();" \
>> /var/log/pg_partman.log 2>&1
수동 자동화 스크립트
-- pg_partman 없이 직접 자동화
CREATE OR REPLACE FUNCTION create_monthly_partition(
p_table TEXT,
p_year INTEGER,
p_month INTEGER
) RETURNS VOID AS $$
DECLARE
partition_name TEXT;
start_date DATE;
end_date DATE;
BEGIN
partition_name := format('%s_%s_%s',
p_table,
p_year,
LPAD(p_month::TEXT, 2, '0')
);
start_date := make_date(p_year, p_month, 1);
end_date := start_date + INTERVAL '1 month';
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
FOR VALUES FROM (%L) TO (%L)',
partition_name, p_table, start_date, end_date
);
RAISE NOTICE 'Created partition: %', partition_name;
END;
$$ LANGUAGE plpgsql;
-- 사용 예
SELECT create_monthly_partition('orders', 2026, 4);
SELECT create_monthly_partition('orders', 2026, 5);
7. 오래된 파티션 삭제
-- 파티션 분리 (데이터 보존, 쿼리에서 제외)
ALTER TABLE orders DETACH PARTITION orders_2025_01;
-- 분리된 파티션을 별도 테이블로 유지하거나 삭제
DROP TABLE orders_2025_01; -- 즉시 삭제 (수억 건도 순간)
-- 대비: DELETE는 매우 느림
-- DELETE FROM orders WHERE order_date < '2025-02-01'; ← 이러지 마세요!
8. 운영 팁
파티션 상태 모니터링
-- 파티션별 행 수 확인
SELECT
schemaname || '.' || relname AS partition,
n_live_tup AS row_count,
pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;
-- 파티션 목록 조회
SELECT
parent.relname AS parent,
child.relname AS partition,
pg_get_expr(child.relpartbound, child.oid) AS bounds
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
주의사항
1. PRIMARY KEY에 파티션 키 포함 필수
CREATE TABLE orders (...) PARTITION BY RANGE (order_date);
→ PK는 (id, order_date) 형태여야 함
2. UNIQUE 제약조건도 파티션 키 포함 필요
3. 파티션 수가 너무 많으면 플래닝 오버헤드 증가
→ 1000개 이하 유지 권장
4. 크로스 파티션 UPDATE는 PostgreSQL 11+에서만 지원
9. 퀴즈
Q1: Range 파티셔닝에서 DEFAULT 파티션의 역할은?
DEFAULT 파티션은 어떤 파티션의 범위에도 속하지 않는 데이터를 수용합니다. 예를 들어 2026년 파티션만 있는데 2027년 데이터가 INSERT되면 DEFAULT 파티션에 저장됩니다. DEFAULT 파티션이 없으면 범위 밖 데이터 INSERT 시 에러가 발생합니다.
Q2: DROP PARTITION이 DELETE보다 빠른 이유는?
DELETE는 각 행을 하나씩 삭제하면서 WAL 로그를 기록하고, dead tuple이 남아 VACUUM이 필요합니다. 반면 DROP TABLE (파티션 삭제)은 테이블의 데이터 파일 자체를 즉시 삭제하므로, 수억 건의 데이터도 순식간에 제거됩니다. 행 수에 관계없이 거의 일정한 시간이 소요됩니다.
Q3: PRIMARY KEY에 파티션 키를 포함해야 하는 이유는?
PostgreSQL은 파티션 테이블의 유니크 제약조건을 각 파티션의 로컬 인덱스로 구현합니다. 파티션 키가 PK에 포함되지 않으면, 다른 파티션에 같은 id가 존재할 수 있어 전체 테이블 수준의 유니크성을 보장할 수 없습니다. 따라서 (id, order_date) 처럼 파티션 키를 PK에 포함해야 합니다.