Appearance
PostgreSQL — VACUUM & Autovacuum
Với BE dev: VACUUM là garbage collector của PostgreSQL — dọn dead rows mà MVCC để lại. Nó chạy tự động, bình thường không cần động vào. Đọc file này khi gặp vấn đề thực tế: bảng write-heavy chậm dần, autovacuum không kịp, hoặc cần debug bloat.
1. Tại sao cần VACUUM
Liên quan trực tiếp đến MVCC. Khi bạn chạy UPDATE hoặc DELETE, PostgreSQL không xóa row cũ ngay — nó chỉ đánh dấu row đó là dead (set xmax). Row cũ vẫn nằm trên disk.
UPDATE users SET tier = 'gold' WHERE id = 1
Trước: [row: id=1, tier='silver', xmin=100, xmax=0 ] ← live
Sau: [row: id=1, tier='silver', xmin=100, xmax=200 ] ← dead
[row: id=1, tier='gold', xmin=200, xmax=0 ] ← live (row mới)Dead rows tích lũy theo thời gian → table bloat: bảng phình to dù data thực tế không tăng. Query phải scan nhiều page hơn cần thiết → chậm dần.
VACUUM dọn dead rows, đánh dấu lại space đó để INSERT mới tái sử dụng.
2. VACUUM không trả space về OS
Đây là điểm hay nhầm:
Trước VACUUM: [live][dead][dead][live][dead] → file size: 5 pages
Sau VACUUM: [live][free][free][live][free] → file size: vẫn 5 pagesVACUUM chỉ đánh dấu các page dead thành free space để INSERT sau dùng lại — không shrink file trên disk, không trả về OS.
Muốn thực sự shrink file → dùng VACUUM FULL. Nhưng VACUUM FULL lock cả bảng trong suốt quá trình chạy — tránh dùng trên production trừ khi thực sự cần.
| Command | Lock | Trả space về OS | Dùng khi |
|---|---|---|---|
VACUUM | Không | Không | Routine cleanup, do autovacuum |
VACUUM FULL | Có (AEL) | Có | Bloat nặng, chấp nhận downtime |
VACUUM FREEZE | Không | Không | Cần freeze XID gấp (xem phần 4) |
3. Autovacuum
Chạy ngầm tự động — không cần can thiệp thủ công trong điều kiện bình thường.
Khi nào autovacuum trigger:
dead tuples > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × table_sizeDefault: 50 + 0.2 × table_size. Với bảng 10 triệu row → trigger khi có 2 triệu dead tuples. Quá cao cho bảng write-heavy.
Tuning per-table cho bảng lớn:
sql
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01, -- trigger khi 1% dead thay vì 20%
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 2 -- ms, giảm xuống để chạy nhanh hơn
);Không bao giờ tắt autovacuum trên production. Autovacuum có 2 nhiệm vụ:
- Dọn dead rows
- Freeze XID cũ → ngăn wraparound (xem phần 4)
Tắt autovacuum → sớm muộn gì DB cũng tự chuyển sang read-only để bảo vệ data.
4. XID Wraparound — Trường hợp khẩn cấp
XID là số 32-bit, max ≈ 4 tỷ. Khi đếm đến 4 tỷ, quay về 0. PostgreSQL so sánh XID theo vòng tròn → nếu XID của row cũ nằm ở phía "tương lai" → PostgreSQL nghĩ row chưa được tạo → data mất.
Giải pháp: FREEZE
VACUUM định kỳ freeze row cũ: đổi xmin thành FrozenXID (giá trị đặc biệt = "luôn visible, không cần so sánh XID"). Row được freeze thì không bao giờ bị wraparound.
sql
-- Kiểm tra nguy cơ wraparound
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
-- age > 1,500,000,000 → cần chạy VACUUM gấp
-- age > 2,000,000,000 → PostgreSQL tự chuyển read-only
-- Kiểm tra per-table
SELECT schemaname, tablename, age(relfrozenxid) AS xid_age
FROM pg_stat_user_tables
ORDER BY xid_age DESC
LIMIT 20;Nếu thấy age đang tiến gần 2 tỷ → chạy thủ công ngay:
sql
VACUUM FREEZE your_table;
-- Hoặc toàn bộ DB:
VACUUM FREEZE;5. Table bloat & Index bloat
Bloat xảy ra khi VACUUM không theo kịp tốc độ write, hoặc autovacuum bị tắt/config sai.
sql
-- Xem dead tuple ratio
SELECT schemaname,
tablename,
n_dead_tup,
n_live_tup,
round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_autovacuum,
last_vacuum
FROM pg_stat_user_tables
WHERE n_live_tup > 0
ORDER BY dead_pct DESC
LIMIT 20;
-- dead_pct > 10-20% → nên chạy VACUUM thủ công hoặc xem lại autovacuum configIndex bloat thường đi kèm table bloat — index vẫn giữ pointer đến dead rows. VACUUM dọn table sẽ đồng thời clean index entries liên quan.
6. VACUUM ANALYZE
ANALYZE cập nhật statistics cho query planner — planner dùng statistics để ước lượng rows, chọn join strategy, quyết định có dùng index không.
Nếu statistics cũ → planner estimate sai → plan sai → query chậm dù index đúng.
sql
-- Chạy cả hai cùng lúc
VACUUM (ANALYZE, VERBOSE) orders;
-- Chỉ analyze (không cần dọn dead rows)
ANALYZE orders;Autovacuum chạy ANALYZE tự động theo autovacuum_analyze_scale_factor. Sau bulk import lớn nên chạy ANALYZE thủ công ngay — autovacuum chưa kịp trigger.
7. Monitoring autovacuum
sql
-- Xem autovacuum có đang chạy không
SELECT pid, query, now() - query_start AS duration
FROM pg_stat_activity
WHERE query LIKE 'autovacuum%';
-- Xem lịch sử vacuum của từng bảng
SELECT schemaname,
tablename,
last_vacuum,
last_autovacuum,
vacuum_count,
autovacuum_count
FROM pg_stat_user_tables
ORDER BY last_autovacuum DESC NULLS LAST;
-- Bật log autovacuum chạy lâu (postgresql.conf)
-- log_autovacuum_min_duration = 500 -- log khi chạy > 500msAutovacuum chạy liên tục không xong thường do:
- Bảng quá write-heavy, dead rows tạo nhanh hơn autovacuum dọn được
autovacuum_vacuum_cost_delayquá cao → autovacuum bị throttle- Long-running transaction giữ snapshot cũ → VACUUM không thể dọn dead rows mà transaction đó còn cần đọc
sql
-- Tìm transaction đang giữ snapshot cũ
SELECT pid, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 5;