or连接不同字段时,mysql优化器因无法高效合并分散的索引区间而常放弃索引、触发全表扫描;仅当各分支均能独立命中索引且无聚合/group by时,union all拆分才安全有效。

直接结论:当 OR 连接的是不同字段(如 status = 'paid' OR level > 10),且各字段有独立索引时,用 UNION ALL 拆分几乎总能提升性能;但若涉及聚合、GROUP BY、或分支高度重叠,强行替换反而出错或更慢。
为什么OR常让索引失效
MySQL 优化器面对 WHERE a = 1 OR b = 2 时,无法保证两个条件在 B+ 树中落在连续区间——a 的匹配行可能散落在索引不同页,b 的同理。合并这些碎片的成本太高,优化器干脆放弃索引,走 type: ALL。这不是 bug,是成本权衡结果。
常见触发点包括:
-
EXPLAIN显示key为空或只用上一个索引,rows预估远高于单个分支 - 其中一边含隐式转换(如字符串字段查数字)、
IS NULL、LIKE '%abc' - MySQL 5.7 及更早版本根本不会尝试
index_merge;8.0 虽默认开启,但仅对 AND 下的交集友好,OR 仍靠不住
UNION ALL 替代的实操边界
不是所有 OR 都适合拆。只有满足以下全部条件,才值得动手:
- 每个
OR分支都能独立命中某个索引(用EXPLAIN单独验证SELECT * FROM t WHERE a = 1和SELECT * FROM t WHERE b = 2) - 分支数量 ≤ 5~10 个;再多会导致执行计划膨胀,优化器可能拒用索引
- 业务允许重复(用
UNION ALL),或你能接受在外层加DISTINCT的开销 - 原始查询不含
GROUP BY、HAVING、聚合函数(如COUNT/SUM)——这类不能直接拆,语义会错
例如:WHERE (htid = 'x1' AND raid = 'y1') OR (htid = 'x2' AND raid = 'y2'),若 (htid, raid) 有联合索引,优化器大概率走 range,此时改 UNION ALL 反而画蛇添足。
拆分时必须注意的细节
写错一个字符就报错或结果异常:
- 每个子查询的
SELECT字段顺序、数量、类型、NULL 属性必须完全一致,否则报ERROR 1222 (21000) - 公共条件(如
AND deleted = 0)要复制到每个子查询里,不能只写在最外层 - 如果原查询带
LIMIT,必须放在外层;若需取 top N,得先在每个子查询里加LIMIT N,再在外层统一LIMIT N,否则可能漏数据 - 不要用
UNION——去重会触发临时表 + 排序,大数据量下比原OR还慢 - 若分支间有重叠(如
status = 'paid'和created_at > '2024-01-01'),加AND status != 'paid'是常见去重手段,但需确认业务逻辑是否允许漏掉这部分交集
聚合查询千万别硬拆
SELECT COUNT(*) FROM t WHERE a = 1 OR b = 2 不能写成:
SELECT COUNT(*) FROM t WHERE a = 1 UNION ALL SELECT COUNT(*) FROM t WHERE b = 2
这返回的是两个计数之和,不是“满足任一条件的总行数”。正确做法是用派生表:
SELECT COUNT(*) FROM ( SELECT id FROM t WHERE a = 1 UNION SELECT id FROM t WHERE b = 2 ) AS tmp
但这时已引入去重排序,性能可能不如原 OR。真要优化聚合,优先考虑覆盖索引(如 INDEX(a, b))或检查是否真需要 OR——很多场景其实是业务逻辑误写,比如本该用 IN 或拆成两次查询。
最容易被忽略的一点:改完之后必须用 EXPLAIN FORMAT=TREE(MySQL 8.0+)或 EXPLAIN 对每个子查询单独看,确认 type 是 ref/range,且 key 列出对应索引名。光语法对,不代表真走索引。











