Bài MVCC cho thấy mỗi UPDATE tạo một phiên bản dòng mới ở vị trí vật lý khác. Điều đó có một hệ quả đắt: vì dòng mới ở vị trí mới, mọi index của bảng phải được cập nhật để trỏ tới nó — dù cột được index có đổi hay không. Với bảng nhiều index và ghi nhiều, đây là chi phí lớn cả về tốc độ lẫn bloat. HOT (Heap-Only Tuple) là tối ưu tránh chính điều này: trong một số điều kiện, PostgreSQL tạo phiên bản mới mà không đụng index nào. Bài này đo thật khi nào HOT xảy ra và tác động của nó.
HOT: dòng mới ở lại cùng trang, index không đổi
Ý tưởng của HOT: nếu phiên bản dòng mới có thể nằm trong cùng một trang với dòng cũ, và không cột nào được index bị thay đổi, thì PostgreSQL dùng một con trỏ nội bộ trong trang để nối bản cũ với bản mới. Các entry index cũ vẫn trỏ đúng (chúng trỏ tới trang, và chuỗi HOT trong trang dẫn tới bản mới nhất) — nên không index nào cần cập nhật.
Điều kiện để HOT xảy ra — cần cả hai:
- Không cột nào được index bị thay đổi. Nếu bạn đổi một cột có index, entry index của cột đó phải trỏ tới giá trị mới, nên HOT bị phá.
- Trang hiện tại còn chỗ trống cho phiên bản mới. Nếu trang đầy (fillfactor 100 mặc định, không còn khoảng trống), bản mới phải sang trang khác và HOT thất bại.

Hình 1: HOT cho phép UPDATE tạo phiên bản mới ngay trong cùng trang mà không cập nhật index — nếu không đổi cột được index và trang còn chỗ. Theo dõi qua n_tup_hot_upd.
Đo thật: cột không index đạt 100% HOT
Trên bảng 500.000 dòng với fillfactor=50 (để chừa chỗ trống trong trang), có cột ci được index và cột ni không index. Cập nhật 200.000 dòng mỗi ca:

Hình 2: Cập nhật cột ni (không index): 200.000/200.000 HOT (100%), index giữ nguyên 11 MB. Cập nhật cột ci (có index): 0/200.000 HOT (0%), index phình 11 → 15 MB vì mỗi update thêm một entry index.
- Ca A — update cột không index (
ni): 200.000/200.000 HOT (100%). Index giữ nguyên 11 MB — HOT không đụng tới index chút nào. - Ca B — update cột có index (
ci): 0/200.000 HOT (0%). Index phình từ 11 lên 15 MB — mỗi update thêm một entry index trỏ tới phiên bản dòng mới.
Con số nói rõ tất cả: chỉ cần đổi một cột có index, toàn bộ lợi ích HOT biến mất và index bắt đầu bloat. Ngược lại, giữ mọi update trên cột không index (với trang còn chỗ) cho 100% HOT — index hoàn toàn không đổi.
Theo dõi HOT
pg_stat_user_tables cho biết tỉ lệ HOT của một bảng:
SELECT n_tup_upd, n_tup_hot_upd,
round(n_tup_hot_upd::numeric / n_tup_upd * 100, 1) AS pct_hot
FROM pg_stat_user_tables WHERE relname = 'c1';
pct_hot thấp trên một bảng ghi nhiều là cờ đỏ: nó nghĩa là các update đang phá HOT (do đổi cột index hoặc trang đầy), gây bloat cả bảng lẫn index. Đây là một trong những chỉ số vận hành đáng theo dõi nhất cho bảng cập nhật thường xuyên.
Đánh đổi cần cân nhắc
Đừng index cột bạn cập nhật thường xuyên. Đây là bài học lớn nhất từ HOT. Một cột như bộ đếm, trạng thái, hay updated_at mà bạn cập nhật liên tục — đặt index lên nó phá HOT cho mọi update và gây bloat index nghiêm trọng. Chỉ index cột đó nếu bạn thật sự cần truy vấn theo nó, và cân nhắc kỹ chi phí ghi.
fillfactor để chừa chỗ cho HOT. Điều kiện thứ hai của HOT — trang còn chỗ — không tự có với fillfactor=100 mặc định (trang lấp đầy khi INSERT). Đặt fillfactor thấp hơn (ví dụ 80-90) cho bảng cập nhật nhiều để lại khoảng trống cho phiên bản mới ở lại trong trang. Đây là chủ đề bài sau — đánh đổi giữa chỗ trống ban đầu và tỉ lệ HOT.
HOT không giúp gì cho INSERT hay DELETE. Nó chỉ là tối ưu cho UPDATE. Bảng chủ yếu INSERT (log, sự kiện) không hưởng lợi từ HOT; với chúng, quan tâm tới autovacuum insert và bloat theo cách khác.
Ba ý mang về
- HOT cho UPDATE tạo phiên bản mới trong cùng trang mà không cập nhật index nào — nếu (1) không đổi cột được index và (2) trang còn chỗ; đo thật update cột không index đạt 100% HOT giữ index nguyên (11 MB).
- Đổi cột có index phá HOT và làm index bloat: đo thật, update cột có index đạt 0% HOT và index phình từ 11 lên 15 MB vì mỗi update thêm một entry — trong khi cùng số update trên cột không index không đụng index chút nào.
- Đừng index cột hay cập nhật, và dùng fillfactor để chừa chỗ: theo dõi
n_tup_hot_upd/pct_hottrongpg_stat_user_tables— tỉ lệ HOT thấp trên bảng ghi nhiều là cờ đỏ báo bloat.
Phần sau ta đi sâu vào điều kiện thứ hai của HOT: Phần sau mổ xẻ fillfactor — cách chừa chỗ trống trong trang để HOT hoạt động, đánh đổi giữa dung lượng ban đầu và tỉ lệ HOT, và giá trị hợp lý cho bảng cập nhật nhiều.