autovacuum 방치했다가 DB 멈춘 썰 (PostgreSQL VACUUM 완벽 정리, 그림 5개)
테스트 환경: PostgreSQL 17, Rocky Linux 9 / Oracle 19c
Oracle DBA 시절엔 신경 쓸 필요 없던 게 PostgreSQL엔 하나 있습니다. 바로 VACUUM입니다.
처음엔 저도 무시했습니다. "자동으로 돈다며? 그럼 놔두면 되지." 그런데 어느 날 디스크가 이유 없이 계속 차오르고, 잘 돌던 쿼리가 느려지기 시작했습니다. 알고 보니 autovacuum이 제대로 안 돌고 있었고, dead tuple이 산더미처럼 쌓여 있었습니다. 최악의 경우엔 트랜잭션 ID wraparound로 DB가 통째로 멈출 수도 있다는 걸 나중에 알고 식은땀을 흘렸습니다.
이번 편은 운영 시리즈의 두 번째 글이자, PostgreSQL 운영의 핵심입니다. 3편(MVCC)에서 예고한 dead tuple 문제를 드디어 제대로 다룹니다. Oracle엔 없는 개념이라, PostgreSQL 운영자라면 반드시 알아야 합니다. 그림 5개로 확실히 정리합니다.
- dead tuple은 왜 쌓이나
- VACUUM vs VACUUM FULL 차이
- autovacuum은 언제 자동 실행되나
- ★ 트랜잭션 ID wraparound (DB 셧다운 위험)
- 운영 모니터링과 튜닝
dead tuple은 왜 쌓이나
3편에서 배운 MVCC를 복습하면서 시작합니다. PostgreSQL은 UPDATE/DELETE 시 옛 행을 지우지 않고 "죽었다(dead)"고 표시만 합니다.

문제는 이 dead tuple이 계속 쌓인다는 겁니다. UPDATE를 많이 하는 테이블은 실제 데이터보다 몇 배씩 커집니다. 이걸 테이블 bloat(팽창) 이라고 합니다.
-- dead tuple 확인 (3편에서도 봤던 쿼리)
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
n_dead_tup이 n_live_tup에 육박하거나 넘으면 심각한 상태입니다. 100만 개의 dead tuple이 있으면, 그 테이블은 200만 행짜리처럼 느리게 동작합니다. 죽은 행도 스캔해야 하니까요.
이 dead tuple을 청소하는 게 바로 VACUUM입니다.
VACUUM vs VACUUM FULL - 헷갈리면 사고 난다
VACUUM에는 두 종류가 있고, 이걸 헷갈리면 운영 사고가 납니다.

-- 일반 VACUUM: dead 공간을 "재사용 가능"으로 표시
VACUUM employees;
-- VACUUM ANALYZE: 통계까지 갱신 (권장)
VACUUM ANALYZE employees;
-- VACUUM FULL: 테이블 통째로 재작성 (주의!)
VACUUM FULL employees;
결정적 차이
일반 VACUUM
→ dead 공간을 재사용 가능으로 표시
→ 테이블 크기는 그대로 (빈 슬롯 재활용)
→ 락 없음 (온라인) · 빠름 · 평소 운영용
→ 단, 디스크는 OS로 반환 안 됨
VACUUM FULL
→ 테이블을 통째로 새로 씀
→ 테이블 크기 축소, 디스크 OS로 반환
→ 테이블 전체 잠금! (읽기/쓰기 모두 차단)
→ 대용량은 몇 시간 걸림
⚠️ 절대 주의: VACUUM FULL은 테이블 전체를 잠급니다. 운영 중인 테이블에 무심코 실행하면 서비스가 멈춥니다. 반드시 유지보수 시간에만, 또는 pg_repack(락 없이 재구성하는 확장)을 쓰세요.
평소: 일반 VACUUM (autovacuum이 자동으로)
디스크 반환 필요: 유지보수 시간에 VACUUM FULL 또는 pg_repack
★ autovacuum은 언제 자동 실행되나
다행히 평소엔 autovacuum이 자동으로 VACUUM을 돌립니다. 그런데 "언제" 도는지 알아야 문제를 진단할 수 있습니다.

