Bài trước ta thấy HOT update cần hai điều kiện: không đổi cột được index, và trang còn chỗ trống cho phiên bản mới. Điều kiện thứ hai không tự có: mặc định, PostgreSQL lấp đầy mỗi trang khi INSERT, không chừa chỗ nào cho UPDATE tương lai. fillfactor là tham số điều khiển chính xác điều này — chừa một phần trăm chỗ trống trong mỗi trang để HOT có đất diễn. Bài này đo thật tác động của fillfactor lên tỉ lệ HOT và bloat, và chỉ cách chọn giá trị hợp lý.
fillfactor là gì
fillfactor là phần trăm không gian của một trang được phép lấp đầy khi INSERT. Phần còn lại được chừa lại cho các phiên bản UPDATE mới ở lại trong cùng trang:
- fillfactor 100 (mặc định): lấp đầy trang khi INSERT — không chừa chỗ nào cho HOT.
- fillfactor 70: chỉ lấp 70% mỗi trang, chừa 30% cho phiên bản UPDATE mới.
CREATE TABLE tk (id int8 PRIMARY KEY, so int4) WITH (fillfactor = 70);
-- đổi sau: chỉ áp cho trang MỚI; cần VACUUM FULL để áp cho dữ liệu cũ
ALTER TABLE tk SET (fillfactor = 70);
VACUUM FULL tk; -- viết lại bảng để áp fillfactor mới cho dữ liệu hiện có
Lưu ý quan trọng: ALTER TABLE ... SET (fillfactor=...) chỉ ảnh hưởng các trang mới; muốn áp cho dữ liệu đã có phải viết lại bảng (VACUUM FULL hoặc pg_repack).

Hình 1: fillfactor chừa phần trăm chỗ trống trong mỗi trang khi INSERT để phiên bản UPDATE mới ở lại cùng trang (HOT). Khai khi tạo bảng, hoặc đổi sau bằng ALTER TABLE + VACUUM FULL.
Đo thật: fillfactor quyết định tỉ lệ HOT và bloat
Ba bảng 500.000 dòng với fillfactor 100, 90, 70, chạy cùng workload: 5 lượt UPDATE cột không index (2,5 triệu update tổng), rồi VACUUM và đo:

Hình 2: Kích thước ban đầu: fillfactor 100 → 21 MB (gọn nhất), 70 → 30 MB (to nhất, chừa chỗ). Sau 2,5 triệu update: fillfactor 100 cho 0% HOT phình lên 127 MB (~6×), fillfactor 70 cho 72,6% HOT chỉ phình lên 72 MB (~2,4×).
- fillfactor 100: ban đầu 21 MB (gọn nhất), nhưng 0% HOT — sau update phình lên 127 MB (~6×). Không có chỗ trống, mọi update sang trang khác → bloat khổng lồ.
- fillfactor 90: 28,1% HOT, phình lên 108 MB.
- fillfactor 70: ban đầu 30 MB (to nhất), nhưng 72,6% HOT — sau update chỉ 72 MB (~2,4×).
Điểm mấu chốt: fillfactor 70 bắt đầu to hơn (30 vs 21 MB) nhưng sau workload update nặng lại nhỏ hơn rất nhiều (72 vs 127 MB). Chừa chỗ trước đổi lấy tỉ lệ HOT cao và ít bloat — với bảng cập nhật nhiều, đây là lãi ròng rõ ràng.
Chọn giá trị theo tải
Không có giá trị "đúng" tuyệt đối — nó phụ thuộc bảng của bạn bị cập nhật nhiều hay không:
| Loại bảng | fillfactor gợi ý |
|---|---|
| Append-only (log, sự kiện) | 100 — không UPDATE, không cần chừa |
| UPDATE vừa phải | 85-90 |
| UPDATE rất nhiều (bộ đếm, trạng thái) | 70-80 |
Logic: bảng chỉ INSERT không bao giờ cần HOT nên fillfactor 100 tối ưu dung lượng. Bảng cập nhật càng nhiều, càng nên chừa chỗ để giữ HOT cao và tránh bloat + ghi index.
Đánh đổi cần cân nhắc
fillfactor thấp làm bảng to hơn khi mới nạp. Bạn đánh đổi dung lượng ban đầu lấy ít bloat về sau. Với bảng không cập nhật, đây là mất mát thuần (chỗ trống không bao giờ dùng) — vì vậy chỉ hạ fillfactor cho bảng thật sự update nhiều. Đừng áp fillfactor thấp cho mọi bảng theo phản xạ.
fillfactor cũng áp cho index. Index B-tree cũng có fillfactor riêng (mặc định 90 cho non-leaf). Với index trên cột hay bị thêm/xóa entry, chừa chỗ trong trang index giảm page split. Nhưng đây là tinh chỉnh nâng cao — mặc định thường ổn.
Hiệu quả HOT phụ thuộc cả pattern update. Ngay cả fillfactor 70 chỉ đạt 72% HOT ở đây (không 100%) vì sau nhiều lượt, chỗ trống dần cạn. Fillfactor giúp nhiều nhưng không tuyệt đối — kết hợp với autovacuum đều đặn (giải phóng chỗ dead để tái dùng) mới giữ HOT cao bền vững. Đo pct_hot thật của bảng để chỉnh fillfactor cho đúng.
Ba ý mang về
- fillfactor chừa chỗ trống trong trang để HOT hoạt động: mặc định 100 lấp đầy trang khi INSERT (không chỗ cho HOT); hạ xuống 70-90 chừa chỗ cho phiên bản UPDATE mới ở lại cùng trang.
- fillfactor thấp cho tỉ lệ HOT cao và ít bloat hơn nhiều: đo thật, cùng 2,5 triệu update, fillfactor 100 cho 0% HOT phình 6× (21→127 MB) còn fillfactor 70 cho 72,6% HOT chỉ phình 2,4× (30→72 MB) — chừa chỗ ban đầu là lãi ròng dưới tải update nặng.
- Chọn theo tải ghi: append-only giữ 100 (không cần chừa), UPDATE vừa phải 85-90, UPDATE rất nhiều 70-80 — và nhớ
ALTER TABLE SET fillfactorchỉ áp cho trang mới, cầnVACUUM FULL/pg_repack để áp cho dữ liệu cũ.
Phần sau ta rời chủ đề bloat/VACUUM sang một cơ chế nền tảng nguy hiểm: Phần sau mổ xẻ transaction ID wraparound — vì sao id giao dịch 32-bit có thể tràn, hậu quả thảm khốc nếu để xảy ra, và cách VACUUM freeze ngăn chặn.