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:

  1. 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á.
  2. 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.

Ảnh chụp đoạn mã SQL nền tối minh hoạ HOT update cập nhật một dòng mà không đụng index, nhắc lại mỗi UPDATE tạo dòng mới MVCC dòng mới ở vị trí vật lý mới mọi index phải trỏ lại dòng mới đó index bloat cộng tốn ghi HOT Heap-Only Tuple tránh việc này, HOT dòng mới ở lại cùng trang index không đổi điều kiện HOT cần cả hai 1 không cột nào được index bị thay đổi 2 trang hiện tại còn chỗ trống cho phiên bản mới fillfactor khi đó dòng mới nằm trong cùng trang dùng con trỏ nội bộ index cũ vẫn trỏ đúng không cần cập nhật index nào, đo cột không index vs cột có index A update cột không index bảng fillfactor 50 chừa chỗ UPDATE c1 SET ni bằng ni cộng 1 WHERE id nhỏ hơn bằng 200000 100 phần trăm HOT B update cột có index UPDATE c2 SET ci bằng ci cộng 1 WHERE id nhỏ hơn bằng 200000 0 phần trăm HOT ci có index HOT bị phá mỗi update thêm một entry index, theo dõi n_tup_hot_upd trong pg_stat_user_tables SELECT n_tup_upd n_tup_hot_upd round n_tup_hot_upd chia n_tup_upd nhân 100 1 AS pct_hot FROM pg_stat_user_tables WHERE relname bằng c1 pct_hot thấp trên bảng ghi nhiều bằng mất lợi ích HOT

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:

Ảnh chụp bảng kết quả đo thật nền tối HOT update bảng 500000 dòng fillfactor 50 PostgreSQL 16 200000 update mỗi ca pg_stat_user_tables pg_relation_size shared_buffers 128MB, cột không index vs cột có index 200000 update ca A cột ni không index n_tup_hot_upd chia n_tup_upd 200000 chia 200000 100 phần trăm HOT index sau 11 MB không đổi ca B cột ci có index 0 chia 200000 0 phần trăm HOT index sau 11 lên 15 MB phình, update cột không index 100 phần trăm HOT index không đụng tới giữ 11 MB update cột có index 0 phần trăm HOT mỗi update thêm entry index index phình 11 lên 15 MB, điều kiện HOT cần cả hai 1 không đổi cột được index nếu vi phạm đổi cột index mọi index phải trỏ lại HOT bị phá 2 trang còn chỗ trống nếu vi phạm trang đầy fillfactor 100 không đủ chỗ cho bản mới HOT thất bại, cốt lõi HOT Heap-Only Tuple 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à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 phần trăm HOT giữ index nguyên update cột có index 0 phần trăm HOT làm index phình đừng index cột hay cập nhật và dùng fillfactor để chừa chỗ

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ề

  1. 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).
  2. Đổ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.
  3. Đừng index cột hay cập nhật, và dùng fillfactor để chừa chỗ: theo dõi n_tup_hot_upd/pct_hot trong pg_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.