Một bảng vài trăm triệu tới hàng tỉ dòng không sống được nhờ một tối ưu đơn lẻ — nó cần kết hợp nhiều đòn bẩy lưu trữ. Sê-ri đã bàn phân vùng ở các bài trước; bài này tổng hợp và đo thật những đòn bẩy còn lại: fillfactor/HOT update cho bảng ghi nặng, và TOAST giữ heap chính luôn hẹp bất kể cột lớn cỡ nào.

Đòn bẩy 2: FILLFACTOR và HOT update

fillfactor quyết định PostgreSQL lấp đầy bao nhiêu phần trăm mỗi trang khi chèn. Mặc định 100 (lấp đầy). Đặt xuống 90 chừa 10% chỗ trống mỗi trang — và chỗ trống đó cho phép HOT update (Heap-Only Tuple): khi cập nhật một cột không có index, phiên bản mới của dòng nằm cùng trang, nên PostgreSQL không phải cập nhật index và ít sinh bloat.

CREATE TABLE t(...) WITH (fillfactor=90);   -- chừa 10% mỗi trang cho HOT update

Đo thật trên bảng 200k dòng, UPDATE cột pad (không có index) 3 lần:

  • fillfactor=100 (mặc định): HOT 0% (264/600.000), bảng phình lên 59 MB.
  • fillfactor=90: HOT 43% (259.402/600.000), bảng chỉ 48 MB.

Việc chừa chỗ trong trang cho phép 43% update nằm tại chỗ (HOT) — không đụng index, ít bloat hơn, và bảng nhỏ hơn. Với bảng ghi nặng (cập nhật thường xuyên các cột không index), đây là đòn bẩy quan trọng.

Ảnh chụp đoạn mã SQL nền tối minh hoạ bảng rất lớn các đòn bẩy lưu trữ để bảng tỉ dòng vẫn chạy được, 1 phân vùng đã bàn chia bảng lớn để pruning cộng archival rẻ RANGE theo thời gian mỗi truy vấn chỉ chạm vùng liên quan DROP vùng cũ tức thì nền tảng cho mọi bảng cực lớn theo thời gian log sự kiện đơn hàng, 2 FILLFACTOR cộng HOT update chừa chỗ trong trang cho bảng ghi nặng CREATE TABLE t WITH fillfactor 90 chừa 10 phần trăm mỗi trang HOT update sửa cột không index phiên bản mới nằm cùng trang không cập nhật index ít bloat chỉ hiệu quả khi có chỗ trống trong trang, 3 TOAST giá trị lớn tự nén hoặc để ngoài dòng cột text bytea lớn PostgreSQL nén trước pglz lz4 còn to thì chuyển sang bảng TOAST riêng dòng chính chỉ giữ con trỏ heap chính vẫn hẹp truy vấn không đụng cột lớn không trả giá đọc nó, 4 sắp cột giảm padding cộng 5 tablespace phân tầng lưu trữ sắp cột từ rộng đến hẹp giảm byte đệm column tetris nhân theo tỉ dòng CREATE TABLESPACE ssd_nong LOCATION mnt ssd ALTER TABLE vung_nong SET TABLESPACE ssd_nong data nóng lên SSD nhanh data lạnh partition cũ để trên đĩa rẻ chậm hơn phân tầng theo nhiệt độ

Hình 1: Năm đòn bẩy lưu trữ cho bảng rất lớn — phân vùng (pruning + archival), fillfactor/HOT (ghi nặng), TOAST (giá trị lớn), sắp cột giảm padding, và tablespace phân tầng theo nhiệt độ dữ liệu.

Đòn bẩy 3: TOAST giữ heap chính hẹp

TOAST (The Oversized-Attribute Storage Technique) xử lý các cột lớn (text, bytea, jsonb) tự động: PostgreSQL nén trước (pglz hoặc lz4), và nếu vẫn quá lớn thì chuyển ra bảng TOAST riêng, dòng chính chỉ giữ con trỏ. Kết quả: heap chính luôn hẹp bất kể giá trị lớn cỡ nào.

Đo thật hai kịch bản:

Ảnh chụp bảng kết quả đo thật nền tối FILLFACTOR cho HOT update TOAST giữ heap chính hẹp bảng 200k dòng UPDATE cột không index 3 lần text lớn PostgreSQL 16, FILLFACTOR cộng HOT update UPDATE cột pad không có index fillfactor 100 mặc định HOT 0 phần trăm 264 trên 600k size 59 MB fillfactor 90 chừa 10 phần trăm HOT 43 phần trăm 259k trên 600k size 48 MB chừa chỗ trong trang 43 phần trăm update nằm tại chỗ HOT không đụng index ít bloat, TOAST giá trị lớn không làm phình heap chính text lặp lại dễ nén 20k dòng khoảng 300MB thô main 6.960 kB TOAST 0 nén khoảng 42 lần vừa nhỏ để giữ inline text md5 ngẫu nhiên khó nén khoảng 9600 byte mỗi dòng main 1.024 kB TOAST 195 MB chuyển ra ngoài dòng main chỉ con trỏ, bảng đòn bẩy fillfactor 90 HOT 43 phần trăm vs 0 phần trăm 48 vs 59 MB ít bloat cho bảng ghi nặng TOAST dễ nén 300MB xuống 6,96MB khoảng 42 lần nén tự động TOAST khó nén main 1MB TOAST 195MB heap chính hẹp cột lớn ra ngoài, chiến lược bảng tỉ dòng bằng kết hợp phân vùng pruning cộng DROP vùng cũ cộng fillfactor ghi nặng cộng TOAST cộng sắp cột cộng tablespace phân tầng

