OR 真的会导致索引失效吗?

一、前言

在 MySQL 的日常开发与调优中,"OR 会导致索引失效"几乎是一句人尽皆知的"铁律"。然而,如果你仔细观察就会发现,这条铁律并不总是成立------有时 OR 前后都建了索引,查询也确实走了索引;有时却又眼睁睁看着 type=ALL,全表扫描把性能拖垮。为什么会出现这种"双标"行为?OR 到底在什么情况下才会让索引失效?我们又该如何写出稳定又高效 SQL?本文将通过两条经典的 SQL 执行计划对比,配合原理剖析与实战方案,一次性把这些问题讲透。

二、从一个"反常识"的现象说起

请看下面这两条结构和数据相似,但执行结果截然相反的 SQL:

例1:OR 导致全表扫描(失效)

表结构假设:

  • zz_users

  • user_id 为主键索引

  • user_name 为联合索引 unite_index 的第一列

    EXPLAIN SELECT * FROM zz_users WHERE user_id = 1 OR user_name = "熊猫";

执行计划:

id select_type table type possible_keys key Extra
1 SIMPLE zz_users ALL PRIMARY,unite_index NULL Using where

可以看到 type=ALL,明明所有查询条件都包含索引字段,却根本没有用到索引,直接全表扫描。

例2:OR 成功走索引(生效)

表结构假设:

  • t_user

  • id 为主键索引

  • age 为普通索引 index_age

    EXPLAIN SELECT * FROM t_user WHERE id = 1 OR age = 18;

执行计划:

id select_type table type possible_keys key Extra
1 SIMPLE t_user index_merge PRIMARY,index_age PRIMARY,index_age Using union(PRIMARY,index_age)

type=index_merge,说明 MySQL 同时对 idage 两个索引进行了扫描,并将结果合并,最终避免了全表扫描。


同样是"OR 前后都有索引",为什么一个生效一个失效? 答案就藏在 MySQL 优化器的 索引合并(Index Merge) 机制当中。

三、先纠正一个流行误区:OR 不是必然导致索引失效

许多教程为了简单,直接抛出"OR 会让索引失效"的结论,其实是 不够准确 的。更准确的说法是:

  • 当 OR 连接的条件 在同一个索引上 时,完全可以走索引(如范围扫描)。
  • 当 OR 连接的条件 分属不同索引 时,MySQL 需要动用 索引合并(Index Merge) 优化才能生效;而优化器会根据代价判断是否真正启用它。
  • 只有当优化器认为索引合并的代价比全表扫描更高时,才会放弃索引,选择全表扫描。

换句话说:OR 不是"索引杀手",而是"索引合并"的触发器;触发成功则用索引,失败则回退全表扫描。

深入辨析:什么是"在同一个索引上" vs "分属不同索引"?

这两种情况直接决定了 OR 能否简单走索引,还是必须依靠复杂的索引合并。

1. OR 的条件在同一个索引上(最理想)

只要 OR 连接的所有列都属于同一棵索引树 ,并且查询条件符合该索引的最左前缀原则,MySQL 就可以只使用这一个索引搞定查询,不需要跨索引合并。

典型情形是利用联合索引。例如有一张用户表,建有联合索引:

复制代码
INDEX idx_name_age (user_name, age)

错误示例(看似同一索引,实则不然):

复制代码
SELECT * FROM users WHERE user_name = '熊猫' OR age = 18;

虽然 user_nameage 同属一个联合索引,但 age 跳过了最左前缀 user_name,这个 OR 实际上无法只用这个联合索引完成,还是会走索引合并或者全表扫描。这里务必留意:同属一个索引不等于一定能简单生效,必须满足最左匹配原则。如果要用上这个联合索引,OR 的每个分支都必须能用到索引的"前缀"。

正确的"在同一个索引上"例子:

复制代码
-- 联合索引 (user_name, age)
SELECT * FROM users WHERE user_name = '熊猫' OR user_name = '老虎';

这里两个 OR 条件都在索引的第一列 user_name 上,MySQL 可以将其转化为 user_name IN ('熊猫','老虎') 的等价形式,直接在联合索引上做范围扫描(type=range),完全不会失效。

