Suốt các bài về scan, join và aggregate, một cụm từ lặp đi lặp lại: "planner ước lượng số dòng". Giờ ta đào tới tận gốc — ước lượng đó đến từ đâu? Câu trả lời là thống kê, dữ liệu mà PostgreSQL thu thập về nội dung mỗi bảng. Khi thống kê đúng, planner chọn kế hoạch tốt; khi thống kê cũ, nó có thể ước lượng sai hàng trăm nghìn lần và chọn kế hoạch thảm họa. Bài này chỉ ra PostgreSQL biết gì, lưu ở đâu, và đo cái bẫy thống kê cũ.

PostgreSQL biết gì về dữ liệu

Với mỗi cột, PostgreSQL lưu một bộ thống kê trong bảng hệ thống pg_statistic, xem thân thiện qua view pg_stats:

SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats WHERE tablename='kh';
  • n_distinct: số giá trị phân biệt trong cột — dùng để ước lượng độ chọn lọc và số nhóm cho GROUP BY.
  • most_common_vals (MCV): các giá trị phổ biến nhất kèm tần suất (most_common_freqs). Ví dụ cột tinh có MCV {HCM, CT, DN, HN, HP} mỗi giá trị ~0,20.
  • histogram_bounds: phân bố của phần còn lại, dùng ước lượng khoảng (>, <, BETWEEN).
  • correlation: mức tương quan giữa cột và thứ tự vật lý trên đĩa — dùng cho BRIN và mô hình chi phí.

Ở cấp bảng, pg_class giữ reltuples (số dòng ước lượng) và relpages (số trang).

Ảnh chụp đoạn mã SQL nền tối minh hoạ thống kê bảng cái planner dựa vào để ước lượng và ANALYZE, mọi quyết định của planner scan join aggregate nào đều dựa trên ước lượng số dòng lấy từ thống kê trong pg_statistic xem qua pg_stats, PostgreSQL lưu cho mỗi cột n_distinct số giá trị phân biệt most_common_vals các giá trị phổ biến nhất kèm tần suất MCV histogram_bounds phân bố phần còn lại cho ước lượng khoảng correlation tương quan cột với thứ tự vật lý, cấp bảng reltuples số dòng relpages số trang trong pg_class, ANALYZE lấy mẫu bảng tính lại các thống kê, autovacuum tự chạy autoanalyze khi số dòng đổi vượt ngưỡng nhưng sau thao tác lớn nạp xóa đổi phân bố nên chạy ANALYZE tay ngay đừng chờ, kiểm last_analyze n_mod_since_analyze trong pg_stat_user_tables

Hình 1: Thống kê PostgreSQL lưu cho mỗi cột (n_distinct, MCV, histogram, correlation) và cấp bảng (reltuples). ANALYZE lấy mẫu và tính lại; autovacuum tự chạy autoanalyze theo ngưỡng.

Đo thật: thống kê cũ, ước lượng sai 900.000 lần

Đây là kịch bản gây sự cố kinh điển. Bắt đầu với bảng kh 100.000 dòng (5 tỉnh), đã ANALYZE. Rồi chèn thêm 900.000 dòng toàn tỉnh mới 'BD' — nhưng chưa chạy ANALYZE. Truy vấn WHERE tinh='BD':

Ảnh chụp kết quả EXPLAIN ANALYZE nền tối đo thật thống kê cũ làm ước lượng sai 900000 lần PostgreSQL 16, bối cảnh bảng 100k dòng 5 tỉnh rồi chèn thêm 900k dòng tỉnh BD chưa ANALYZE, reltuples pg_class vẫn 100000 số cũ thực tế đã 1000000 MCV cột tinh HCM CT DN HN HP BD không có trong danh sách, truy vấn WHERE tinh bằng BD với thống kê cũ Parallel Seq Scan on kh cost rows 1 actual rows 900000 planner ước lượng 1 dòng thực tế 900000 dòng lệch 900000 lần vì BD không có trong MCV planner tưởng nó cực hiếm, sau ANALYZE kh reltuples 1000000 MCV BD 0.90 HN DN CT HP HCM n_distinct 6 Seq Scan on kh cost rows 901733 ước lượng giờ khớp thực tế, vì sao quan trọng ước lượng rows 1 khiến planner chọn Nested Loop Index Scan tưởng ít dòng rồi vấp 900k dòng thảm họa thống kê đúng là kế hoạch đúng chạy ANALYZE ngay sau mọi thao tác nạp xóa đổi phân bố lớn

Hình 2: Trước ANALYZE, planner ước lượng rows=1 cho tinh='BD' nhưng thực tế 900.000 dòng — lệch 900.000 lần, vì 'BD' chưa có trong MCV. Sau ANALYZE, ước lượng thành rows=901.733, khớp thực tế.

