VACUUM 이란?
PostgreSQL은 MVCC(다중 버전 동시성 제어)를 사용합니다. UPDATE나 DELETE를 해도 기존 행을 즉시 지우지 않고, "죽은 버전(dead tuple)"으로 남겨 둡니다. 다른 트랜잭션이 아직 그 버전을 볼 수 있기 때문입니다. 시간이 지나 아무도 참조하지 않게 된 dead tuple을 정리해 공간을 재사용 가능하게 만드는 작업이 바로 VACUUM 입니다.
VACUUM이 하는 일:
- dead tuple이 차지하던 공간을 재사용 가능 상태로 표시
- Visibility Map 갱신 → Index-Only Scan 효율화
- 트랜잭션 ID(XID) freezing → wraparound 장애 예방
- 통계 갱신(
ANALYZE병행 시) → 플래너 정확도 향상
일반 VACUUM은 디스크를 OS에 반환하지 않습니다(파일 크기 그대로). 공간은 테이블 내부에서 재사용됩니다. 실제 파일 축소는 VACUUM FULL이지만 배타 락이 걸립니다.
Bloat(테이블 팽창) 이란?
dead tuple이 제때 정리되지 않거나, 정리되어도 즉시 채워지지 않으면 테이블·인덱스가 실제 데이터보다 훨씬 커집니다. 이를 bloat 라 합니다.
bloat의 영향:
- Seq Scan 시 읽어야 할 페이지 증가 → 쿼리 느려짐
- 캐시(shared_buffers) 효율 저하
- 인덱스 비대화 → 인덱스 스캔 비용 증가
1. dead tuple 진단
SELECT
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;dead_pct 가 10~20%를 넘어가면서 last_autovacuum 이 오래 전이라면 autovacuum이 따라오지 못하고 있다는 신호입니다.
테이블 물리 크기와 함께 보기:
SELECT
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
n_dead_tup
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;bloat 추정에는 pgstattuple 확장이 정확합니다.
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('public.orders');
-- dead_tuple_percent, free_percent 등 확인2. 수동 VACUUM
-- 자세한 로그와 통계 갱신을 함께
VACUUM (VERBOSE, ANALYZE) public.orders;
-- 전체 DB 대상(주기적 운영보다는 점검용)
VACUUM (ANALYZE);VERBOSE 출력에서 removed N row versions, there were N dead row versions 등을 확인할 수 있습니다.
실무에서 일상 정리는 autovacuum에 맡기고, 수동 VACUUM은 대량 배치 직후나 진단 목적으로만 사용하는 것이 정석입니다.
3. autovacuum 동작 원리
autovacuum은 다음 조건이 충족되면 테이블별로 자동 실행됩니다.
임계치 = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * 테이블 행 수기본값은 threshold=50, scale_factor=0.2 입니다. 즉 1,000만 행 테이블은 약 200만 개의 dead tuple이 쌓여야 autovacuum이 돕니다. 대형 테이블에서 너무 늦게 도는 전형적인 문제입니다.
postgresql.conf 전역 기본:
autovacuum = on
autovacuum_max_workers = 4
autovacuum_naptime = 30s
# 임계치
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.1 # 0.2 → 0.1로 더 자주
# I/O 비용 제한(클수록 더 공격적)
autovacuum_vacuum_cost_limit = 2000
autovacuum_vacuum_cost_delay = 2ms4. 대형 테이블 개별 튜닝
전역 값은 보수적으로 두고, 갱신이 잦은 큰 테이블만 storage parameter로 공격적으로 설정하는 것이 좋습니다.
ALTER TABLE public.orders SET (
autovacuum_vacuum_scale_factor = 0.02, -- 2% 쌓이면 실행
autovacuum_vacuum_threshold = 1000,
autovacuum_vacuum_cost_limit = 4000,
autovacuum_analyze_scale_factor = 0.01
);| 파라미터 | 의미 | 큰 테이블 권장 |
|---|---|---|
| vacuum_scale_factor | 비율 임계치 | 0.01 ~ 0.05 |
| vacuum_threshold | 절대 임계치 | 1000+ |
| vacuum_cost_limit | I/O 예산 | 2000 ~ 4000 |
| vacuum_cost_delay | 휴식 시간 | 0 ~ 2ms |
5. 진행 중인 VACUUM 모니터링
SELECT
p.pid, t.relname, p.phase,
p.heap_blks_scanned, p.heap_blks_total,
round(100.0 * p.heap_blks_scanned / NULLIF(p.heap_blks_total, 0), 1) AS pct
FROM pg_stat_progress_vacuum p
JOIN pg_stat_user_tables t ON t.relid = p.relid;장시간 도는 autovacuum이 다른 작업을 막는지 확인:
SELECT pid, state, wait_event_type, query, now() - xact_start AS duration
FROM pg_stat_activity
WHERE query ILIKE '%autovacuum%' OR query ILIKE '%vacuum%';6. XID Wraparound 방지
트랜잭션 ID는 약 21억 개로 순환합니다. freezing이 제때 안 되면 wraparound로 DB가 강제로 읽기 전용/중단될 수 있는 치명적 상황이 됩니다.
-- 데이터베이스별 남은 트랜잭션 여유 확인
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;xid_age 가 autovacuum_freeze_max_age(기본 2억) 에 근접하면 강제 anti-wraparound autovacuum이 돕니다. 이 작업은 멈추면 안 되며, 평소 정상적인 VACUUM이 돌고 있으면 거의 마주칠 일이 없습니다.
특정 테이블만 age가 비정상적으로 높다면, 그 테이블에 장시간 열린 트랜잭션(idle in transaction)이 freeze를 막고 있는 경우가 많습니다. pg_stat_activity 에서 오래된 트랜잭션을 찾아 정리하세요.
7. bloat 제거 — VACUUM FULL과 대안
-- 파일을 실제로 축소하지만 ACCESS EXCLUSIVE 락(테이블 전체 잠금)
VACUUM FULL public.orders;운영 중 무중단이 필요하면 pg_repack 확장을 사용합니다. 새 테이블을 백그라운드로 만들고 교체하므로 짧은 락만 발생합니다.
pg_repack -d appdb -t orders --no-order정리
| 항목 | 핵심 |
|---|---|
| dead tuple | UPDATE/DELETE가 남기는 죽은 행 버전 |
| 진단 | pg_stat_user_tables.n_dead_tup, pgstattuple |
| 일상 정리 | autovacuum (수동 VACUUM은 배치 직후/진단용) |
| 대형 테이블 | 테이블별 scale_factor 0.01~0.05 |
| 진행 모니터링 | pg_stat_progress_vacuum |
| wraparound | age(datfrozenxid) 감시, 오래된 트랜잭션 정리 |
| bloat 제거 | VACUUM FULL(락) 또는 pg_repack(무중단) |
VACUUM은 "끄면 안 되는" 핵심 유지보수입니다. 큰 테이블의 scale_factor를 낮춰 자주 돌게 하고, dead tuple 비율과 XID age를 정기 모니터링하는 것이 bloat와 wraparound를 동시에 막는 가장 확실한 방법입니다.
이 가이드는 AI 도구를 활용해 초안을 구성하고 사람이 명령어·문맥을 검토해 발행했습니다. 운영체제와 도구 버전에 따라 결과가 달라질 수 있으므로 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요.
질문 & 답변 (Q&A)
이 가이드에 대해 궁금한 점을 질문해보세요. 확인 후 답변드립니다.