Cost-Based Ranker (P3/3): CBR và plan cache; biết ranker nào chạy
Ở phần trước: CBR ước lượng từ dữ liệu mẫu, và một lần chọn sai có thể làm query chậm gấp đôi. Với chunk sampling, CBR chọn sai 15 trên 200 lần.
- Cần đọc trước: Nguồn ước lượng và ba thí nghiệm
- Dẫn tới: Query Execution Engine. Phần này là phần cuối của bài về CBR.
CBR và plan cache
Cùng một cache, cùng luật chơi
[tài liệu] Cả hai cơ chế ghi plan thắng vào cùng plan cache và dùng lại cho các query cùng plan cache query shape. Luật Missing → Inactive → Active và replanning của bài Query Planner & Plan Cache áp dụng nguyên vẹn.
Chi tiết quan trọng là con số works được ghi cho một plan do CBR chọn.
[quan sát] Trong allPlansExecution, plan được CBR chọn tiếp tục chạy sau trial ngắn cho tới khi ra kết quả, chạy hết, hoặc chạm trần 30% số document của collection (300.000 works trên orders, 180.000 trên imported). Con số đó vào plan cache:
lần 1: CBR chọn sai tenantId_1
$planCacheStats: tenantId_1, isActive false, works 180.000 (= 0,3 × 600.000)
lần 2: entry Inactive → lập plan lại → CBR lấy mẫu mới, chọn region_1
region_1 cần 120.377 works ≤ 180.000 → thay entry, isActive true
lần 3+: dùng cache, cbr.count không tăng[chi tiết cài đặt] Trần 30% khớp với tham số nội bộ internalQueryPlanEvaluationCollFraction: 0.3.
Lần này cơ chế Inactive đã cứu: một lần chọn sai chỉ để lại entry Inactive, và lần chạy sau có cơ hội sửa. Nhưng nếu hai lần liên tiếp cùng chọn sai thì sao?
Thí nghiệm: plan sai thành Active
Lặp: xoá plan cache, chạy query thật (find(q).toArray(), không phải explain). Nếu lần đầu chọn sai, chạy thêm lần nữa và xem entry.
281 lần thử: 21 lần lần đầu chọn sai (7,5%, khớp thí nghiệm 4)
lần thử 281: lần thứ hai cũng sai → entry tenantId_1, isActive true, works 180.000
9 lần chạy tiếp theo:
cbr.count không tăng (dùng cache)
thời gian median 95 ms (91–98), so với 47–55 ms khi region_1 được cache[quan sát] Plan sai giờ được cache Active với budget replan 10 × 180.000 = 1,8 triệu works. Query chỉ cần 270.000. Theo luật replanning, entry này không bao giờ tự bị loại, cho tới khi cache bị xoá (restart, tạo/xoá/ẩn index, LRU) hoặc ai đó can thiệp.
Cùng loại bẫy với vụ plan cache lật trong bài Query Planner & Plan Cache, chỉ khác nguồn gốc: ở đó là tham số lệch, ở đây là mẫu xui. Hai lần xui liên tiếp, trong lab là 1 trên 281 lần cache trống (khoảng 0,4%), nghe nhỏ. Nhưng cache bị xoá mỗi lần tạo index hay restart, và mỗi lần như vậy là một lần thử mới.
Cách chữa
Giống hệt chương trước, vì vấn đề nằm ở chỗ có hai plan gần nhau mà planner phải đoán:
- Compound index phục vụ cả hai điều kiện (
{ tenantId: 1, region: 1 }) biến hai plan "đoán được" thành một plan rõ ràng tốt nhất. - Query settings (8.0+) ghim plan cho một query shape khi bạn biết chắc plan nào đúng. Xem bài Query Planner & Plan Cache.
- Không dùng các tham số nội bộ (
internalQueryCBRCEMode,internalQuerySamplingCEMethod) trên production. Chúng không có trong tài liệu, không được hỗ trợ, và có thể biến mất ở bản vá sau.
Làm sao biết ranker nào đã chạy
[quan sát] Trên 8.3.11, plan cache entry và slow query log không ghi plan do multi-planner hay CBR chọn. $planCacheStats của một entry do CBR tạo có cùng các trường như mọi entry khác, candidatePlanScores vẫn có một score kiểu multi-planner (2.0002). Slow query log ghi fromMultiPlanner: true và planningTimeMicros (58.855 µs cho lần chọn đúng, 74.672 µs cho lần chọn sai, vì thời gian lập plan gồm cả phần chạy tiếp tới trần 30%), nhưng không có trường nào về CBR.
Những gì dùng được trên 8.3:
| Công cụ | Cho biết gì | Ghi chú |
|---|---|---|
db.serverStatus().metrics.query.cbr | count (số lần CBR được dùng), choseWinningPlan, numPlans, numPlansFailedCostEstimation, numPlansTiedCostEstimation, micros, samplingMicros, và histogram thời gian | [tài liệu] mới từ 8.3.3. Đếm toàn server, không theo query. So sánh hai lần đọc để thấy CBR có chạy không |
explain() | costEstimate, cardinalityEstimate, numKeysEstimate, estimatesMetadata.ceSource trên các stage được CBR định giá | [tài liệu] mới từ 8.3.3. [quan sát] ở chế độ mặc định chỉ thấy trên plan bị loại. Explain bỏ qua plan cache, nên nó cho biết planner sẽ chọn gì bây giờ, không phải production đang dùng gì |
$queryStats → metrics.costBasedRanker | theo từng query shape: nDocsSampled (sum/min/max), cardinalityEstimationMethods (Sampling, Heuristics, Histogram, Mixed, Metadata, Code) | [tài liệu] mới từ 8.3.3; $queryStats được bật trên Atlas từ tier M10. [quan sát] trong lab tự host phải bật bằng tham số nội bộ, chỉ để kiểm chứng |
Một đoạn kiểm tra nhanh:
const before = db.serverStatus().metrics.query.cbr;
db.orders.find({ tenantId: "t0000", status: "completed", channel: "web" }).toArray();
const after = db.serverStatus().metrics.query.cbr;
print("CBR calls:", after.count - before.count, "chose:", after.choseWinningPlan - before.choseWinningPlan);Trên server có nhiều client, hiệu này lẫn cả query của người khác; theo từng query shape thì dùng $queryStats.
Từ 9.0 (không áp dụng cho 8.3): [tài liệu] $queryStats được bật mặc định (lấy mẫu 1% thao tác) và có nhóm metrics.queryPlanner mới, gồm bộ đếm true/false fromMultiPlanner, fromPlanCache và planShapeCounters. Tài liệu 9.0 không có trường slow query log nào cho biết ranker nào đã chạy.
So với PostgreSQL
PostgreSQL luôn ước lượng, không chạy thử. So sánh rõ nhất là ở chỗ ước lượng lấy dữ liệu từ đâu.
[tài liệu PostgreSQL] Planner dùng thống kê lưu trong catalog pg_statistic (bản dễ đọc là view pg_stats): số giá trị khác nhau (n_distinct), danh sách giá trị phổ biến nhất (most_common_vals), và histogram_bounds. Thống kê được tạo bởi ANALYZE, chạy tay hoặc do autovacuum tự chạy khi bảng thay đổi nhiều. Với bảng lớn, ANALYZE lấy một mẫu ngẫu nhiên chứ không đọc hết. default_statistics_target (mặc định 100) quyết định độ dài tối đa của danh sách giá trị phổ biến và số bin của histogram. Thống kê "luôn là xấp xỉ" và đổi chút ít sau mỗi lần ANALYZE, nên hiếm khi plan đổi theo. Với các cột tương quan, có thể tạo thống kê mở rộng bằng CREATE STATISTICS (dependencies, ndistinct, mcv).
Trong EXPLAIN, rows= là số dòng ước lượng mà một node trả ra. EXPLAIN ANALYZE in thêm actual rows. Cost tính theo đơn vị trừu tượng, quy ước theo chi phí đọc một trang đĩa.
| MongoDB 8.3 (CBR) | PostgreSQL | |
|---|---|---|
| Khi nào ước lượng | chỉ khi trial ngắn không ra kết quả (một lượng nhỏ query) | mọi query |
| Nguồn ước lượng | lấy mẫu lúc lập plan, mỗi lần một mẫu mới (~380 document) | thống kê lưu sẵn từ ANALYZE (mẫu ngẫu nhiên, MCV, histogram) |
| Dữ liệu cũ | không có khái niệm thống kê cũ: mẫu luôn mới | thống kê cũ sau thay đổi lớn là nguyên nhân kinh điển của plan sai |
| Tương quan giữa field | sampling đánh giá cả filter trên mẫu, nên tự nắm được | mặc định coi các cột độc lập; cần CREATE STATISTICS |
| Giá trị hiếm | thường đoán 0 | MCV và histogram nắm được giá trị phổ biến; giá trị hiếm ước lượng từ phần còn lại |
| Chi phí ước lượng | trả mỗi lần lập plan (~0,2 ms trong lab) | trả một lần lúc ANALYZE; lập plan chỉ đọc thống kê |
| Xem ước lượng vs thật | cardinalityEstimate (explain) vs nReturned/keys (executionStats) | rows= vs actual rows trong EXPLAIN ANALYZE |
| Điều khiển | không có tham số được tài liệu hoá | default_statistics_target, ALTER TABLE ... SET STATISTICS, CREATE STATISTICS |
Hai điểm đáng suy nghĩ:
- Mẫu mới mỗi lần của MongoDB tránh được vấn đề "thống kê cũ", nhưng đổi lại tính ngẫu nhiên: cùng query, cùng dữ liệu, hai lần lập plan có thể ra hai plan khác nhau (thí nghiệm 4). Thống kê của PostgreSQL chỉ đổi khi
ANALYZEchạy lại; tài liệu của nó thừa nhận thống kê đổi chút ít sau mỗi lầnANALYZE, và hiếm khi điều đó làm plan đổi theo. - Cách đọc
rows=cạnhactual rowsáp dụng y nguyên cho MongoDB: đặtcardinalityEstimatecạnh số thật. Lệch một bậc độ lớn ở plan đáng lẽ thắng là dấu hiệu của điểm mù.
Những lỗi thường gặp
- Nghĩ rằng 8.3 "chuyển sang cost-based optimizer như PostgreSQL". Trên 8.3, multi-planner vẫn quyết định đại đa số query. CBR chỉ là dự phòng khi trial ngắn không ra kết quả. [tài liệu] "chỉ gọi CBR cho một lượng nhỏ query".
- Nhầm query cardinality với selectivity hay relationship cardinality. Cardinality là số đếm qua một stage của một plan, selectivity là tỉ lệ, relationship cardinality là chuyện schema.
cardinalityEstimate: 0không có nghĩa "field này có cardinality thấp". - Tin ước lượng cho điều kiện hiếm. Mẫu ~380 document đoán 0 cho mọi điều kiện dưới khoảng 0,3%.
cardinalityEstimate: 0là "không thấy trong mẫu", không phải "không có". - Bật tham số nội bộ trên production để "tắt CBR" hay "lấy mẫu chuẩn hơn". Muốn ghim plan, dùng query settings. Muốn planner khỏi phải đoán, dùng compound index.
- Kiểm tra plan bằng một lần explain duy nhất cho query có thể gặp CBR. Mỗi lần lập plan là một mẫu mới. Với query trả 0 kết quả có hai index gần nhau, hãy explain nhiều lần và đếm.
Cột mốc: Bạn đã biết plan do CBR chọn cũng vào plan cache và có thể thành Active như mọi plan khác, và biết hai cách chữa khi plan sai: compound index hoặc query settings. Bài CBR khép lại ở đây.
Hỏi & đáp
Plan cache có entry do CBR chọn: tenantId_1, isActive: true, works: 180.000 trên collection 600.000 document. Chạy hết plan này cần 270.000 works. Điều gì xảy ra ở các lần chạy sau?
Một query trả 0 kết quả, có hai index gần nhau, thỉnh thoảng bị CBR chọn plan chậm gấp đôi và plan đó có lúc kẹt Active trong cache. Cách xử lý nào hợp lý trên production?
Nếu bỏ hết thuật ngữ: khi chạy thử mà không tuyến nào giao được kiện nào, người quản lý rút vài trăm phiếu trong kho để đoán tuyến nào ngắn hơn. Đoán khá đúng với khách lớn, không thấy khách nhỏ. Nếu phiếu được rút theo xấp liền nhau và một khách lớn nằm gọn trong vài xấp, có hôm anh đoán sai, ghi tuyến dài vào sổ, và cuốn sổ giữ tuyến đó rất lâu.
Bài tiếp theo
Đến đây, Phần Index & Query đã trả lời xong câu hỏi "chọn plan nào": multi-planner chạy thử, CBR ước lượng, plan cache ghi nhớ. Nhưng suốt hai bài về planner, ta coi việc chạy một plan là hộp đen. Ta chỉ đọc kết quả của nó qua works, advanced, isEOF, và thấy pipeline có $group thì chạy bằng một engine khác, đến mức CBR không chạm tới.
Bài Query Execution Engine mở hộp đen đó. Một plan là một cây stage hoạt động theo kiểu "kéo" (pull-based). works thật ra là gì, NEED_TIME và NEED_YIELD là gì. SORT dùng bao nhiêu bộ nhớ khi có và không có limit. Classic engine khác SBE ở đâu, và query nào chạy bằng engine nào trên 8.x. Cuối cùng là chuyện yielding: vì sao một lần đọc không nằm trong transaction có thể thấy một document hai lần hoặc bỏ sót nó. Câu hỏi đó dẫn thẳng sang Phần Transactions.
Tài liệu tham khảo
- Query Plans (Query Plan Options: multi-planner và cost-based ranker, plan cache)
- Explain Results (costEstimate, cardinalityEstimate, numKeysEstimate, estimatesMetadata, ceSource)
- serverStatus (metrics.query.cbr)
- $queryStats (metrics.costBasedRanker)
- Release Notes for MongoDB 8.3 (Cost-Based Ranker, 8.3.3)
- Release Notes for MongoDB 9.0 ($queryStats output, query stats enabled by default)
- $queryStats trong 9.0 (metrics.queryPlanner, planShapeCounters)
- PostgreSQL: Statistics Used by the Planner
- PostgreSQL: ANALYZE
- PostgreSQL: Using EXPLAIN