再如:

复制代码
-- 联合索引 (a, b, c)
SELECT * FROM t WHERE a = 1 OR (a = 2 AND b = 5);

条件都从索引最左列 a 开始,也属于同一索引树的范围扫描。

所以,严格来说:OR 的所有分支都能各自利用同一个索引的最左前缀,就叫作"在同一个索引上"

2. OR 的条件分属不同索引(触发索引合并的场景)

当 OR 连接的两个列,分别**建在不同的索引(或不同索引树)**上时------比如一列是主键,另一列是单独的二级索引------MySQL 单走一个索引无法拿到另一个条件的数据,这时就必须用索引合并。

典型例子

复制代码
-- id 是主键索引(聚簇索引)
-- age 列单独建了普通索引 idx_age
SELECT * FROM t_user WHERE id = 1 OR age = 18;

这里 id 走主键索引,ageidx_age,它们属于两棵独立的索引树,这就是"分属不同索引"。

要完成这个查询,MySQL 需要:

  • 扫描主键索引拿到 id=1 的行ID集合;
  • 扫描 idx_age 拿到 age=18 的行ID集合;
  • 合并两个ID集合并去重;
  • 再拿着最终ID回表取完整数据。

这正是 索引合并(Index Merge) 的典型场景,执行计划会显示 type=index_mergeUsing union

再举个复合索引与主键的情况:

复制代码
-- 联合索引 idx_name_age (user_name, age)
-- id 是主键
SELECT * FROM users WHERE id = 100 OR user_name = '熊猫';

条件分属主键索引和联合索引,依然属于"分属不同索引",同样需要索引合并。

四、OR 索引失效的常见场景一览

在对"分属不同索引"的 OR 进行深入剖析之前,我们先系统地梳理一下:哪些情况下 OR 会导致索引直接失效(完全不触发索引合并)? 以下是几种经典的场景。

场景1:OR 的某一侧没建索引(最常见)

这是导致 OR 全表扫描的最普遍原因。只要 OR 连接的任一列没有索引,整个查询就只能全表扫描。因为 MySQL 无法只用一棵索引树找出所有满足条件的数据。

复制代码
-- 假设 id 是主键,但 age 列没有索引
SELECT * FROM t_user WHERE id = 1 OR age = 18;
-- 执行计划:type=ALL,直接全表扫描
场景2:OR 的某一侧对索引列使用了函数或计算

即使索引列都建了索引,只要某一侧对列做了操作(函数、运算、隐式类型转换),该侧就失效了,进而导致整个 OR 回退到全表扫描。

复制代码
-- id 是主键,create_time 也有索引
-- 但右侧用了函数,索引彻底失效
SELECT * FROM orders WHERE id = 1 OR YEAR(create_time) = 2025;
-- 执行计划:type=ALL
场景3:隐式类型转换导致失效

在 MySQL 中,当字符串类型的列与数字比较时,会发生隐式类型转换,从而导致索引失效。这一点在 OR 中同样适用。

复制代码
-- phone 列是 varchar 类型,建了索引,但传入了数字
SELECT * FROM users WHERE id = 1 OR phone = 13800138000;
-- phone 索引因隐式类型转换而失效,type=ALL
场景4:OR 在联合索引中跳过了最左列

即便 OR 两端的列都存在于同一个联合索引中,但若其中一侧没有以最左列开头,索引同样无法使用。

复制代码
-- 联合索引 (a, b)
SELECT * FROM t WHERE a = 1 OR b = 2;
-- b 跳过了 a,不符合最左前缀;尽管 b 在索引中,但无法使用该索引
场景5:OR 包含范围查询且结果集过大

即便 OR 两端都建了索引,但如果某侧是范围条件(如 >LIKE '%xx%'),优化器可能直接判定"走索引不如全表扫描",主动放弃索引合并。

复制代码
SELECT * FROM t_user WHERE id = 1 OR age > 10;
-- 若 age > 10 匹配数据占比很高,索引合并可能被放弃

**一句话总结:**只要 OR 有任何一侧无法独立走索引(无索引/函数/类型转换/跳过最左),整个查询的索引就会全部失效,直接全表扫描。