Kết quả rất rõ:

  • Trước ANALYZE: reltuples vẫn là 100.000 (số cũ, thực tế đã 1 triệu). MCV không chứa 'BD', nên planner cho rằng 'BD' là giá trị cực hiếm và ước lượng rows=1. Nhưng thực tế truy vấn khớp 900.000 dòng — lệch 900.000 lần.
  • Sau ANALYZE kh: reltuples thành 1.000.000, MCV cập nhật thành {BD:0.90, HN, DN, CT, HP, HCM}, n_distinct=6. Giờ planner ước lượng rows=901.733 — khớp thực tế.

Vì sao lệch ước lượng là thảm họa

Ước lượng rows=1 không chỉ là con số sai vô hại — nó dẫn đến kế hoạch sai. Nhớ bài về planner chọn join: nếu planner tin rằng tinh='BD' chỉ trả 1 dòng, nó có thể chọn Nested Loop tưởng bảng ngoài tí hon, rồi thực tế phải lặp 900.000 lần — đúng thảm họa Nested Loop. Hoặc chọn Index Scan cho một truy vấn thực ra lấy 90% bảng, gây 900.000 lần đọc heap ngẫu nhiên.

Nói cách khác: mọi kỹ thuật tối ưu ở các bài trước đều giả định thống kê đúng. Thống kê cũ làm sụp đổ toàn bộ quá trình ra quyết định của planner.

ANALYZE và autovacuum

ANALYZE lấy một mẫu dữ liệu (mặc định 30.000 dòng, điều chỉnh bằng default_statistics_target — bài sau) và tính lại toàn bộ thống kê trên. Nó nhanh và nhẹ so với việc quét cả bảng.

Autovacuum tự chạy autoanalyze. PostgreSQL theo dõi số dòng thay đổi từ lần analyze cuối (n_mod_since_analyze); khi vượt ngưỡng (mặc định ~10% bảng + 50 dòng), autovacuum tự chạy ANALYZE. Với hoạt động bình thường, thống kê được giữ tươi tự động.

Nhưng sau thao tác lớn, đừng chờ autovacuum. Nạp hàng loạt, xóa lớn, TRUNCATE rồi nạp lại, hay đổi phân bố dữ liệu đột ngột — chạy ANALYZE ten_bang; bằng tay ngay. Autovacuum có thể mất vài phút mới kích hoạt, và trong khoảng đó mọi truy vấn chạy trên thống kê sai. Kiểm bằng:

SELECT last_analyze, last_autoanalyze, n_mod_since_analyze
FROM pg_stat_user_tables WHERE relname='kh';

Đánh đổi và lưu ý

Thống kê là ước lượng từ mẫu, không tuyệt đối chính xác. n_distinct đặc biệt khó ước lượng đúng từ mẫu với cột có phân bố lệch. Nếu bạn biết chắc số giá trị phân biệt, có thể ghi đè: ALTER TABLE ... ALTER COLUMN ... SET (n_distinct = ...).

ANALYZE sau khi phục hồi backup. pg_restore không tự chạy ANALYZE — sau khi restore, thống kê trống rỗng và mọi truy vấn chạy kế hoạch tệ cho tới lần autoanalyze đầu. Chạy ANALYZE toàn database ngay sau restore.

Bảng tạm cần ANALYZE tay. Bảng tạm (CREATE TEMP TABLE) không được autovacuum đụng tới; nếu bạn nạp nhiều dữ liệu vào một bảng tạm rồi join/aggregate, hãy ANALYZE nó trước.

Ba ý mang về

  1. Planner quyết định dựa trên thống kê trong pg_stats — n_distinct, MCV, histogram, correlation cho từng cột, cùng reltuples/relpages cấp bảng; đây là toàn bộ những gì PostgreSQL "biết" để ước lượng số dòng.
  2. Thống kê cũ làm ước lượng sai thảm khốc: đo thật, sau khi chèn 900.000 dòng mà chưa ANALYZE, planner ước lượng rows=1 cho một truy vấn khớp 900.000 dòng (lệch 900.000 lần) — và ước lượng sai dẫn tới kế hoạch sai.
  3. Chạy ANALYZE ngay sau mọi thao tác lớn (nạp/xóa hàng loạt, restore backup, bảng tạm) thay vì chờ autovacuum; kiểm last_analyze và n_mod_since_analyze để biết thống kê có tươi không.

Phần sau ta chỉnh chính độ chi tiết của thống kê: Phần sau mổ xẻ default_statistics_target — số mẫu và số MCV/histogram PostgreSQL giữ, khi nào tăng nó để ước lượng chính xác hơn.