一文系统梳理MySQL数据库索引失效的七个场景
明明建了索引,为什么还是慢
在开发环境跑一条查询,发现是毫秒级返回。上线之后,同样的查询,用户反馈页面转圈要好几秒。你打开 EXPLAIN 一看,type: ALL,全表扫描。明明在 create_time 上建了索引,为什么没有用上?
这类问题的根源,往往不是索引坏了,而是写法触发了某种"索引失效"的条件。MySQL 的优化器在选择执行计划时,会判断当前查询能否利用索引的有序性。如果不能,它宁可走全表扫描,也不会用一个没有用的索引。
这篇文章将把常见的失效场景做一次系统性的梳理,每种都给出原因和改法,让你在遇到慢查询时能快速定位问题。
索引失效的本质
在逐个看场景之前,先搞清楚一件事:索引为什么会失效?
索引的本质是一棵 B+ 树,它的价值在于有序性。二分查找之所以快,是因为数据排好了序。索引失效就是:写法破坏了 B+ 树有序性的利用条件,MySQL 无法通过索引快速定位数据。
索引就像一本按拼音排序的字典。你可以用拼音快速找到"张"字在哪一页。但如果你的查询是"找所有姓名中包含’张’字的人",这本字典就帮不了你了,因为"包含张"这个条件无法利用拼音的排序规则,你只能一页一页翻。
索引只在能利用有序性的查询中生效。一旦查询条件破坏了有序性的利用方式,索引就会失效。
场景一:对索引列做函数运算
这是最常见、也最容易犯的索引失效场景。
-- 索引:idx_create_time(create_time) -- 失效:对索引列用了 YEAR() 函数 SELECT * FROM orders WHERE YEAR(create_time) = 2025; -- 生效:改成范围查询 SELECT * FROM orders WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01';
第一条查询为什么失效?因为 YEAR(create_time) 的结果是计算出来的,MySQL 需要对表中的每一行都执行一次 YEAR() 函数,然后拿结果去比较。这时候 B+ 树里存的是 create_time 的原始值,不是 YEAR() 的计算结果,有序性对计算后的值没有意义。
场景二:隐式类型转换
-- 索引:idx_phone(phone),phone 列是 VARCHAR 类型 -- 失效:传入了数字 SELECT * FROM user WHERE phone = 13800138000; -- 生效:传入字符串 SELECT * FROM user WHERE phone = '13800138000';
phone 是 VARCHAR,你传了一个 INT。MySQL 的规则是:当字符串和数字比较时,MySQL 会把字符串转成数字。 这意味着对 phone 列的每一行,MySQL 都要先把它从字符串转成数字,再和 13800138000 比较。这等价于对索引列做了 CAST(phone AS SIGNED) 函数运算,和场景一相同,索引就会失效。
那如果反过来呢?
-- id 列是 INT 类型 -- 传入字符串也不会失效,因为 INT 和字符串比较时,MySQL 把字符串转成数字 -- 转换发生在常量一侧,不影响索引列 SELECT * FROM user WHERE id = '123'; -- 走索引
规则:字符串列传数字会失效,数字列传字符串不会失效。 因为只有当转换发生在索引列一侧时,才相当于对索引列做了函数运算。
怎么判断是否发生了隐式类型转换?用 EXPLAIN 看,如果 Extra 里出现了 Using where 且 type 是 ALL,那么大概率就是。
场景三:隐式字符编码转换
这个场景比类型转换更隐蔽,很多人踩过坑但不知道原因。
-- 库 A:字符集 utf8mb4,库 B:字符集 utf8 SELECT * FROM db_a.user a JOIN db_b.order b ON a.name = b.user_name WHERE a.name = '张三';
如果两张表的关联列字符集不同,MySQL 在做 JOIN 比较时需要做字符集转换。转换发生在索引列上时,等价于对索引列做了函数运算,索引失效。原理和场景一相同,只是"变形"的方式从类型转换变成了编码转换。
场景四:LIKE 左模糊匹配
-- 索引:idx_name(name) -- 走索引:右模糊 SELECT * FROM user WHERE name LIKE '张%'; -- 不走索引:左模糊 SELECT * FROM user WHERE name LIKE '%三'; -- 不走索引:全模糊 SELECT * FROM user WHERE name LIKE '%三%';
回到字典的类比。字典按拼音排序,你可以快速找到"zhang"开头的所有字。但如果要求找"名字里包含’三’的字",拼音排序就帮不了你了,因为"三"可能出现在名字的任何位置,它的拼音分布在字典的各个角落。
LIKE '张%' 能走索引,是因为前缀确定时,这些值在 B+ 树中是连续的,可以范围扫描。LIKE '%三' 不行,是因为前缀不确定,值在树中是分散的,只能逐行检查。
场景五:OR 连接不同索引列
-- 索引:idx_a(a),idx_b(b) -- 失效:OR 连接了不同索引的列 SELECT * FROM t WHERE a = 1 OR b = 2;
a = 1 可以走 idx_a,b = 2 可以走 idx_b,但两条路径的结果要合并去重。MySQL 在大多数情况下只能选一个索引或者直接全表扫描。MySQL 8.0 引入了 Index Merge 优化,某些 OR 场景下可以同时使用多个索引,但优化器也可能认为全表扫描更快而放弃,不是万能的。
场景六:违反最左前缀原则
联合索引 (a, b, c) 的排序规则是先按 a 排,a 相同再按 b 排,b 相同再按 c 排。这个话题在我的另一篇文章《MySQL 最左前缀法则:为什么你的联合索引总是不生效》里详细展开过,这里只列核心规则,不做重复展开。
-- 索引:idx_abc(a, b, c) WHERE b = 2; -- 跳过了 a,不走索引 WHERE b = 2 AND c = 3; -- 跳过了 a,不走索引 WHERE a = 1 AND c = 3; -- 跳过了 b,只用到 a WHERE a = 1 AND b > 5 AND c = 3; -- b 范围之后 c 无序,c 失效
原因和前面的场景一样:跳过最左列后,后续列在 B+ 树中是分散的,无法利用有序性。
场景七:数据量太小,优化器主动放弃
这个场景经常让人困惑:明明写法没问题,索引也建了,为什么 EXPLAIN 显示全表扫描?
-- 表里只有 100 条数据 EXPLAIN SELECT * FROM user WHERE age > 20; -- type: ALL,没走索引
MySQL 的优化器是基于成本做决策的。走索引不是免费的,需要从 B+ 树根节点遍历到叶子节点,再通过主键回表取完整行数据。当表的数据量很小时,全表扫描可能只需要读一两个数据页,走索引反而需要更多 I/O。优化器经过成本估算,发现全表扫描更快,就会主动放弃索引。
同样的道理,当索引列的区分度太低时(比如 gender 只有 ‘M’ 和 ‘F’),走索引要回表的行数接近全表行数,顺序 I/O 比随机 I/O 快得多,优化器也会选择全表扫描。
这不是 bug,而是合理的优化。 可以用 FORCE INDEX 强制走索引来验证:如果强制走索引反而更慢,说明优化器的选择是对的。
一个公式:索引为什么会失效
看完七个场景,你会发现它们背后有一个共同的逻辑:
当 MySQL 无法利用 B+ 树的有序性来减少扫描行数时,索引就会失效。
拆开来看,有三种情况会导致有序性无法被利用:
| 情况 | 本质 | 对应场景 |
|---|---|---|
| 索引列被"变形"了 | 原始值的有序性对变形后的值无意义 | 函数运算、类型转换、字符集转换 |
| 查询条件无法利用排序 | 模糊匹配无法定位到连续区间 | LIKE 左模糊、OR 跨索引 |
| 走索引的成本太高 | 优化器认为全表扫描更划算 | 数据量小、区分度低 |
遇到索引失效的问题,先用 EXPLAIN 确认是否走了索引,再对照这个表格判断属于哪种情况。大多数时候,我们的改法都是围绕一个原则:让索引列保持"原样",让查询条件能利用 B+ 树的有序性。
小结
索引失效不是 MySQL 出了问题,而是 B+ 树有序性的自然推论。索引之所以快,是因为数据排好了序,可以二分查找。一旦你的查询写法让 MySQL 没法利用这个排序,索引就等于没有用处了。
七个场景中,最值得记住的是两个:对索引列做函数/运算和隐式类型转换。这两个在实际开发中出现频率最高,常容易被忽略。
排查索引失效时,EXPLAIN 是第一工具。看 type 是不是 ALL,看 key 是不是 NULL,看 Extra 里有没有 Using where。确认失效之后,对照本文提到的七个场景逐一排查,基本就能找到具体原因。
以上就是一文系统梳理MySQL数据库索引失效的七个场景的详细内容,更多关于MySQL索引失效常见场景的资料请关注脚本之家其它相关文章!
相关文章
CentOS Linux更改MySQL数据库目录位置具体操作
由于MySQL的数据库太大,默认安装的/var盘已经再也无法容纳新增加的数据,没有办法,只能想办法转移数据的目录,本文整理了一些MySQL从/var/lib/mysql目录下面转移到/home/mysql_data/mysql目录的具体操作,感兴趣的你可不要走开啊2013-01-01
MySQL开启配置binlog及通过binlog恢复数据步骤详析
这篇文章主要给大家介绍了关于MySQL开启配置binlog及通过binlog恢复数据的相关资料,binlog是MySQL最重要的日志,binlog是二进制日志,它记录了所有的DDL和DML语句,除了查询语句select、show等,需要的朋友可以参考下2024-06-06


最新评论