Bài về bloat cho thấy VACUUM cần thiết để dọn dòng chết. Nhưng "dọn rác" mới chỉ là một phần công việc của nó. VACUUM thực ra làm ba việc quan trọng, và một trong số đó — cập nhật visibility map — có ảnh hưởng trực tiếp tới tốc độ truy vấn theo cách nhiều người không ngờ. Bài này đọc kỹ output VACUUM VERBOSE để thấy nó làm gì, và đo thật cách nó biến một Index-Only Scan từ "vẫn phải đọc heap" thành thực sự chỉ-đọc-index.
VACUUM VERBOSE: nhìn vào bên trong
Cách tốt nhất để hiểu VACUUM là đọc báo cáo của chính nó. Sau khi UPDATE 333.333 dòng trong bảng 1 triệu dòng:
VACUUM (VERBOSE) vt;
-- tuples: 333333 removed, 1000000 remain
-- index scans: 1
-- index scan needed: 5406 pages (75.00%) had 333333 dead item identifiers removed
Ba thông tin quan trọng: nó xóa 333.333 dead tuple khỏi heap, thực hiện 1 lượt quét index, và trong lượt đó gỡ 333.333 con trỏ (item identifier) trỏ tới các dòng chết khỏi index. VACUUM không chỉ dọn heap — nó còn phải dọn mọi index của bảng, vì chúng chứa con trỏ tới các dòng vừa chết.

Hình 1: VACUUM VERBOSE cho thấy nó xóa dead tuple khỏi heap và dọn con trỏ index tới chúng. VACUUM làm ba việc: dọn dead tuple, dọn index, và cập nhật visibility map + freeze.
Việc thứ ba: visibility map mở khóa Index-Only Scan
Đây là phần bất ngờ nhất. VACUUM duy trì một cấu trúc gọi là visibility map (bản đồ khả kiến) — đánh dấu những trang mà mọi dòng đều hiển thị với mọi giao dịch (all-visible). Điều này nghe trừu tượng, nhưng nó quyết định Index-Only Scan có thực sự "chỉ đọc index" hay không.
Vấn đề: một Index-Only Scan đọc dữ liệu từ index, nhưng vẫn phải kiểm tra dòng có hiển thị không — và thông tin khả kiến nằm trong heap, không phải index. Trừ khi trang được đánh dấu all-visible trong visibility map, PostgreSQL vẫn phải "fetch heap" cho mỗi dòng để kiểm. Đo thật:

Hình 2: VACUUM VERBOSE xóa 333.333 dead tuple + dọn con trỏ index. Trước VACUUM, Index-Only Scan vẫn có Heap Fetches: 300000 (kiểm từng dòng ở heap, 31 ms); sau VACUUM, visibility map đánh dấu trang all-visible nên Heap Fetches: 0 (18 ms, ~1,7× nhanh hơn).
- Chưa VACUUM: kế hoạch là
Index Only ScannhưngHeap Fetches: 300000— nó vẫn phải đọc heap 300.000 lần để kiểm khả kiến. 31 ms. - Sau VACUUM:
Heap Fetches: 0— visibility map nói các trang đã all-visible, không cần đọc heap. 18 ms, nhanh hơn ~1,7 lần.
Đây là lý do một Index-Only Scan đôi khi không nhanh như kỳ vọng: nếu bảng vừa bị ghi nhiều và chưa VACUUM, visibility map lỗi thời và mọi dòng vẫn phải fetch heap. VACUUM (thường là autovacuum) giữ map cập nhật để index-only scan phát huy hết tốc độ.
Ba việc VACUUM làm
Tóm lại, mỗi lần VACUUM chạy nó làm:
- Xóa dead tuple: trả chỗ về "freelist" để tái dùng cho dòng mới (không thu nhỏ file — đó là việc của VACUUM FULL).
- Dọn con trỏ index: gỡ mọi item identifier trỏ tới dòng chết khỏi tất cả index của bảng.
- Cập nhật visibility map và freeze: đánh dấu trang all-visible (mở khóa index-only scan), và freeze các dòng cũ — gán cho chúng một trạng thái "luôn hiển thị" để chống XID wraparound (id giao dịch là số 32-bit, nếu không freeze sẽ tràn sau ~2 tỉ giao dịch và gây thảm họa mất dữ liệu).
Đánh đổi cần cân nhắc
Hầu như không cần chạy VACUUM tay — autovacuum lo việc đó. PostgreSQL có tiến trình autovacuum tự động chạy VACUUM khi dòng chết vượt ngưỡng. Trong đa số hệ thống, bạn không cần gõ VACUUM bao giờ. Chạy tay chỉ hữu ích sau một thao tác ghi/xóa lớn đột biến, hoặc để lấy visibility map cập nhật ngay trước một truy vấn index-only quan trọng.
VACUUM khác hẳn VACUUM FULL. VACUUM chạy online, không khóa đọc/ghi (chỉ khóa nhẹ), làm ba việc trên. VACUUM FULL viết lại toàn bộ bảng để thu nhỏ file nhưng khóa ACCESS EXCLUSIVE. Dùng VACUUM (hoặc autovacuum) thường xuyên; VACUUM FULL chỉ khi cần thu hồi dung lượng và có cửa sổ bảo trì.
VACUUM ANALYZE gộp hai việc. Ngoài dọn dẹp, thường nên cập nhật cả thống kê cho planner — VACUUM ANALYZE làm cả hai. Autovacuum cũng chạy autoanalyze riêng, nên thống kê được giữ tươi tự động; nhưng sau nạp dữ liệu lớn, chạy tay ANALYZE ngay là thói quen tốt (đã bàn ở bài thống kê).
Ba ý mang về
- VACUUM làm ba việc, không chỉ dọn rác: đo thật qua VERBOSE, nó xóa dead tuple (333.333), dọn con trỏ index tới chúng (một lượt quét index), và cập nhật visibility map — mỗi index của bảng đều được dọn.
- Visibility map mở khóa Index-Only Scan thật: đo thật, trước VACUUM một
Index-Only Scanvẫn cóHeap Fetches: 300000(kiểm khả kiến từng dòng, 31 ms); sau VACUUMHeap Fetches: 0(18 ms, ~1,7×) vì trang được đánh dấu all-visible. - VACUUM chạy online, autovacuum tự làm — hiếm khi cần chạy tay: nó không khóa đọc/ghi, còn freeze dòng cũ chống XID wraparound;
VACUUM FULLmới là cái thu nhỏ file và khóa bảng.
Phần sau ta đi vào chính tiến trình lo việc này tự động: Phần sau mổ xẻ autovacuum — cách nó quyết định khi nào chạy, các tham số ngưỡng quan trọng, và cách theo dõi để biết nó có theo kịp tải ghi không.