打开OPTIMIZER_TRACE,把上一篇index_merge的代价拆成读索引、回表、去重三块,用真实数字验证'去重才是大头'这个结论。
EXPLAIN只告诉你最终选了哪个计划,OPTIMIZER_TRACE会把优化器内部比较过的每一个候选方案和它们各自的代价数字都记录下来。
代码SET optimizer_trace='enabled=on'; SELECT COUNT(*) FROM orders WHERE customer_id = 500 OR amount > 475; SELECT TRACE FROM information_schema.OPTIMIZER_TRACE; SET optimizer_trace='enabled=off';
"indexes_to_merge": [
{ "index_to_merge": "idx_cust", "cumulated_cost": 0.560549 },
{ "index_to_merge": "idx_amount", "cumulated_cost": 355.08 }
],
"cost_of_reading_ranges": 355.08, ← 读两条索引range本身的代价
"cost_sort_rowid_and_read_disk": 514.794, ← 按rowid排序+回表读磁盘的代价
"cost_duplicate_removal": 3657.61, ← 去重的代价,真实最大的一块
"total_cost": 4527.48
对照:table_scan候选方案:
"table_scan": { "rows": 49488, "cost": 5135.15 }trace里真实把index_merge这一个候选方案的代价拆成了三块:cost_of_reading_ranges(355.08,读idx_cust和idx_amount两条索引range本身)、cost_sort_rowid_and_read_disk(514.79,把两条索引扫描出的rowid排序后回表读数据)、cost_duplicate_removal(3657.61,对读出来的行按主键去重)。三个数字里,去重这一项真实是另外两项加起来的5倍还多——这和上一篇在真实计时里观察到的"index_merge在低选择性下变慢"完全对得上:匹配行数越多,需要去重的行就越多,这一块代价会持续膨胀。
"original_condition": "((customer_id = 500) or (amount > 475))",
"steps": [
{ "transformation": "equality_propagation",
"resulting_condition": "(multiple equal(500, customer_id) or (amount > 475))" },
{ "transformation": "constant_propagation", ... },
{ "transformation": "trivial_condition_removal",
"resulting_condition": "(multiple equal(500, customer_id) or (amount > 475.00))" }
]trace真实记录了WHERE条件从原始文本到最终形态经过的每一步改写:equality_propagation把customer_id = 500规范成内部的"多重相等"表示,constant_propagation和trivial_condition_removal做进一步的常量传播和化简。这类改写平时完全不可见,只有打开OPTIMIZER_TRACE才能真实看到优化器在背后做了什么。
EXPLAIN(包括EXPLAIN ANALYZE)只会展示优化器最终选中的那一个执行计划,如果想知道"为什么没选另一个看起来也合理的方案",或者想验证一个执行计划贵在哪个具体步骤,就需要OPTIMIZER_TRACE——它把优化器内部比较过的全部候选方案、每个方案的代价拆解、条件改写的每一步都真实记录了下来。代价是trace本身的JSON输出可以非常大(这次简单的查询trace文本就有好几KB),而且optimizer_trace_max_mem_size有默认上限,复杂查询的trace可能被截断,所以一般只在排查具体问题时临时打开,不会在生产环境常开。
每一步都是本文上面用OPTIMIZER_TRACE跑出来的原始结果。
SET optimizer_trace='enabled=on' + information_schema.OPTIMIZER_TRACE