五、核心机制:跨索引的 OR 与索引合并(Index Merge)

当 OR 连接的两列分别属于不同的索引时,单靠一棵索引树无法拿到所有满足条件的数据,MySQL 必须走两条路径:

  1. 分别走每个索引,拿到符合条件的主键 ID 集合;
  2. 将多个 ID 集合做 UNION(取并集),去重;
  3. 拿着最终的 ID 集合,再回聚簇索引获取完整的行记录。

这个过程就是 索引合并 (Index Merge)。在 EXPLAIN 输出中,我们会看到:

  • type = index_merge
  • Extra 字段显示 Using union(索引1,索引2)

它本质上是一种"用多个单列索引模拟复合条件查询"的优化策略。索引合并生效的条件包括但不限于:

  • 查询条件使用 OR 连接,且每个条件都有对应的索引;
  • 条件中等值、范围均可(不同合并算法支持不同操作);
  • 优化器估算出「两次索引查找 + 合并 + 回表」的代价低于全表扫描。

六、为什么"OR 前后都有索引"还会失效?------优化器的算盘

在分析失效的具体原因之前,我们先理解执行计划中一个至关重要的字段------rows

rows 是 MySQL 优化器估算的"为了执行查询,需要检查的行数",它是一个基于统计信息的预测值,不是实际扫描的行数。它的核心特点包括:

  • 估算依据 :InnoDB 根据索引基数(Cardinality)、表大小、条件过滤性等统计信息计算得出。如果统计信息不准确(如未及时更新),rows 可能偏差很大。
  • 代价计算的基础 :优化器主要根据 rows 评估不同执行计划的 I/O 开销,选择预估扫描行数最小的路径。
  • 不是实际值 :实际执行时可能扫描更少(如遇到 LIMIT 提前终止)或更多(如统计信息过时)。

在我们的例子中:

  • 例1 执行计划中 rows=3,表示优化器估计全表只有 3 行数据,扫描全表成本极低,因此直接放弃了索引合并,选择 ALL 全表扫描。这就是小表下 OR 即使有索引也常失效的直接证据。
  • 例2 中 rows=9, filtered=20.99,表示估计需要扫描 9 行数据,经过条件过滤后结果集约占总扫描行数的 20.99%,即约 1.89 行,优化器据此判断索引合并更划算。

rows 是优化器决策的核心依据之一,能帮助我们快速判断 SQL 是否存在统计信息不准导致的执行计划走偏。


现在我们可以明确回答文章开头例1失效的根本原因了:优化器经过代价估算,认为走索引合并不如直接全表扫描划算。 具体来说,以下场景最容易触发这种"放弃"行为:

1. 表数据量极小

例1 的 rows=3 说明整张表只有 3 行数据。此时全表扫描的成本几乎为零;而索引合并需要两次索引查找、结果合并、再回表,流程反而更重。优化器很聪明,直接选择了成本更低的全表扫描。这是 延迟物化索引深度 带来的天然代价。

2. 某一个 OR 条件的过滤性极差

如果 user_name="熊猫" 匹配了表中绝大多数行(比如 80% 的数据),那么走 user_name 索引后,拿到的 ID 集合非常庞大,和主键索引的结果合并、去重、回表这一系列操作的代价会急剧膨胀。此时直接全表扫描,顺序读效率远高于大量随机回表,优化器自然会选择全表扫描。

3. MySQL 版本与优化器限制

早期 MySQL 对索引合并的支持并不完善,部分场景下不会主动选择;某些版本对 OR 条件的索引合并采用更保守的成本模型,也容易导致"能合却不合"的情况。

4. 条件类型不兼容

例如一条是等值,另一条是范围,且范围查询的过滤性难以精确估算,导致合并后的结果集大小被高估,进而被优化器判定为不如全表扫描。


因此,OR 前后都有索引只是索引合并能够发生的"必要条件",而非"充分条件"。 最终决策权在优化器的代价估算模型手中。

七、实战中最稳妥的方案:用 UNION ALL 替代 OR

