第4章 索引失效场景:隐式转换、函数运算、不等号
索引是个好东西,但碰上某些写法就跟撞了邪似的——优化器死活不走索引,全表扫描就来了。我见过不少项目,明明索引建得漂漂亮亮,一条慢查询就把整个库拖垮了。问题出在哪?多数时候就是这三个字:“用错了”。
这一章我带你扫一遍最常见的索引失效场景。你想想看,SQL写起来顺手,可背后优化器根本不吃你那套。我们把坑一个个填上。
4.1 隐式类型转换——无声的杀手
先看个常见写法:
-- 假设 id 是 VARCHAR(32),有索引
SELECT * FROM users WHERE id = 10086;
嗯,注意了。id 是字符串,条件写了个数字。MySQL 拿到 10086 会怎么做?它会尝试把 id 列转换成数字再比较。这一转,索引列上就偷偷加了一个类型转换函数。优化器觉得这列已经不“干净”了,放弃索引。
我曾经接过一个线上紧急工单,某核心表查询耗时从 5ms 飙到 5s。查了半天,结果就是表里手机号字段是 char(11),业务代码传了个整型参数。那次之后我规定所有字符串字段条件必须显式加引号——谁不加谁背锅。
避坑口诀:字符串用引号,数字不用管,但字段类型你心里得有数。
4.2 函数运算包裹索引列
这个更隐蔽。很多开发喜欢在 SQL 里对日期字段做函数处理:
-- 不走索引(函数包裹在索引列上)
SELECT * FROM orders WHERE DATE(order_time) = '2025-04-01';
-- 走索引(等值比较,不破坏索引列)
SELECT * FROM orders WHERE order_time >= '2025-04-01 00:00:00' AND order_time < '2025-04-02 00:00:00';
第一种写法,DATE() 函数把 order_time 包裹住了,索引列的值被“加工”过,B+ Tree 没法按原始顺序二分查找。第二种写法用范围查询代替函数,索引照用不误。
我个人习惯:只要条件里有一列出现在函数参数里,就立刻警惕。能改写成范围查询就改,实在不行加个生成列再做索引——MySQL 5.7+ 支持,挺实用。
4.3 不等号、OR 与 NOT IN
先说不等号。你写个 WHERE status != 'closed',优化器一看:好家伙,除了 'closed' 全都要。如果表里绝大部分数据都不是 'closed',那走索引还不如全表扫来得快。不走。
再说 OR:
-- 假设 a 和 b 都有独立索引
SELECT * FROM t WHERE a = 1 OR b = 2;
早期 MySQL 对 OR 的处理很蠢——要分别走两个索引再合并,代价不小。现在版本有索引合并优化,但并非所有情况都触发。更稳妥的做法:用 UNION 拆成两条。
最后提一下 NOT IN 和 NOT EXISTS:
| 写法 | 索引使用 | 说明 |
|---|---|---|
col NOT IN (1,2,3) |
通常不走索引 | 优化器认为结果集太大,直接扫表 |
col NOT EXISTS (子查询) |
子查询可能用索引 | 看关联列是否有索引,NestLoop 可能走 |
col IS NULL / col IS NOT NULL |
可能走索引 | MySQL 对 IS NULL 有特殊优化,但 IS NOT NULL 经常不走 |
我的经验:不等号、NOT IN、OR 这三个团伙,十次里有八次是索引失效的元凶。写代码时多用 BETWEEN、IN(列表短时)、UNION 替代。
4.4 其他“坑”——LIKE 模糊匹配与联合索引
LIKE 以 % 开头,比如 WHERE name LIKE '%张',索引前缀无法确定,自然不走。要搜后缀怎么办?生成一个反转列然后建索引,或者上搜索引擎。
联合索引最左前缀原则,这个老生常谈了。我补充一个实际例子:
-- 联合索引 (a, b, c)
SELECT * FROM t WHERE b = 1 AND c = 2; -- 不走(跳过了 a)
SELECT * FROM t WHERE a = 1 AND c = 2; -- 只用到 a,c 的过滤走不了索引
嗯,这里要注意:不是建了联合索引就万事大吉,列顺序决定了你的查询能不能用好它。我建议每次建联合索引前,拿实际业务查询去 EXPLAIN 一把。
4.5 一张图看懂索引失效决策路径
下面这张 SVG 图把核心判断逻辑画出来了,你自己对照着分析 SQL 就能快速定位问题:
4.6 实战避坑清单
我把上面提到的全部整理成一个速查表,你写 SQL 时可以对着看一眼:
| 场景 | 错误示例 | 正确做法 |
|---|---|---|
| 隐式转换 | WHERE str_col = 123 |
WHERE str_col = '123' |
| 函数包裹 | WHERE DATE(dt)=... |
WHERE dt >= ... AND dt < ... |
| 不等号 | WHERE status != 1 |
重构业务逻辑,或使用 NOT EXISTS |
| OR 连接 | WHERE a=1 OR b=2 |
拆成 UNION ALL 两条查询 |
| LIKE 左匹配 | WHERE name LIKE '%abc' |
改用反转列索引或全文索引 |
| 联合索引跳跃 | WHERE b=1 AND c=2(索引 (a,b,c)) |
调整列顺序,或增加新索引 |
最后提醒一句:别信直觉,信 EXPLAIN。我在每个测试环境都习惯先跑一遍 EXPLAIN FORMAT=JSON,看 key 是不是 NULL,rows 是不是太大。索引用没用,一眼便知。
这一章的内容就这些。记住核心原则:别在索引列上做手术,别让条件变得“模糊”,别让优化器看不起你的查询。下个章节,我会聊聊反向扫描与索引跳跃扫描的实战技巧,但那是后话了。