Statistics, ANALYZE và cách PostgreSQL ước lượng query Trung cấp
Thời gian đọc ước tính: 9 phút
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”Bảng tenant_demo có 100.000 dòng. Cột tenant_id (1-100) và region (west/east) không độc lập — dữ liệu được tạo sao cho tenant 1-50 luôn là west, tenant 51-100 luôn là east. Query:
SELECT * FROM tenant_demo WHERE tenant_id = 42 AND region = 'west';Thực tế câu này khớp đúng 1.000 dòng (toàn bộ dữ liệu của tenant 42). Nhưng planner ước lượng:
Seq Scan on tenant_demo (cost=0.00..2236.00 rows=489 width=26) (actual time=0.008..2.167 rows=1000 loops=1) Filter: ((tenant_id = 42) AND (region = 'west'::text))Execution Time: 2.206 msrows=489 nhưng actual rows=1000 — thấp hơn phân nửa.
Thực tế 1000 ████████████████████████████████████████Mặc định (độc lập) 489 ███████████████████▌Sau CREATE STATISTICS 1060 ██████████████████████████████████████████▍Với query đơn giản này không sao (vẫn Seq Scan, không đổi plan). Nhưng nếu tenant_demo là một bên của một join lớn, sai lệch 2 lần ở đây có thể khiến planner chọn sai thuật toán join phía trên (xem Nested loop, hash join và merge join).
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”Planner không đếm bảng cho mỗi query. Nó tra thống kê thu thập sẵn từ lần ANALYZE gần nhất rồi đoán số dòng khớp.
| Thống kê (mỗi cột) | Dùng để |
|---|---|
most_common_vals + tần suất (MCV) |
Ước lượng col = x cho giá trị phổ biến |
histogram_bounds |
Ước lượng khoảng <, >, BETWEEN |
n_distinct |
Ước lượng col = x cho giá trị hiếm, GROUP BY |
correlation |
Chọn Index Scan hay Bitmap (thứ tự vật lý khớp thứ tự logic?) |
Mặc định thống kê lưu theo từng cột riêng lẻ. Với A AND B, planner giả định độc lập rồi nhân hai selectivity. Giả định đúng phần lớn thời gian, sai khi hai cột tương quan như tenant_id và region.
Con số mặc định cần nhớ
Phần tiêu đề “Con số mặc định cần nhớ”| Tham số | Mặc định | Ý nghĩa |
|---|---|---|
default_statistics_target |
100 | Tối đa 100 MCV và 100 bucket histogram mỗi cột |
| Số dòng lấy mẫu | khoảng 300 × target (tức 30.000 dòng) | Cột có target lớn nhất quyết định cỡ mẫu |
autovacuum_analyze_threshold |
50 | Số dòng thay đổi tối thiểu |
autovacuum_analyze_scale_factor |
0,1 | Cộng thêm 10% kích thước bảng |
autovacuum_naptime |
1 phút | Chu kỳ autovacuum kiểm tra mỗi database |
Bảng 100.000 dòng: auto-analyze chạy khi có khoảng 50 + 0,1 × 100.000 = 10.050 dòng thay đổi kể từ lần analyze trước. Bảng 100 triệu dòng cần 10 triệu dòng đổi, lúc đó thống kê có thể cũ lâu. Bảng lớn nên hạ autovacuum_analyze_scale_factor theo từng bảng.
Bảng TEMP không bao giờ được auto-analyze: autovacuum không truy cập được bảng tạm. Mọi demo trong loạt bài này đều gọi ANALYZE thủ công vì lý do đó. Sau bulk load, hãy ANALYZE ngay. PostgreSQL 18 giữ lại thống kê của bảng khi pg_upgrade, nhưng extended statistics thì không; PG17 trở xuống cần ANALYZE sau upgrade.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”Vì sao 489, không phải 1000?
Phần tiêu đề “Vì sao 489, không phải 1000?”| Điều kiện | Selectivity ước lượng |
|---|---|
tenant_id = 42 |
Khớp 1/100 giá trị → ~1% |
region = 'west' |
Khớp khoảng 50% dữ liệu |
| Cả hai (nhân lại) | 1% × 50% × 100.000 ≈ 500 (planner tính ra 489, gần đúng công thức này) |
Đây chính là phép tính “giả định độc lập” — coi như một tenant bất kỳ có 50% cơ hội rơi vào mỗi region.
Thực tế, tenant 42 luôn luôn là west — không có sự “may rủi” 50-50 nào cả. Toàn bộ 1.000 dòng của tenant 42 đều khớp cả hai điều kiện, không phải một nửa. Chênh lệch 489 so với 1.000 chính là cái giá của giả định độc lập sai.
1%Thống kê mặc định lưu theo từng cột. tenant_id = 42 khớp 1/100 giá trị, nên planner ước lượng khoảng 1% của 100.000 dòng.
Sửa bằng CREATE STATISTICS
Phần tiêu đề “Sửa bằng CREATE STATISTICS”CREATE STATISTICS tenant_demo_tenant_region_stats (dependencies, mcv)ON tenant_id, region FROM tenant_demo;ANALYZE tenant_demo;CREATE STATISTICS không tự thu thập dữ liệu — nó chỉ khai báo “hãy để ý tương quan giữa hai cột này”. Bước ANALYZE sau đó mới thực sự tính. Chạy lại đúng câu query cũ:
Seq Scan on tenant_demo (cost=0.00..2236.00 rows=1060 width=26) (actual time=0.012..2.030 rows=1000 loops=1) Filter: ((tenant_id = 42) AND (region = 'west'::text))Execution Time: 2.056 ms489 → 1060, so với thực tế 1000. Từ ước lượng chỉ bằng 49% thực tế, giờ thành 106% thực tế — sai lệch theo chiều ngược lại nhưng nhỏ hơn nhiều.
(dependencies, mcv) yêu cầu PostgreSQL ghi nhận cả mức độ phụ thuộc giữa hai cột (dependencies) lẫn tổ hợp giá trị phổ biến (mcv), đủ để nhận ra “biết tenant_id, gần như đoán được region”.
Xác nhận thống kê đã tồn tại và loại nào được lưu:
statistics_name | kinds---------------------------------+------- tenant_demo_tenant_region_stats | {f,m}f là functional dependencies, m là MCV — khớp đúng hai loại đã khai báo.
Xem thống kê planner đang dùng
Phần tiêu đề “Xem thống kê planner đang dùng”-- Thống kê từng cộtSELECT attname, n_distinct, most_common_vals, most_common_freqs, correlationFROM pg_statsWHERE tablename = 'tenant_demo';
-- Extended statistics đã khai báo (pg_stats_ext hiển thị nội dung đã thu thập)SELECT stxname, stxkeys, stxkind FROM pg_statistic_ext;
-- Lần analyze gần nhấtSELECT relname, last_analyze, last_autoanalyze, n_mod_since_analyzeFROM pg_stat_user_tablesWHERE relname = 'tenant_demo';n_mod_since_analyze so với ngưỡng 50 + 0,1 × reltuples cho biết auto-analyze sắp chạy hay đã trễ.
Các loại extended statistics
Phần tiêu đề “Các loại extended statistics”| Loại | Trị vấn đề | Ví dụ |
|---|---|---|
dependencies |
Cột A xác định cột B | zip → city; chỉ áp dụng cho điều kiện = với hằng số và IN |
ndistinct |
GROUP BY a, b đoán sai số nhóm |
GROUP BY city, state |
mcv |
Tổ hợp giá trị phổ biến, hỗ trợ cả điều kiện khoảng | Cặp (country, currency) |
expressions (PG14 trở lên) |
Điều kiện trên biểu thức | WHERE date_trunc('day', ts) = ... |
Extended statistics chỉ cải thiện ước lượng trên một bảng. Sai lệch ở điều kiện join giữa hai bảng không được sửa bằng CREATE STATISTICS.
Quy trình chẩn đoán khi nghi ngờ estimate sai
Phần tiêu đề “Quy trình chẩn đoán khi nghi ngờ estimate sai”- Chạy
EXPLAIN (ANALYZE, BUFFERS), sorows=(ước lượng) vớiactual rows=ở node thấp nhất có sai lệch lớn — sai lệch ở node dưới sẽ lan lên các node cha phía trên. - Nếu chỉ một điều kiện đơn cột sai, thử
ANALYZElại trước (có thể do bulk load chưa kịp auto-analyze) hoặc tăngALTER TABLE ... ALTER COLUMN ... SET STATISTICScho riêng cột đó. - Nếu sai lệch xuất hiện đúng lúc kết hợp nhiều điều kiện như trường hợp trên, đó là dấu hiệu tương quan giữa các cột — dùng
CREATE STATISTICS. - Đo lại bằng
EXPLAIN ANALYZEsau mỗi thay đổi, đổi một yếu tố một lần — đừng vừa tăng target vừa thêm extended statistics cùng lúc rồi không biết cái nào có tác dụng.
Checklist production
Phần tiêu đề “Checklist production”- Theo dõi
n_mod_since_analyzevàlast_autoanalyzecủa các bảng lớn; bảng chậm auto-analyze thì hạautovacuum_analyze_scale_factorriêng cho bảng đó. - Sau bulk load, import, hoặc
pg_upgrade(PG17 trở xuống): chạyANALYZEthủ công ngay. - Điều tra estimate sai từ node thấp nhất có
rowslệchactual rows. - Một cột đơn sai:
ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 500rồiANALYZE, chỉ cho cột đó. - Nhiều điều kiện cùng lệch:
CREATE STATISTICScho đúng cặp cột tương quan, rồiANALYZE. - Đổi một yếu tố mỗi lần và đo lại bằng
EXPLAIN (ANALYZE, BUFFERS). - Bảng TEMP: luôn
ANALYZEthủ công.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Trade-offs
Phần tiêu đề “Trade-offs”Ưu điểm
- Cho phép planner ước lượng mà không cần đếm chính xác toàn bảng mỗi lần lập plan
CREATE STATISTICSsửa đúng nguyên nhân gốc (tương quan bị bỏ sót) thay vì ép plan bằng tay- Có thể áp dụng chọn lọc cho đúng cặp cột nghi ngờ, không cần bật cho toàn bảng hay toàn database
Nhược điểm
- Thống kê luôn là ước lượng từ mẫu (sample), không phải đếm chính xác — kể cả sau khi cải thiện, 1060 vẫn không phải 1000 tuyệt đối
CREATE STATISTICSkhông tự động thu thập; quênANALYZEsau khi tạo thì object tồn tại nhưng vô dụng- Tăng
default_statistics_targetcho toàn cột/toàn bảng làmANALYZEchậm hơn và tốn thêm dung lượng catalog, trong khi có thể chỉ cần extended statistics đúng một cặp cột