bigywater.tech
MySQL / InnoDB · 47

索引合并为什么有时更慢

用同一个OR查询在两种选择性下的真实对比,看index_merge什么时候真的省时间,什么时候反而会比一次全表扫描更慢。

3.3倍更快
高选择性下index_merge对全表扫描的真实优势
66%更慢
低选择性下手动模拟index_merge的真实代价

做的事:同一个OR查询,调整两个分支各自匹配的行数比例

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真实比全表扫描快3.3倍

实测 · 本机EXPLAIN ANALYZE,amount>475(约7%行匹配)
优化器自己选的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行,整体真实比读完全表要快得多。

验证:低选择性时,手动模拟index_merge反而比全表扫描慢66%

amount>50这个条件本身就能匹配掉约92%的行,此时MySQL优化器自己会算出全表扫描更便宜,直接放弃index_merge。用UNION手动模拟"两条索引分别扫描再去重"的效果,看这个被优化器放弃的方案到底有多慢。

实测 · 本机执行耗时(两种写法返回同样的45906行)
优化器自己选的方案(自动判定为全表扫描更划算):
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%
当amount>50这个条件本身就能匹配掉约92%的行时,"分别走索引再去重"这套思路本身就不划算了——两条索引扫描合起来读到的行数已经接近全表,索引范围扫描的随机I/O比全表扫描的顺序I/O更慢,而且UNION还要额外为这么多行做一次全量去重,真实的耗时(160.0ms)反而比老老实实做一次全表扫描(96.6ms)慢了66%。MySQL优化器在这个例子里真实自己就避开了这个坑:面对amount>50这种低选择性条件,它会直接放弃index_merge,选择全表扫描。

结论:index_merge贵在"去重"这一步,不贵在"扫两条索引"

高选择性(约7%)时index_merge比全表扫描快3.3倍,低选择性(约92%)时手动模拟的index_merge反而比全表扫描慢66%——同一种执行策略,是快是慢完全取决于两条分支各自匹配了多少行。索引本身的范围扫描通常很快,真正的成本大头是"把两条分支的结果按行ID去重合并"这一步:匹配的行越多,这一步要处理的数据量就越大,随着匹配行数逼近全表,index_merge的代价会持续上升,直到反超一次简单的全表扫描。这也是为什么MySQL优化器要基于代价模型做选择,而不是"能用索引就一定用索引"——下一篇会用OPTIMIZER_TRACE把这个"去重"步骤的真实代价数字扒出来看。

分步演示:高低选择性下index_merge的真实表现

每一步都是本文上面用EXPLAIN ANALYZE和真实计时跑出来的原始结果。

选择性决定了index_merge是赚还是亏
这一步新验证的场景

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB),5万行orders,customer_id和amount各有独立索引
    • optimizer_switch='index_merge=off' + 手动UNION:分别验证两种极端选择性下的真实耗时
    京ICP备2026067502号-1京公网安备11010502063487号