MySQL中OR查询常导致索引失效,因其多分支对应B+树中不连续片段,合并成本高;可用UNION ALL拆解并配合覆盖索引优化,但需规避全量扫描、小表或复合索引已适配等无效场景。

MySQL 中 OR 查询为什么常导致索引失效
当 WHERE 子句中出现多个 OR 条件(尤其是跨字段或涉及函数/表达式时),MySQL 优化器往往放弃使用索引,转为全表扫描。根本原因是:B+ 树索引天然适合范围查找或单值等值查找,而 OR 的多个分支可能对应索引中不连续的片段,合并成本高,优化器判定走索引反而更慢。
典型触发场景包括:WHERE a = 1 OR b = 2(a、b 分别有单列索引但无联合索引)、WHERE status = 'active' OR created_at > '2024-01-01'(类型/范围混合)、WHERE id IN (1,2) OR name LIKE 'abc%'(等值 + 模糊匹配)。
注意:如果所有 OR 条件都作用于同一字段且是等值查询(如 WHERE id = 1 OR id = 2 OR id = 3),MySQL 通常会自动优化为 IN 并走索引——这种不用改。
用 UNION 替代 OR 的实操要点
将一个含多条件 OR 的查询,拆成多个独立子查询,用 UNION ALL(非 UNION)拼接,每个子查询只保留一个可走索引的条件。关键不是“语法等价”,而是让每个分支都能命中对应字段的索引。
-
UNION ALL比UNION快得多,因为它不查重;业务上若能确认结果无交集(如不同状态、不同时间分区),优先用UNION ALL - 每个子查询必须包含完全相同的列名和顺序,否则报错
ERROR 1222 (21000): The used SELECT statements have a different number of columns - 记得给每个子查询单独加
ORDER BY和LIMIT(如果需要分页),因为UNION整体的LIMIT只作用于最终结果,无法下推到各分支 - 如果原查询有
GROUP BY或HAVING,不能直接拆;需先在子查询中聚合,再在外层UNION ALL后二次聚合(代价更高,慎用)
示例对比:
-- 原写法(可能全表扫描) SELECT id, name FROM users WHERE status = 'paid' OR level > 10; <p>-- 改写后(假设 status 和 level 各有索引) SELECT id, name FROM users WHERE status = 'paid' UNION ALL SELECT id, name FROM users WHERE level > 10 AND status != 'paid';
第二条里加了 AND status != 'paid' 是为了去重逻辑(避免重复行),若业务允许少量重复,可省略——这时用 UNION ALL 更快。
什么情况下 UNION 也不管用?
不是所有 OR 都适合用 UNION 拆解。以下情况改了也白改,甚至更慢:
- 所有
OR条件都落在同一个复合索引的最左前缀上(如WHERE (a=1 AND b=2) OR (a=1 AND b=3),而索引是(a,b)),此时 MySQL 可能走索引的 range 类型扫描,比UNION更优 - 其中一个子查询返回大量数据(比如
WHERE created_at > '2020-01-01'),导致UNION ALL后内存排序或临时表膨胀,I/O 反而上升 - 表本身很小(
- 用了
SELECT *且表有很多大字段(如TEXT),每次子查询都要回表取全字段,IO 放大效应明显
判断是否真有效,唯一办法是看 EXPLAIN 输出:重点对比 type(是否从 ALL 变成 ref/range)、rows(预估扫描行数是否显著下降)、Extra(是否出现 Using temporary 或 Using filesort)。
更彻底的替代方案:覆盖索引 + UNION
如果查询只涉及少数几个字段,且这些字段都能被索引“覆盖”,就能避免回表,把 UNION 的性能拉到最高。例如:
原查询:SELECT id, name, email FROM users WHERE status = 'active' OR score >= 90;
建两个覆盖索引:
ALTER TABLE users ADD INDEX idx_status_id_name_email (status, id, name, email); ALTER TABLE users ADD INDEX idx_score_id_name_email (score, id, name, email);
再改写为:
SELECT id, name, email FROM users WHERE status = 'active' UNION ALL SELECT id, name, email FROM users WHERE score >= 90 AND status != 'active';
此时两个子查询都只需访问索引 B+ 树,不回主键聚簇索引,速度提升明显。但索引越多,写入开销越大,需权衡读写比例。
真正难处理的是那些既没法建覆盖索引、又必须多字段 OR、还要求低延迟的查询——这时候得考虑应用层分治,或者引入 Elasticsearch 这类专为复杂条件设计的引擎。


















