用同一个OR查询在两种选择性下的真实对比,看index_merge什么时候真的省时间,什么时候反而会比一次全表扫描更慢。
customer_id = 500 OR amount > <阈值>,只改amount的阈值,让匹配的总行数从约7%变到约92%。
代码SELECT * FROM orders WHERE customer_id = 500 OR amount > 475; -- 约7%的行匹配 SELECT * FROM orders WHERE customer_id = 500 OR amount > 50; -- 约92%的行匹配
优化器自己选的index_merge:
-> Sort-deduplicate by row ID (actual time=1.02..7.43 rows=1515 loops=1)
-> Index range scan using idx_cust (customer_id = 500)
-> Index range scan using idx_amount (475.00 < amount)
总耗时约 4.6 ms
强制关掉index_merge,改成全表扫描:
总耗时约 15.2 ms ← index_merge真实快3.3倍匹配行数少的时候,index_merge真实的思路是"分别走两条索引各自取出一小撮数据,再把这两撮数据按主键去重合并",两条索引扫描本身都很快(几百微秒到几毫秒),去重合并这一步(Sort-deduplicate)面对的也只是1515行,整体真实比读完全表要快得多。
amount>50这个条件本身就能匹配掉约92%的行,此时MySQL优化器自己会算出全表扫描更便宜,直接放弃index_merge。用UNION手动模拟"两条索引分别扫描再去重"的效果,看这个被优化器放弃的方案到底有多慢。
优化器自己选的方案(自动判定为全表扫描更划算): SELECT * FROM orders WHERE customer_id = 500 OR amount > 50; 耗时约 96.6 ms 手动模拟index_merge(UNION去重两条索引扫描的结果): (SELECT * FROM orders WHERE customer_id = 500) UNION (SELECT * FROM orders WHERE amount > 50); 耗时约 160.0 ms ← 比优化器选的全表扫描慢约66%
高选择性(约7%)时index_merge比全表扫描快3.3倍,低选择性(约92%)时手动模拟的index_merge反而比全表扫描慢66%——同一种执行策略,是快是慢完全取决于两条分支各自匹配了多少行。索引本身的范围扫描通常很快,真正的成本大头是"把两条分支的结果按行ID去重合并"这一步:匹配的行越多,这一步要处理的数据量就越大,随着匹配行数逼近全表,index_merge的代价会持续上升,直到反超一次简单的全表扫描。这也是为什么MySQL优化器要基于代价模型做选择,而不是"能用索引就一定用索引"——下一篇会用OPTIMIZER_TRACE把这个"去重"步骤的真实代价数字扒出来看。
每一步都是本文上面用EXPLAIN ANALYZE和真实计时跑出来的原始结果。
optimizer_switch='index_merge=off' + 手动UNION:分别验证两种极端选择性下的真实耗时