凌晨两点,报警群里的钉钉机器人突然“发疯”,提示数据库 CPU 飙升到 99%。
作为运维负责人,我揉着惺忪的睡眼打开监控,发现一条看起来“人畜无害”的查询 SQL 堵塞了整个线程池。更要命的是,开发兄弟在旁边一脸委屈地发誓:“哥,这个字段我明明加了索引的,本地测都很快,怎么上线就炸了?”
这种场景,不知道你是不是也似曾相识?
很多时候,我们以为加了索引就是给数据库穿上了“加速鞋”,殊不知在某些特定写法的“骚操作”下,索引会直接失效,让查询变成全表扫描(Full Table Scan)的“慢动作重播”。
今天咱们不聊晦涩的 B+树原理,就聊聊我这几年实实在在踩过的坑。我整理了 10个最常见的索引失效场景,特别是前三个,简直是“性能杀手”。
一、 “隐形”的类型转换:最冤枉的失效
这是新手最容易犯,也是老手偶尔会翻车的坑。
真实复盘:
大概两年前,我们接手了一个电商大促活动。系统里有一个根据手机号查询用户订单的接口。表结构里 phone 字段定义的是 VARCHAR(20),并且建了索引。
结果大促开始没多久,数据库就开始告警。DBA 一看慢日志,抓到了这条 SQL:
SELECT * FROM user_orders WHERE phone = 13800138000;
看着没毛病?甚至觉得很标准?
坑在这里: 注意那个手机号,它没加单引号。
在 MySQL 眼里,phone 是字符串,而输入的 13800138000 是数字。MySQL 的优化器虽然聪明,它会试着帮你把字符串转成数字来进行比较。
但是,这种隐式类型转换一旦发生,索引就彻底废了。这就好比你书架上的书是按拼音排序的,结果你非要按书的重量去找书,那目录(索引)自然就没用了,只能一本本称重(全表扫描)。
避坑指南: 写代码时,参数类型必须和数据库字段类型严格一致。如果是字符串,千万别省那对单引号。
二、 在索引列上“动刀子”:计算与函数
我以前有个习惯,写 SQL 喜欢怎么顺手怎么来,觉得数据库既然提供了函数,不用白不用。直到有一次,我把一张千万级流水表的查询搞崩了。
真实场景: 我们要统计“2023年所有订单”。我当时是这么写的:
SELECT count(*) FROM orders WHERE YEAR(create_time) = 2023;
create_time 字段明明有索引,但这条 SQL 跑了十几秒才出结果。
原因分析:
我们在索引列 create_time 上套了一个 YEAR() 函数。
这就像是你去查字典,字典是按 a, b, c... 排序的,但你现在的要求是:“帮我找所有倒数第三个字母是 k 的单词”。字典的目录结构对这种需求完全无能为力。
只要在索引列上做计算、使用函数(如 left, substr, to_days 等),索引大概率会失效。
落地建议: 把计算放到“等号右边”,或者在应用层算好再传给数据库。上面那个例子,改成范围查询瞬间起飞:
SELECT count(*) FROM orders
WHERE create_time BETWEEN '2023-01-01 00:00:00' AND '2023-12-31 23:59:59';
三、 模糊查询的“左撇子”陷阱
很多业务场景需要做搜索,比如“查找名字里包含‘小’的用户”。
如果你写成:
SELECT * FROM users WHERE name LIKE '%小%';
或者
SELECT * FROM users WHERE name LIKE '%小';
恭喜你,索引又失效了。
原理大白话: MySQL 的 B+树索引是“最左前缀”匹配。想象你在查电话簿,如果你知道姓“张”,你可以很快定位到;但如果你只知道名字里带个“伟”字,或者名字以“伟”结尾,你是没法利用姓氏排序来查找的,只能从头翻到尾。
踩坑经验:
只有 LIKE '小%'(通配符在最右边)才能用到索引。
怎么办?
如果业务非要两边都模糊匹配(%...%)怎么办?
- 覆盖索引:如果你的查询字段都在索引里(比如只查
id),那虽然不能快速定位,但至少不用回表,速度能快点。 - 别难为 MySQL:这种全文检索的需求,建议出门左转找 Elasticsearch (ES)。术业有专攻,别拿水果刀砍柴。
四、 还有哪些坑?索引失效的“全家桶”
除了上面三个“大坑”,我把剩下的常见场景整理了一份清单。这张清单我至今贴在工位的隔板上,每次 Code Review 都会扫一眼:
- 不等于的坑:使用
!=或者<>时,MySQL 经常会觉得“查大部分数据还不如全表扫描快”,从而放弃索引。 - IS NULL / IS NOT NULL:这个看运气(数据分布)。如果数据库里大部分数据都是 NULL,你查
IS NOT NULL就会走全表扫描,反之亦然。 - OR 的锅:
WHERE id = 1 OR age = 18。如果id有索引但age没有,整个查询就会变成全表扫描。解决办法是给age也加索引,或者用UNION ALL代替OR。 - 联合索引没用对:建了联合索引
(a, b, c),结果查询只用了WHERE b = 1。这违背了“最左前缀法则”,就像你还没上楼梯的第一阶,就想直接踩第二阶,肯定不行。 - 字符串不加引号:虽然前面说了,但还是要强调,这是低级错误之王。
- 全表扫描更快:这不是 bug。如果表里一共就 10 行数据,MySQL 觉得直接读比去翻索引再回表还要快,它就会弃用索引。这属于“正常失效”。
- 字符集不统一:两个表做关联查询(JOIN),如果一个表是
utf8,另一个是utf8mb4,索引也会失效!这个问题极隐蔽,排查起来能要把人逼疯。
五、 写给你的落地锦囊
说了这么多坑,最后给你一套我一直在用的排查与优化模板。下次遇到慢查询,别慌,按这个步骤来。
1. 必杀技:EXPLAIN
遇到慢 SQL,第一反应不应该是改代码,而是加个 EXPLAIN 看看执行计划。
复制这个模板去跑一下:
EXPLAIN SELECT * FROM your_table WHERE ...;
重点看这三列:
- type:如果是
ALL,说明全表扫描了,准备挨打吧;如果是ref或range,说明还凑合。 - key:显示实际用到的索引。如果是
NULL,说明没用到。 - key_len:通过这个长度可以推算出联合索引到底用到了哪几列。
2. 开发自查清单(建议加入代码走查规范)
-
WHERE后面的字段,类型是否匹配?(特别是数字vs字符串) - 是否对索引字段进行了
+ - * /或函数操作? -
LIKE语句是否把%放在了最前面? - 联合索引是否遵守了“最左前缀”原则?
-
OR两边的字段是否都有索引?
3. 行动建议
如果你的项目里已经有不少慢查询了,不要试图一次性全部优化完。 建议做法: 每周五下午抽出半小时,拉出线上的 Slow Query Log(慢查询日志),按“执行频率 * 平均耗时”排序,每次只优化 Top 3。
相信我,坚持两个月,你的数据库负载能降一半,报警群也能清净不少。
小互动:你在生产环境遇到过最离谱的索引失效是什么情况?欢迎在评论区吐个槽,让我们避避坑!