시계열 데이터를 월 단위로 파티션했는데 시간이 지나 파티션이 너무 많아졌다면 분기나 연 단위로 합치고 싶어진다. 반대로 특정 월의 데이터가 예상보다 커지면 더 잘게 쪼개고 싶어진다. 포스트그레SQL에는 아직 이를 한 번에 처리하는 표준 DDL이 널리 쓰이지 않아, DETACH/ATTACH 조합으로 수행하는 것이 실무 표준이다.
핵심은 잠금 시간을 최소화하는 것이다. 순진하게 수행하면 수 분에서 수십 분간 테이블 전체가 잠긴다. 운영 중인 서비스에서는 그대로 장애다.
나는 이 작업을 처음 할 때 유지보수 창 없이 시도했다가 크게 데인 적이 있다. ATTACH PARTITION이 제약 조건을 검증하느라 전체 스캔을 했고, 그동안 테이블에 ACCESS EXCLUSIVE 락이 걸렸다. 5분이면 될 줄 알았던 작업이 40분이 됐고, 그 40분 동안 서비스가 멈췄다.
먼저 이해할 것 — 잠금의 종류
| 작업 | 필요한 잠금 | 영향 |
|---|---|---|
DETACH PARTITION | ACCESS EXCLUSIVE (부모) | 모든 읽기·쓰기 차단 |
DETACH PARTITION CONCURRENTLY | 약한 잠금 | 읽기·쓰기 계속 가능(제약 있음) |
ATTACH PARTITION | 부모에 SHARE UPDATE EXCLUSIVE + 대상 검증 | 검증 중 대상 테이블 잠금 |
CREATE TABLE ... LIKE | 새 테이블만 | 영향 없음 |
| 데이터 INSERT SELECT | 대상 테이블 쓰기 잠금 | 원본은 읽기만 |
ATTACH PARTITION은 붙이려는 테이블의 모든 행이 파티션 경계 조건을 만족하는지 검증한다. 이 검증은 전체 스캔이다. 미리 동등한 CHECK 제약을 걸어 두면 이 스캔을 건너뛴다. 대용량에서는 이 한 가지가 수십 분을 좌우한다.병합 절차 — 월 3개를 분기 1개로
-- 0) 사전 준비: 작업 범위 확인
SELECT relname, pg_size_pretty(pg_total_relation_size(oid))
FROM pg_class WHERE relname LIKE 'events_2026%' ORDER BY relname;
-- 1) 새 파티션이 될 테이블 생성 (아직 붙이지 않는다)
CREATE TABLE events_2026q1 (LIKE events INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
-- 2) 데이터 이동 — 배치로 나눈다
INSERT INTO events_2026q1
SELECT * FROM events_202601; -- 큰 테이블이면 이것도 청크로 분할
INSERT INTO events_2026q1
SELECT * FROM events_202602;
INSERT INTO events_2026q1
SELECT * FROM events_202603;
-- 3) 인덱스 생성 (데이터 이동 후가 빠르다)
CREATE INDEX CONCURRENTLY idx_events_2026q1_created
ON events_2026q1 (created_at);
-- 4) CHECK 제약 — ATTACH 시 전체 스캔을 건너뛰기 위해 필수
ALTER TABLE events_2026q1 ADD CONSTRAINT events_2026q1_range
CHECK (created_at >= '2026-01-01' AND created_at < '2026-04-01');
-- 5) 기존 파티션 분리 (가능하면 CONCURRENTLY)
ALTER TABLE events DETACH PARTITION events_202601 CONCURRENTLY;
ALTER TABLE events DETACH PARTITION events_202602 CONCURRENTLY;
ALTER TABLE events DETACH PARTITION events_202603 CONCURRENTLY;
-- 6) 새 파티션 연결
ALTER TABLE events ATTACH PARTITION events_2026q1
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
-- 7) 검증 후 정리
SELECT count(*) FROM events WHERE created_at >= '2026-01-01'
AND created_at < '2026-04-01';
-- DROP TABLE events_202601, events_202602, events_202603; -- 하루 뒤에
분할 절차 — 큰 파티션을 쪼개기
방향만 반대일 뿐 구조는 같다. 다만 데이터를 여러 대상으로 나눠 넣어야 하므로 이동 단계가 더 오래 걸린다.
-- 분할: 2026년 1분기 → 월별 3개
CREATE TABLE events_202601 (LIKE events INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
CREATE TABLE events_202602 (LIKE events INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
CREATE TABLE events_202603 (LIKE events INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
-- 조건별 이동 (배치 크기를 제어하며)
INSERT INTO events_202601 SELECT * FROM events_2026q1
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';
-- ... 나머지 월도 동일
-- CHECK 제약 + DETACH + ATTACH 는 병합과 같은 순서
대용량에서의 배치 이동
수억 행을 한 번의 INSERT ... SELECT로 옮기면 트랜잭션이 거대해지고, WAL이 폭증하며, 실패 시 롤백에도 오랜 시간이 걸린다. 청크로 나누는 것이 정석이다.
-- 키 범위로 나눠 반복 실행 (애플리케이션이나 스크립트에서 루프)
DO $$
DECLARE
lo timestamptz := '2026-01-01';
hi timestamptz;
BEGIN
WHILE lo < '2026-02-01' LOOP
hi := lo + interval '1 day';
INSERT INTO events_202601
SELECT * FROM events_2026q1
WHERE created_at >= lo AND created_at < hi;
COMMIT; -- 하루치마다 커밋해 WAL 과 잠금을 끊는다
lo := hi;
END LOOP;
END $$;
CONCURRENTLY의 제약
DETACH PARTITION CONCURRENTLY는 잠금을 크게 줄여 주지만 조건이 있다. 트랜잭션 블록 안에서 실행할 수 없고, 한 번에 하나의 파티션만 처리할 수 있으며, 중단되면 파티션이 중간 상태로 남을 수 있다. 중간 상태가 되면 FINALIZE 구문으로 마무리해야 한다.
-- 중단된 DETACH 마무리
ALTER TABLE events DETACH PARTITION events_202601 FINALIZE;
-- 현재 파티션 상태 확인
SELECT c.relname, p.partstrat, i.inhparent::regclass
FROM pg_class c
LEFT JOIN pg_inherits i ON i.inhrelid = c.oid
LEFT JOIN pg_partitioned_table p ON p.partrelid = c.oid
WHERE c.relname LIKE 'events%'
ORDER BY c.relname;
애초에 덜 아프게 설계하기
- 파티션 수를 적정하게 — 수천 개면 계획 수립 자체가 느려진다
- 보관 기간이 명확한 데이터는 파티션 단위 삭제(DROP)로 정리
- 기본 파티션(DEFAULT)을 두되 거기에 쌓이지 않게 감시
- 새 파티션 생성을 자동화(스케줄러)해 수작업을 없앤다
- 파티션 키를 자주 바꾸는 설계
- 월 파티션 수백 개를 방치
- 파티션 경계에 걸치는 쿼리 패턴
- 파티션마다 인덱스 구성이 다른 상태
경험상 재구성 작업의 절반은 초기 설계에서 파티션 단위를 잘못 잡아서 생긴다. 데이터 증가 속도를 보수적으로 예측해 단위를 정하고, 자동 생성 스크립트를 처음부터 넣어 두면 이 고생을 상당 부분 피할 수 있다.
자주 묻는 질문
파티션을 합치거나 쪼갤 때 서비스를 멈춰야 하나요?
완전 무중단은 어렵지만 중단 시간을 크게 줄일 수 있습니다. 데이터 이동을 미리 끝내 두고, CHECK 제약으로 ATTACH 검증을 생략하며, DETACH에 CONCURRENTLY를 사용하면 실제 잠금 구간은 매우 짧아집니다.
ATTACH PARTITION이 오래 걸리는 이유는 무엇인가요?
붙이려는 테이블의 모든 행이 파티션 경계 조건을 만족하는지 전체 스캔으로 검증하기 때문입니다. 파티션 경계와 동일한 CHECK 제약을 미리 추가해 두면 이 검증이 생략돼 즉시 완료됩니다.
데이터 이동은 한 번에 해도 되나요?
대용량에서는 권장하지 않습니다. 트랜잭션이 커지면 WAL이 폭증하고 복제 지연이 생기며, 실패 시 롤백에도 오랜 시간이 걸립니다. 날짜나 키 범위로 청크를 나눠 각 청크마다 커밋하는 방식이 안전합니다.
DETACH CONCURRENTLY 중에 작업이 중단되면 어떻게 하나요?
파티션이 중간 상태로 남을 수 있습니다. 이 경우 DETACH PARTITION ... FINALIZE로 마무리해야 합니다. 상태는 pg_inherits와 pg_class를 조회해 확인할 수 있습니다.
작업 중 디스크가 부족해질 수 있나요?
이동 중에는 원본과 사본이 동시에 존재하므로 최소 대상 데이터 크기의 2배 이상 여유가 필요합니다. 인덱스 재생성 공간과 WAL 증가분까지 고려해 여유를 넉넉히 확보한 뒤 시작하세요.
이런 작업을 줄이려면 처음에 어떻게 설계해야 하나요?
데이터 증가 속도를 보수적으로 예측해 파티션 단위를 정하고, 파티션 자동 생성과 오래된 파티션 삭제를 스케줄러로 자동화하세요. 파티션 수가 수천 개로 늘어나면 실행 계획 수립 자체가 느려지므로 적정 개수를 유지하는 것이 중요합니다.

댓글 0