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.

Ảnh chụp đoạn mã SQL nền tối minh hoạ VACUUM dọn dead tuple và nhiều hơn thế, VACUUM VERBOSE báo cáo nó làm gì VACUUM VERBOSE vt tuples 333333 removed 1000000 remain xóa dead tuple index scans 1 dọn con trỏ index tới dead index scan needed 5406 pages 75 phần trăm had 333333 dead item identifiers removed 333333 item id đã gỡ khỏi index, VACUUM làm ba việc không chỉ dọn chỗ 1 xóa dead tuple chỗ trống tái dùng không trả OS bài bloat 2 dọn con trỏ index trỏ tới dòng đã chết 3 cập nhật visibility map đánh dấu trang all-visible và freeze dòng cũ chống XID wraparound, visibility map mở khóa Index-Only Scan thật Index-Only Scan vẫn phải kiểm khả kiến mỗi dòng ở heap trừ khi trang được đánh dấu all-visible trong visibility map SELECT count id FROM vm WHERE id BETWEEN 1 AND 300000 chưa vacuum Heap Fetches bằng 300000 kiểm từng dòng sau vacuum Heap Fetches bằng 0 map nói trang đã all-visible, VACUUM vs VACUUM FULL VACUUM online không khóa dọn dead thành free giữ file chạy thường xuyên VACUUM FULL viết lại cộng thu nhỏ file nhưng khóa ACCESS EXCLUSIVE hiếm khi autovacuum tự chạy VACUUM thường hầu như không cần chạy tay

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:

Ảnh chụp bảng kết quả đo thật nền tối VACUUM dọn dead tuple bảng 1 triệu dòng PostgreSQL 16 VACUUM VERBOSE EXPLAIN ANALYZE shared_buffers 128MB, VACUUM VERBOSE sau UPDATE 333333 dòng index scans 1 pages 7208 remain 7208 scanned 100 phần trăm tuples 333333 removed 1000000 remain index scan needed 5406 pages 75 phần trăm had 333333 dead item identifiers removed VACUUM xóa dead tuple khỏi heap và dọn con trỏ index tới chúng một lượt quét index, visibility map VACUUM mở khóa Index-Only Scan thật chưa vacuum Index Only Scan Heap Fetches 300000 31 mili giây sau vacuum Index Only Scan Heap Fetches 0 18 mili giây chưa vacuum Index-Only Scan vẫn phải kiểm khả kiến từng dòng ở heap 300k lần fetch VACUUM đánh dấu trang all-visible trong visibility map Heap Fetches bằng 0 nhanh hơn khoảng 1,7 lần, VACUUM làm ba việc xóa dead tuple trả chỗ về freelist để tái dùng không thu nhỏ file dọn con trỏ index gỡ item id trỏ tới dòng chết khỏi mọi index cập nhật visibility map cộng freeze mở khóa index-only scan freeze dòng cũ chống XID wraparound, cốt lõi VACUUM không chỉ dọn rác nó xóa dead tuple dọn con trỏ index cập nhật visibility map mở khóa index-only scan thật Heap Fetches về 0 và freeze dòng cũ chống wraparound VACUUM chạy online không khóa autovacuum tự làm hầu như không cần chạy tay VACUUM FULL mới khóa

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 Scan nhưng Heap 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:

  1. 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).
  2. 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.
  3. 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ề

  1. 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.
  2. Visibility map mở khóa Index-Only Scan thật: đo thật, trước VACUUM một Index-Only Scan vẫn có Heap Fetches: 300000 (kiểm khả kiến từng dòng, 31 ms); sau VACUUM Heap Fetches: 0 (18 ms, ~1,7×) vì trang được đánh dấu all-visible.
  3. 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 FULL mớ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.