기본 발동 조건은 이렇습니다.
dead tuple > 50 + (autovacuum_vacuum_scale_factor × 전체 행 수)
↑ 기본값 0.2 (20%)
예시로 이해하기:
1,000행 테이블:
50 + (0.2 × 1,000) = 250
→ dead tuple 250개 쌓이면 autovacuum 실행
★ 큰 테이블의 함정
여기에 큰 테이블의 함정이 있습니다.
10억 행 테이블:
50 + (0.2 × 10억) = 2억!
→ dead tuple이 2억 개 쌓일 때까지 청소 안 함
→ 그 사이 테이블은 bloat로 폭발
이래서 큰 테이블일수록 scale_factor를 낮춰야 합니다. 기본값 0.2는 작은 테이블 기준이라, 대용량 운영 DB엔 부적합합니다. (튜닝은 아래에서)
★ 트랜잭션 ID Wraparound - DB가 멈추는 최악의 사고
이게 제가 식은땀 흘린 부분입니다. autovacuum을 오래 방치하면 단순히 느려지는 걸 넘어 DB가 통째로 멈출 수 있습니다.

원리
PostgreSQL의 트랜잭션 ID는 32비트 (약 21억 개)
→ 계속 증가하다가 한계에 도달하면 "한 바퀴 돌음(wraparound)"
→ 옛 데이터가 미래 것으로 보여 사라질 위험
→ 이를 막으려고 PostgreSQL이 스스로 셧다운!
VACUUM은 dead tuple 청소뿐 아니라, 오래된 행을 "freeze(동결)" 해서 트랜잭션 ID를 안전하게 재사용하게 합니다. 이 freeze를 안 하면 wraparound로 치닫습니다.
모니터링 (정기 확인 필수)
-- 각 DB의 wraparound 위험도 확인
SELECT datname,
age(datfrozenxid) AS xid_age,
ROUND(age(datfrozenxid)::numeric / 2147483647 * 100, 2) AS pct
FROM pg_database
ORDER BY xid_age DESC;
xid_age가:
~15억 미만: 정상
15억 이상: 경보 (autovacuum 점검 필요)
20억 근접: 위험 (곧 셧다운)
healthy한 시스템에선 거의 안 생기지만, 터지면 재앙입니다. 그래서 정기 모니터링이 필수입니다.
★ 운영 모니터링과 튜닝
그럼 실무에서 뭘 챙겨야 할까요? 방치가 사고를 부르니, 주기적 확인 3가지와 튜닝을 정리합니다.

