试被问到"索引失效的场景有哪些",你是不是也曾一口气背出"最左前缀、函数运算、左模糊、OR 条件......",然后看到面试官面无表情地点了点头?今天这篇博客,我们不背清单,从 B+ 树的结构出发,把每一条失效规则背后的"为什么"彻底搞明白。
一、先搞清楚:B+ 树凭什么能加速查询?
在聊失效之前,先回顾一下索引为什么快。
B+ 树索引之所以能加速查询,核心依赖两个特性:
- 有序性:叶子节点按键值从小到大排列,支持二分查找快速定位
- 前缀匹配:可以从一个确定的"起点"开始,沿着有序链表连续扫描
所有索引失效场景,本质上都是破坏了这两个特性中的某一个。 最终归结为三种破坏方式:
- 找不到起点:无法确定从 B+ 树的哪个节点开始查找
- 排序链断裂:后续列在全局无序,无法继续利用索引
- 值域被改写:原始值有序 ≠ 计算后有序,索引排序失效
带着这三个关键词,我们逐个拆解。
二、最左前缀原则:排序链断裂
联合索引 (a, b, c) 的 B+ 树排序规则是:先按 a 排序,a 相同再按 b 排序,b 也相同再按 c 排序。
WHERE a=1 AND b=2 -- 走索引
WHERE b=2 AND a=1 -- 走索引(优化器自动调整顺序)
WHERE b=2 AND c=1 -- 不走索引
WHERE c=1 AND a=2 -- 部分走索引
为什么 WHERE b=2 AND c=1 不走索引?
B+ 树的第一排序键是 a,跳过 a 直接查 b,b 的值在全局是分散的(a=1 时有 b=10、20,a=2 时也有 b=10、20),MySQL 无法确定"b=2 从哪个节点开始找",只能全索引扫描。
为什么 WHERE b=2 AND a=1 能走索引?
MySQL 优化器会自动把条件重排为 WHERE a=1 AND b=2,WHERE 子句中条件的书写顺序不影响索引使用。
为什么 WHERE c=1 AND a=2 只走部分?
a 能用,但 c 因为中间缺少 b,排序链断裂,无法继续利用索引。
本质:索引的排序是从左到右逐层生效的,左边断了,右边就废了。
三、索引列运算/函数:值域被改写
WHERE substring(phone, 10, 2) = '12' -- ❌ 索引失效
WHERE YEAR(create_time) = 2023 -- ❌ 索引失效
WHERE id + 1 = 1001 -- ❌ 索引失效
为什么索引会失效?
B+ 树存的是 phone 的原始值 ,按原始值的字典序排列。当你写 substring(phone, 10, 2) = '12' 时,MySQL 需要对每一行的 phone 值做截取运算,再判断结果是否等于 '12'。
问题在于:B+ 树是按原始 phone 值排序的,不是按截取后的结果排序的。原始值有序 ≠ 函数计算后有序。MySQL 无法从 B+ 树的根节点开始二分查找,只能逐行扫描。
修复方式:把运算移到等号右边。
-- 改写前(索引失效)
WHERE id + 1 = 1001
-- 改写后(索引生效)
WHERE id = 1000
本质:对索引列做运算,等于改写了值域,B+ 树的排序规则不再适用。
四、模糊查询:起点丢失
WHERE name LIKE '王%' -- ✅ 索引生效
WHERE name LIKE '%三' -- ❌ 索引失效
WHERE name LIKE '%王%三' -- ❌ 索引失效
为什么 LIKE '王%' 能用索引?
B+ 树的叶子节点按字典序排列,比如:张三 → 李四 → 王五 → 王六 → 赵七。前缀确定是"王",MySQL 可以直接在 B+ 树里定位到"王"开头的起点,然后顺着链表往后扫,遇到不以"王"开头的就停。起点确定,连续扫描,索引生效。
为什么 LIKE '%三' 索引失效?
前缀未知,"三"可能出现在任何名字的任意位置。MySQL 无法确定从哪个节点开始找,只能从头到尾遍历所有叶子节点。起点丢失,索引失效。
本质:左模糊导致前缀未知,B+ 树无法定位起始节点。
五、OR 条件:扫描成本反超
WHERE name = '张三' OR age = 18 -- age 无索引,索引失效
为什么索引会失效?
name = '张三' 可以通过 B+ 树快速定位,age = 18 因为没有索引只能全表扫描。OR 的语义是"满足任一条件即可",MySQL 必须把两个结果集合并 后去重。既然 age = 18 已经需要全表扫描了,那再单独走一次 name 索引反而多此一举------优化器会直接选择全表扫描,顺便把两个条件都检查了。
修复方式 :给 age 也加上索引,或者用 UNION 改写。
本质:OR 中混入非索引列,导致全表扫描的成本反超索引扫描,优化器放弃索引。
六、覆盖索引:不是失效,而是优化
覆盖索引不是"失效"场景,而是优化手段。它的原理是:
非聚簇索引的叶子节点存的是主键值,查询时需要回表去聚簇索引拿完整数据。但如果你的查询只需要索引里已经包含的字段,MySQL 就不需要回表了。
-- 假设建了联合索引 (user_id, amount)
SELECT user_id, amount FROM orders WHERE user_id = 200;
这条 SQL 只需要 user_id 和 amount,它们都在非聚簇索引的叶子节点里,所以不需要回表,直接从索引里返回结果。EXPLAIN 的 Extra 列会显示 Using index。
七、一张表总结
| 失效场景 | 破坏了 B+ 树的什么 | 本质 |
|---|---|---|
| 违反最左前缀 | 排序链断裂,后续列全局无序 | 找不到起点 |
| 索引列函数/运算 | 原始值有序 ≠ 计算后有序 | 值域被改写 |
| LIKE 左模糊 | 前缀未知,无法定位起始节点 | 找不到起点 |
| OR 含非索引列 | 需要全表扫描才能合并结果 | 扫描成本反超 |
| 隐式类型转换 | 等价于对索引列加了 CAST 函数 | 值域被改写 |
八、一句话总结
索引失效的本质就是破坏了 B+ 树的有序性------要么找不到起点,要么排序链断裂,要么值域被改写,导致 MySQL 无法利用索引的快速定位能力,只能退化为全表扫描。
理解了这一点,你就不需要死记硬背失效场景了。遇到任何 SQL,只要问自己一句:"这个条件还能不能利用 B+ 树的有序性快速定位?",答案自然就出来了。