加了索引还慢?揭秘10个索引失效现场,别再背锅了!

凌晨两点,报警群里的钉钉机器人突然“发疯”,提示数据库 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 '小%'(通配符在最右边)才能用到索引。

怎么办? 如果业务非要两边都模糊匹配(%...%)怎么办?

  1. 覆盖索引:如果你的查询字段都在索引里(比如只查 id),那虽然不能快速定位,但至少不用回表,速度能快点。
  2. 别难为 MySQL:这种全文检索的需求,建议出门左转找 Elasticsearch (ES)。术业有专攻,别拿水果刀砍柴。

四、 还有哪些坑?索引失效的“全家桶”

除了上面三个“大坑”,我把剩下的常见场景整理了一份清单。这张清单我至今贴在工位的隔板上,每次 Code Review 都会扫一眼:

  1. 不等于的坑:使用 != 或者 <> 时,MySQL 经常会觉得“查大部分数据还不如全表扫描快”,从而放弃索引。
  2. IS NULL / IS NOT NULL:这个看运气(数据分布)。如果数据库里大部分数据都是 NULL,你查 IS NOT NULL 就会走全表扫描,反之亦然。
  3. OR 的锅WHERE id = 1 OR age = 18。如果 id 有索引但 age 没有,整个查询就会变成全表扫描。解决办法是给 age 也加索引,或者用 UNION ALL 代替 OR
  4. 联合索引没用对:建了联合索引 (a, b, c),结果查询只用了 WHERE b = 1。这违背了“最左前缀法则”,就像你还没上楼梯的第一阶,就想直接踩第二阶,肯定不行。
  5. 字符串不加引号:虽然前面说了,但还是要强调,这是低级错误之王。
  6. 全表扫描更快:这不是 bug。如果表里一共就 10 行数据,MySQL 觉得直接读比去翻索引再回表还要快,它就会弃用索引。这属于“正常失效”。
  7. 字符集不统一:两个表做关联查询(JOIN),如果一个表是 utf8,另一个是 utf8mb4,索引也会失效!这个问题极隐蔽,排查起来能要把人逼疯。

配图

五、 写给你的落地锦囊

说了这么多坑,最后给你一套我一直在用的排查与优化模板。下次遇到慢查询,别慌,按这个步骤来。

1. 必杀技:EXPLAIN

遇到慢 SQL,第一反应不应该是改代码,而是加个 EXPLAIN 看看执行计划。

复制这个模板去跑一下:

EXPLAIN SELECT * FROM your_table WHERE ...;

重点看这三列:

  • type:如果是 ALL,说明全表扫描了,准备挨打吧;如果是 refrange,说明还凑合。
  • key:显示实际用到的索引。如果是 NULL,说明没用到。
  • key_len:通过这个长度可以推算出联合索引到底用到了哪几列。

2. 开发自查清单(建议加入代码走查规范)

  • WHERE 后面的字段,类型是否匹配?(特别是数字vs字符串)
  • 是否对索引字段进行了 + - * / 或函数操作?
  • LIKE 语句是否把 % 放在了最前面?
  • 联合索引是否遵守了“最左前缀”原则?
  • OR 两边的字段是否都有索引?

3. 行动建议

如果你的项目里已经有不少慢查询了,不要试图一次性全部优化完。 建议做法: 每周五下午抽出半小时,拉出线上的 Slow Query Log(慢查询日志),按“执行频率 * 平均耗时”排序,每次只优化 Top 3。

相信我,坚持两个月,你的数据库负载能降一半,报警群也能清净不少。

小互动:你在生产环境遇到过最离谱的索引失效是什么情况?欢迎在评论区吐个槽,让我们避避坑!