정기 확인 3가지
-- ① dead tuple 비율 (5~10% 이하 유지)
SELECT relname, n_live_tup, n_dead_tup,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup,0) * 100, 1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 20;
-- ② 마지막 autovacuum 시각 (오래됐으면 문제)
SELECT relname, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY last_autovacuum NULLS FIRST LIMIT 20;
-- ③ XID 나이 (위 wraparound 쿼리)
튜닝 핵심
기본값은 소규모 워크로드용이라 운영 DB엔 부족합니다. 핵심 튜닝 포인트입니다.
-- 큰/바쁜 테이블은 per-table로 공격적으로 (권장)
ALTER TABLE big_table SET (
autovacuum_vacuum_scale_factor = 0.05, -- 기본 0.2 → 0.05
autovacuum_vacuum_cost_limit = 2000 -- 더 빠르게
);
postgresql.conf 전역 튜닝:
autovacuum_max_workers = 6 # 기본 3, 워커 증가
autovacuum_naptime = 30s # 더 자주 체크
maintenance_work_mem = 1GB # VACUUM 속도 향상
# SSD/NVMe라면 더 공격적으로
autovacuum_vacuum_cost_delay = 0 # 지연 없이 풀속도
핵심 원칙:
1. 전역을 무작정 낮추지 말고, 바쁜 테이블만 per-table 튜닝
2. dead 비율 5~10% 이하 유지 목표
3. XID 나이 15억 넘으면 경보 설정
4. VACUUM FULL 대신 pg_repack (온라인)
5. 스테이징에서 먼저 테스트 (autovacuum이 I/O 경쟁 유발 가능)
★ AI로 VACUUM 상태 진단하기
VACUUM 관련 지표가 복잡하면 Claude에게 진단을 맡기세요.
프롬프트:
"PostgreSQL 17 운영 중인데 특정 테이블이 느려졌어.
pg_stat_user_tables 결과야:
[결과 붙여넣기]
1. dead tuple 상태 진단
2. autovacuum이 잘 도는지
3. bloat이 심각한지
4. 튜닝 방안 (per-table 설정 포함)
Oracle DBA가 이해하기 쉽게 설명해줘."
특히 "이 테이블에 맞는 autovacuum 설정을 계산해줘"라고 하면 테이블 크기·변경 빈도에 맞는 값을 제안해줍니다.
⚠️ 검증 필수: autovacuum 튜닝은 I/O에 영향을 줍니다. AI 제안값은 반드시 스테이징에서 먼저 적용하고, 운영 워크로드와 경쟁하지 않는지 확인하세요.
Oracle DBA가 VACUUM에서 저지르는 실수 TOP 5
1. VACUUM을 아예 신경 안 씀
"자동이니까 괜찮겠지" → autovacuum이 못 따라가는 걸 모름. 정기 모니터링 필수.
2. 운영 중 VACUUM FULL 실행
테이블 전체 잠금으로 서비스 중단. 유지보수 시간에만.
3. 큰 테이블에 기본 설정 방치
scale_factor 0.2면 큰 테이블은 청소가 너무 늦음. per-table로 낮춰야.
4. XID wraparound 무시
"설마 그런 일이" → 터지면 DB 셧다운. 정기 확인 필수.
5. 긴 트랜잭션 방치
오래 열린 트랜잭션은 VACUUM을 방해함(옛 행을 못 지움). idle_in_transaction_session_timeout 설정 권장.
마무리
PostgreSQL VACUUM의 핵심입니다.
- dead tuple은 MVCC의 대가 — UPDATE/DELETE마다 쌓임
- 일반 VACUUM은 온라인, FULL은 락 — FULL은 유지보수 시간에만
- autovacuum 공식 — 큰 테이블은 scale_factor 낮춰야
- XID wraparound — 방치하면 DB 셧다운, 정기 모니터링 필수
- 튜닝은 per-table로 — 바쁜 테이블만 공격적으로
Oracle엔 없던 개념이라 처음엔 낯설지만, VACUUM은 PostgreSQL 운영자의 필수 교양입니다. "UNDO 관리에서 VACUUM 관리로"의 사고 전환, 기억나시죠? 3편에서 예고한 그 관리 포인트가 바로 이겁니다. 저처럼 방치하다 식은땀 흘리지 마시고, 오늘 위 모니터링 쿼리 3개를 본인 DB에 돌려보세요.
다음 편은 운영 중 자주 나오는 문제 TOP 10입니다. VACUUM 문제를 포함해, 연결 부족·락·느린 쿼리 같은 실전 장애를 한 번에 정리합니다.
여러분이 VACUUM으로 겪은 사고나, bloat를 극적으로 줄인 경험이 있다면 댓글로 공유해주세요.
'DBA 실무 > PostgreSQL' 카테고리의 다른 글
| [PostgreSQL 시리즈 10편] Oracle 권한 관리 그대로 했다가 막힌 썰 (PostgreSQL ROLE 완벽 정리) (0) | 2026.09.02 |
|---|---|
| Oracle 사용자가 PostgreSQL 보고 배 아파하는 기능 6가지 (PostgreSQL 킬러 기능 총정리) (0) | 2026.08.31 |
| PL/SQL 그대로 옮겼다가 다 터진 썰 (Oracle DBA를 위한 PL/pgSQL 완벽 비교) (0) | 2026.08.28 |
| B-tree만 알던 내가 GIN 인덱스 보고 눈 돌아간 썰 (PostgreSQL 인덱스 완벽 정리) (0) | 2026.08.26 |
| Oracle 함수 그대로 쳤다가 에러 폭탄 맞은 썰 (PostgreSQL 함수 완벽 변환표) (0) | 2026.08.24 |