Hình 2: fillfactor=90 cho 43% HOT update (48 MB) so với 0% của fillfactor=100 (59 MB). TOAST: text lặp lại (dễ nén) 300 MB thô → main 6,96 MB (nén ~42 lần, giữ inline); text md5 ngẫu nhiên (khó nén) → main chỉ 1 MB (con trỏ), TOAST 195 MB (chuyển ra ngoài dòng).

  • Text lặp lại (dễ nén), 20k dòng, ~300 MB thô: main=6.960 kB, TOAST=0 — nén ~42 lần và đủ nhỏ để giữ inline trong heap chính.
  • Text md5 ngẫu nhiên (khó nén), ~9600 byte/dòng: main=1.024 kB, TOAST=195 MB — không nén được nhiều nên chuyển ra ngoài dòng; heap chính chỉ còn 1 MB con trỏ, dữ liệu lớn nằm ở bảng TOAST.

Điểm quan trọng: dù cột lớn thế nào, heap chính vẫn hẹp. Truy vấn không đụng cột lớn (ví dụ SELECT id, ten FROM ...) không phải đọc qua nó — chỉ khi thật sự lấy cột TOAST mới trả giá. Đây là lý do đặt blob lớn vào cột riêng an toàn.

Kết hợp các đòn bẩy

Chiến lược cho bảng tỉ dòng là kết hợp, không phải chọn một:

  • Phân vùng (các bài trước): nền tảng — pruning cho truy vấn, DROP vùng cũ cho archival, VACUUM/index theo vùng nhỏ.
  • fillfactor trên các partition ghi nặng để tối đa HOT update.
  • TOAST tự lo cột lớn — chỉ cần thiết kế schema tách blob ra cột riêng.
  • Sắp cột từ rộng đến hẹp (column tetris, bài kiểu số đã đo) giảm byte đệm — nhân theo tỉ dòng thành nhiều GB.
  • Tablespace phân tầng: CREATE TABLESPACE + ALTER TABLE ... SET TABLESPACE để đặt partition nóng lên SSD nhanh, partition cũ (lạnh) trên đĩa rẻ hơn.

Đánh đổi cần cân nhắc

fillfactor thấp làm bảng lớn hơn lúc đầu. Chừa 10% mỗi trang nghĩa là bảng chiếm nhiều trang hơn khi mới nạp (chưa update). Lợi ích HOT chỉ bù lại khi bảng thật sự bị update nhiều. Với bảng chỉ-đọc hoặc chỉ-chèn (append-only như log), giữ fillfactor=100. Chỉ hạ cho bảng update-heavy.

TOAST có chi phí truy cập cột lớn. Khi bạn thật sự đọc cột TOAST out-of-line, PostgreSQL phải đọc thêm từ bảng TOAST và giải nén — chậm hơn cột inline. Nếu một cột lớn được đọc thường xuyên cùng phần còn lại của dòng, cân nhắc ALTER TABLE ... SET STORAGE để chỉnh chiến lược, hoặc thiết kế lại.

Tablespace thêm phức tạp vận hành. Phân tầng lưu trữ mạnh nhưng thêm điểm cần quản lý (sao lưu, quyền, dung lượng từng tầng). Chỉ dùng khi lợi ích chi phí lưu trữ thật sự đáng — nhiều hệ thống chỉ cần một tầng SSD tốt.

Ba ý mang về

  1. fillfactor=90 cho phép HOT update, giảm bloat cho bảng ghi nặng: đo thật, UPDATE cột không-index cho 43% HOT (bảng 48 MB) so với 0% HOT của fillfactor=100 (59 MB) — chừa chỗ trong trang để phiên bản mới nằm cùng trang, không đụng index; chỉ hạ cho bảng update-heavy.
  2. TOAST giữ heap chính luôn hẹp: đo thật, text lặp lại nén ~42 lần (300 MB → 6,96 MB, giữ inline), text ngẫu nhiên chuyển ra ngoài dòng (main chỉ 1 MB, TOAST 195 MB) — truy vấn không đụng cột lớn không trả giá đọc nó.
  3. Chiến lược bảng tỉ dòng là KẾT HỢP: phân vùng (nền tảng) + fillfactor (ghi nặng) + TOAST (cột lớn) + sắp cột giảm padding + tablespace phân tầng — không đòn bẩy đơn lẻ nào đủ, và mỗi cái có đánh đổi riêng.

Phần sau ta xét khi một máy đơn không còn đủ: Phần sau bàn sharding — chia dữ liệu ngang qua nhiều máy chủ, khi nào cần, các cách tiếp cận (Citus, phân mảnh ứng dụng), và cái giá của phân tán.