依赖优化器的"索引合并"决策终究有不确定性,生产环境数据分布和版本差异都可能让同一条 SQL 在不同的时间选择不同的执行计划。因此,最佳实践是 使用 UNION ALL 手动拆分查询,从根本上避免对索引合并的依赖。

改写示例
  • 原始 SQL(可能走索引合并,也可能全表扫描):

    SELECT * FROM t_user WHERE id = 1 OR age = 18;

  • 改写为 UNION ALL 的写法:

    SELECT * FROM t_user WHERE id = 1
    UNION ALL
    SELECT * FROM t_user WHERE age = 18;

为什么 UNION ALL 更优?
  1. 每个子查询都能独立、确定地走自己的索引,优化器不用再纠结是否启用索引合并。
  2. 如果两边的结果集不重复UNION ALL 不去重,性能更高(如果担心重复,可改用 UNION,但会增加排序去重开销)。
  3. 执行计划清晰可控,不会因为数据量、统计信息的变化而发生执行路径突变。

在绝大多数"单表多列 OR"查询场景下,UNION ALL 都提供了更稳定、更高效的性能表现。这也是诸多 MySQL 优化规范中强烈推荐的做法。

八、补充:什么情况可以放心使用 OR?

既然 UNION ALL 这么稳,是不是所有的 OR 都要改写?其实也不尽然。以下场景 OR 本身就是高效的,无需改写:

  1. OR 的列属于同一个索引且满足最左前缀
    比如联合索引 idx(user_id, user_name),查询 WHERE user_id = 1 OR user_id = 2。此时 MySQL 可以直接在该索引上执行范围扫描,不会触发索引合并,也不会失效。
  2. 表行数极小
    几十行的小表,全表扫描本身已是最优解,改写为 UNION ALL 反而增加复杂度,没有实际收益。
  3. 索引合并明确生效且经过基准测试确认更优
    某些特殊场景下(如两个条件的选择度都极高),索引合并表现可能略优于 UNION ALL(减少了一次查询解析开销),但这种情况较少,且不稳定,不建议作为首选。

九、总结

  • OR 并非天然导致索引失效,只有跨索引的 OR 且未能成功触发「索引合并」时,才会回退全表扫描。
  • OR 导致索引失效的常见情况:某一侧无索引、对索引列使用函数或计算、隐式类型转换、联合索引中跳过最左列、范围查询结果集过大等。
  • "在同一个索引上" 指 OR 的所有分支都能各自利用同一索引的最左前缀;"分属不同索引" 则是指条件列建在不同的索引树上,此时必须依靠索引合并。
  • rows 字段是优化器估算的扫描行数,是其代价计算的核心依据,也是解读执行计划时不可忽视的指标。
  • 索引合并能否生效取决于 代价估算:数据量极小、条件过滤性差、版本限制等都可能导致其"未启用"。
  • 开发过程中的最佳实践是:UNION ALL 代替跨不同索引的 OR,让每个条件各自稳定走索引,避免依赖优化器的"黑盒"决策。
相关推荐
隐层漫游者13 小时前
SQL核心技能全景图:DDL数据定义、DML安全操作、DQL高级查询、多表JOIN与窗口函数实战
mysql
雨辰AI13 小时前
人大金仓慢 SQL 根治方法论:问题定位 - 分析 - 优化全流程
数据库·后端·sql·mysql·政务
LCG元13 小时前
MySQL慢查询分析与索引调优:从故障诊断到性能翻倍的进阶之路
android·前端·mysql
问心无愧051314 小时前
ctf show web 入门173
数据库·笔记·sql·mysql
sukioe14 小时前
深入理解 MySQL 索引:底层数据结构与 B+ 树设计原理
数据结构·mysql·oracle
承渊政道16 小时前
【MySQL数据库学习】(MySQL数据库基础)
数据库·学习·mysql·ubuntu·bash·数据库架构·数据库系统
Java成神之路-16 小时前
为什么 DDL 无法回滚?
mysql
·醉挽清风·16 小时前
学习笔记—MySQL—索引
笔记·学习·mysql
AI人工智能+电脑小能手16 小时前
【大白话说Java面试题 第74题】【Mysql篇】第4题:InnoDB 和 MyISAM 的数据文件存储区别?
java·开发语言·mysql·面试