Files
2nd/10_Wiki/Topics/Coding/DB_Vacuum_Autovacuum.md
T
2026-05-09 21:08:02 +09:00

249 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]]