--- id: db-vacuum-autovacuum title: Vacuum / Autovacuum — Bloat / Wraparound 방지 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, postgres, vacuum, vibe-coding] tech_stack: { language: "Postgres", applicable_to: ["Backend"] } applied_in: [] aliases: [VACUUM, autovacuum, bloat, dead tuple, transaction wraparound, freeze] --- # Vacuum / Autovacuum > Postgres MVCC = UPDATE/DELETE 가 dead tuple 생성. **VACUUM 이 정리**. Autovacuum 자동 — 그러나 큰 테이블 / write 많을 때 manual 필요. Bloat / wraparound 위험. ## 📖 핵심 개념 - Dead tuple: 삭제 / 업데이트 된 row. - Bloat: dead tuple 누적 → 테이블 거대. - VACUUM: dead tuple 마킹 + 재사용. - VACUUM FULL: 테이블 rewrite (lock). - ANALYZE: 통계 업데이트. ## 💻 코드 패턴 ### Autovacuum 기본 ``` default 활성. 각 테이블 별: - 50 row + 20% 변경 → autovacuum - 50 row + 10% 변경 → autoanalyze ``` ### 큰 테이블 = autovacuum 안 따라옴 ``` 1억 row × 20% = 2천만 → autovacuum 한 번에 큰 cost → 자주 작게 하라. ``` ```sql ALTER TABLE orders SET ( autovacuum_vacuum_scale_factor = 0.05, -- 5% 마다 (default 20%) autovacuum_analyze_scale_factor = 0.02, autovacuum_vacuum_cost_limit = 2000, -- I/O cost limit ↑ ); ``` ### 수동 VACUUM ```sql VACUUM (ANALYZE, VERBOSE) orders; -- Bloat 정리 + 통계 + log -- Concurrent (lock 적음) VACUUM (ANALYZE) orders; -- 이미 concurrent default -- Full (lock 큼 — 잘 안 씀) VACUUM FULL orders; -- 테이블 lock + rewrite ``` ### Bloat 측정 ```sql SELECT schemaname, relname, n_live_tup, n_dead_tup, round(100.0 * n_dead_tup / 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 > 20% = vacuum 필요. ### pgstattuple (정확한 bloat) ```sql CREATE EXTENSION pgstattuple; SELECT * FROM pgstattuple('orders'); -- tuple_count, dead_tuple_count, free_space ``` ### Index bloat ```sql CREATE EXTENSION pgstattuple; SELECT * FROM pgstatindex('orders_pkey'); -- index_size, leaf_fragmentation ``` → Index bloat → REINDEX. ### REINDEX (CONCURRENTLY) ```sql REINDEX INDEX CONCURRENTLY orders_pkey; -- lock 없이 새 인덱스 빌드 + swap ``` ### pg_repack (extension) ```bash pg_repack -d mydb -t orders # Online table rewrite — bloat 제거 lock 없이 ``` → VACUUM FULL 의 zero-downtime 대안. ### Transaction wraparound (위험) ``` Postgres = 32-bit transaction ID. 2B → wrap. → 모든 row 의 xmin 이 frozen 안 되면 — DB freeze. Autovacuum 자동 freeze. but very busy DB 일 때 추적 필요. ``` ```sql -- 가장 오래된 frozen 까지의 거리 SELECT datname, age(datfrozenxid) AS xid_age, 2147483647 - age(datfrozenxid) AS remaining FROM pg_database ORDER BY age(datfrozenxid) DESC; ``` → 200M+ = 주의. 1.5B = 위험. 2B = freeze. ```sql -- 명시 freeze VACUUM FREEZE orders; ``` ### Long-running transaction = autovacuum 차단 ```sql -- 오래된 transaction SELECT pid, usename, query, state, age(NOW(), query_start) AS age FROM pg_stat_activity WHERE state != 'idle' ORDER BY query_start; ``` → 30분+ = 의심. 2시간+ = autovacuum 차단. ```ts // App 에서 transaction 짧게 async function work() { await db.transaction(async (tx) => { // ❌ 안 — fetch 외부 API // const data = await fetch(...); // 짧게 — write 만 await tx.insert(...); }); // 외부 호출 후 commit } ``` ### Maintenance window ```sql -- Off-peak 시간 더 적극 vacuum ALTER TABLE big_table SET ( autovacuum_vacuum_cost_delay = 0 -- 빠르게 ); ``` ### Monitoring ```sql -- Autovacuum 진행 SELECT pid, datname, query, query_start FROM pg_stat_activity WHERE query LIKE 'autovacuum:%'; -- 횟수 SELECT relname, n_tup_ins, n_tup_upd, n_tup_del, autovacuum_count, autoanalyze_count, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_tup_upd + n_tup_del DESC LIMIT 20; ``` ### Tunables ``` autovacuum_max_workers = 3 -- worker 수 autovacuum_naptime = 1min -- check 주기 maintenance_work_mem = 256MB -- vacuum 메모리 # 큰 cluster: autovacuum_max_workers = 6 maintenance_work_mem = 2GB ``` ### HOT update (no index update) ```sql -- Index 컬럼 안 변경 + page 안 free space 있음 → HOT update -- → bloat 적음 + 빠름 ``` ```sql -- 측정 SELECT relname, n_tup_upd, n_tup_hot_upd, round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 2) AS hot_pct FROM pg_stat_user_tables ORDER BY n_tup_upd DESC LIMIT 20; ``` → hot_pct 높음 = 좋음. ```sql -- Fillfactor (page 안 free space 남기기) ALTER TABLE orders SET (fillfactor = 80); -- 20% 비움 ``` ### Alarm ```yaml - alert: HighDeadTuples expr: pg_stat_user_tables_n_dead_tup / pg_stat_user_tables_n_live_tup > 0.3 for: 1h - alert: TransactionWraparound expr: pg_database_xid_age > 1500000000 for: 10m ``` ## 🤔 의사결정 기준 | 상황 | 액션 | |---|---| | Dead pct > 20% | Manual VACUUM | | Bloat 큼 | pg_repack | | Index bloat | REINDEX CONCURRENTLY | | Wraparound 임박 | VACUUM FREEZE | | Long transaction | App fix — short tx | | 큰 write 테이블 | scale_factor 낮게 | ## ❌ 안티패턴 - **VACUUM FULL prod**: lock — table 못 사용. pg_repack. - **Autovacuum 끄기**: bloat 폭발. - **Long transaction (30분+)**: autovacuum 차단. - **모든 table 같은 setting**: 큰 vs 작은 다름. - **Wraparound 모니터링 X**: freeze 위험. - **REINDEX 직접 prod**: lock. CONCURRENTLY. - **Fillfactor 100% prod**: HOT update X — bloat. ## 🤖 LLM 활용 힌트 - 큰 table = scale_factor 낮게 + cost limit 높게. - Long transaction = 적. - pg_repack 가 zero-downtime maintenance. - Wraparound + bloat alarm 항상. ## 🔗 관련 문서 - [[DB_Postgres_EXPLAIN]] - [[DB_Query_Optimization]] - [[DB_Lock_Analysis]]