登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 索引合并为什么不如联合索引:OR 条件的执行计划与改写边界

来源:17golang原创

时间:2026-08-25 09:01:41 451浏览 收藏

订单筛选接口突然变慢时,SQL 里一个看似方便的 OR 往往比单列索引少走了一大段路。MySQL 可能选择 Index Merge,把两个索引扫描结果合并后再回表;如果回表行很多、还要排序,这条路径就可能不如一个贴合查询条件的联合索引。判断依据不是看到 “Using union” 就下结论,而是对比估算行数、实际扫描量和结果是否需要额外排序。

要点速览
  • Index Merge 是一种可用的访问策略,不是联合索引的通用替代品。
  • 两个单列索引分别找出大量候选行后,合并、去重和回表都要付成本。
  • 先用 EXPLAIN 看计划,再用 EXPLAIN ANALYZE 核对实际行数和耗时。
  • 改成联合索引或 UNION ALL 前,必须确认条件选择性、结果重复和排序语义。

日常写带OR条件的查询时,不少人会碰到MySQL选了索引合并策略,实际运行耗时却远超出预期,大部分场景下这种方案的综合开销比合理设计的联合索引高不少,我们结合实际执行流程把这类查询的判定边界讲清楚。MySQL OR 条件触发两个单列索引扫描并产生合并回表成本的订单查询示意图

先看清 Index Merge 到底合并了什么

假设订单表有客户、状态和创建时间三个常用过滤字段:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL,
  status VARCHAR(16) NOT NULL,
  created_at DATETIME NOT NULL,
  KEY idx_customer (customer_id),
  KEY idx_status (status)
);

查询想找出某个客户的订单,或者最近处于待支付状态的订单:

SELECT id, customer_id, status, created_at
FROM orders
WHERE customer_id = 128
   OR status = 'pending'
ORDER BY created_at DESC
LIMIT 50;

优化器可能分别扫描 idx_customeridx_status,把两边得到的行位置合并,再回到聚簇索引取完整记录。这个策略在两个条件都很有选择性时可能合适;一旦某个状态占了大半张表,第二条索引路径就会带来大量候选行。

为什么两个单列索引会输给一个联合索引

合并之前已经产生了两批候选行

Index Merge 的成本不只是一条额外的合并操作。每个索引都要先找到候选记录,之后还可能进行排序、去重和回表。查询只返回 50 行,并不代表数据库只读了 50 行;LIMIT 通常要等候选结果完成排序或筛选后才能生效。

MySQL Index Merge 与联合索引改写后的 EXPLAIN 和 EXPLAIN ANALYZE 对比路径

联合索引也不是把所有字段都堆进去

如果真实查询是“客户 + 时间范围”,更直接的索引通常是 (customer_id, created_at),而不是为了覆盖所有可能的 OR 条件盲目添加大索引:

ALTER TABLE orders
  ADD KEY idx_customer_time (customer_id, created_at);

联合索引遵循最左前缀规则。它能帮助客户条件先缩小范围,再按创建时间读取;但它并不会自动让 customer_id = 128 OR status = 'pending' 变成一条连续的索引范围。索引设计必须跟真实查询形状一起看。

用 EXPLAIN 找到真正的成本信号

先固定同一组参数,分别观察访问类型、使用的键和估算行数:

EXPLAIN FORMAT=TREE
SELECT id, customer_id, status, created_at
FROM orders
WHERE customer_id = 128 OR status = 'pending'
ORDER BY created_at DESC
LIMIT 50;

重点看 possible_keyskeyrowsExtra。看到 Index Merge 只是事实描述,不等于性能结论;还要确认合并后的候选量、是否出现额外排序,以及返回列是否需要频繁回表。

信号通常说明下一步
rows 很大至少一个 OR 分支选择性差检查值分布与条件是否可拆
Using filesort结果顺序不能直接由访问路径提供评估排序字段和索引顺序
Index Merge + 大量回表两条索引只筛出行位置检查是否适合覆盖或联合索引

然后在低风险环境执行:

EXPLAIN ANALYZE
SELECT id, customer_id, status, created_at
FROM orders
WHERE customer_id = 128 OR status = 'pending'
ORDER BY created_at DESC
LIMIT 50;

把实际读取行数和耗时记下来,再跟 EXPLAIN 的估算值对照。估算偏差很大时,先检查统计信息和数据分布,别急着把问题归结为索引类型。

三种改写方式,各自有边界

把同一业务条件改成联合索引

当实际需求是客户、时间和状态的固定组合时,优先把最常用的等值条件放在前面:

ALTER TABLE orders
  ADD KEY idx_customer_status_time (customer_id, status, created_at);

先用一组真实查询验证,不要只凭字段数量猜索引效果。联合索引会增加写入和存储成本,状态值高度重复时也不一定值得单独放在最前面。

用 UNION ALL 拆开两个互斥分支

当两个分支可以分别利用索引,并且业务能接受合并结果后再排序,可以考虑拆查询。若两边可能命中同一订单,不能直接把 OR 替换为 UNION ALL,否则会重复返回;改用 UNION 或增加互斥条件又会引入去重成本。

SELECT id, customer_id, status, created_at
FROM orders WHERE customer_id = 128
UNION ALL
SELECT id, customer_id, status, created_at
FROM orders WHERE status = 'pending' AND customer_id  128;

只在证据充分时调整优化器开关

为了验证假设,可以在测试环境临时比较关闭 Index Merge 前后的计划;但这不是生产修复。数据量和分布变化后,今天更快的访问策略可能变成明天的负担,最终还是应该回到查询形状、索引和统计信息上。

线上排查可以按这张清单走

  1. 保存原始 SQL、参数值范围和返回列,确保对比实验只改一个变量。
  2. 检查 EXPLAIN 中的 key、rows、Extra,确认是否为 Index Merge、是否排序和回表。
  3. 用两组选择性不同的数据执行 EXPLAIN ANALYZE,记录实际行数和耗时。
  4. 根据真实查询形状试验联合索引或互斥的 UNION ALL,验证结果不重复。
  5. 灰度观察慢查询和写入开销,确认索引收益没有转化成更新成本。

常见问题与回归边界

看到 Index Merge 就应该关闭它吗?

不应该。它在选择性好的条件下可能是合理方案,是否需要改写要看实际扫描行数、回表量和排序耗时。

联合索引一定比两个单列索引快吗?

不一定。联合索引只对匹配的查询形状有效,还会增加写入和存储成本。应使用代表性数据和 EXPLAIN ANALYZE 验证。

OR 改成 UNION ALL 会不会重复数据?

会有这个风险。两个分支命中同一行时必须增加互斥条件,或选择能去重的写法,并重新核对排序和分页语义。

rows 很大是不是一定说明索引失效?

不是。rows 是优化器估算的候选量,可能受统计信息和数据分布影响;实际情况要结合 EXPLAIN ANALYZE 判断。

把一次慢查询变成可回归的检查

索引合并与联合索引不是简单的二选一。先确认 OR 两侧各自的选择性,再用执行计划解释“读了什么”,最后用实际运行结果确认“真的花了多少”。只有查询结果、排序、重复数据和写入成本都通过回归,改写才算完成。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>