1. 开篇:说说这次索引失效的现场
连着上篇聊完索引规则之后,后台留言一下子多了起来,不少人都在问同一个问题:为什么明明按照规范建了索引,线上慢查询还是层出不穷?恰好上周我就遇到一个挺典型的案例,一个本来预期毫秒级返回的列表查询,愣是变成了三秒多,接口超时告警直接打到手机上。排查下来,罪魁祸首又是索引失效。这次我不打算只梳理场景,更想把排查思路和底层原理一起讲透,因为单纯背场景清单,换个条件换个写法照样踩坑,关键是得理解优化器是怎么决策的。
这篇内容主要针对Java后端开发、数据开发以及所有需要写SQL的日常打工人,无论你是刚接触索引优化,还是已经在生产环境里摸爬滚打多年,这篇文章都会给你一些可直接上手的经验。我会结合MySQL 8.0的优化器行为,把高频的索引失效场景逐个拆解,同时给出我实际用的排查方法和预防手段,看完之后至少面对慢SQL时心里有底。
先给没看过上篇的朋友补个背景:上篇重点聊了B+树的组织方式、联合索引的最左匹配原则,以及回表和覆盖索引的基本概念。这篇外延会更大一些,覆盖六种高频失效场景、索引失效背后的底层逻辑、一套完整的排查流程,最后聊聊我从这些坑里总结出来的预防机制和索引治理思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引失效的六种高频场景拆解
2.1 隐式类型转换:最隐蔽的索引杀手
先说说隐式类型转换,这是我在生产环境里碰到频率最高,也是最让人头疼的一种失效场景,因为它的隐蔽性极强,看SQL语句时完全看不出问题,甚至配上参数一跑还觉得挺正常的。
有一天下午同事跑过来和我吐槽,说用户表查询突然变慢了,看执行计划发现type还是ALL,全表扫描。那条SQL大概长这样:
sql复制SELECT * FROM user_info WHERE phone = 13800138000;
乍看没什么问题,phone是手机号,用等值去查,索引肯定能用。但这里有个细节——如果要优化这条SQL,第一步得检查phone字段在表里的定义。我让同事查了一下字段类型,发现是VARCHAR(11)。问题就在这:当字段类型是字符串,等号右侧传的是数值型参数时,MySQL优化器会默认将字符串字段转为数值再比较,于是对索引列应用了隐式转换函数,破坏了索引的有序性,索引就失效了。
换成你日常业务里的写法,最常见的隐式转换集中在两类:表和字面量之间的类型不一致,多表关联时两张表的字段类型或字符集不一致。第二种我们后面单独说,这里先撑开第一种。
如果不想改SQL写法,可以通过CAST主动调整参数类型,把条件改成phone = CAST(13800138000 AS CHAR),这样优化器就知道右侧是字符串,匹配规则清晰,不会在索引列上做转换。但说实话,根治方案还是规范开发习惯,字符串类型就用引号包起来,只要SQL里传参格式和字段类型保持一致,这种问题完全可以避免。
2.2 函数包裹索引列:索引有序性被彻底破坏
比隐式类型转换更直白的失效场景,是直接对索引列使用函数。我在很多业务代码里见过的写法是:
sql复制SELECT * FROM order_info WHERE DATE(create_time) = '2025-01-15';
这条SQL在逻辑上没毛病,就是想查某一天创建的订单,但执行的时候会把create_time列上所有值都先套一层DATE()函数,让索引的有序排列失去意义。前面说过,B+树的索引是严格按照字段原始值排序的,一经过函数变换,优化器没办法按这个变换后的结果去找指定位置,只能全表扫描。
对应方案通常有两个思路。第一个思路是改写SQL,把函数从列上挪走,改成范围查询:
sql复制SELECT * FROM order_info
WHERE create_time >= '2025-01-15 00:00:00'
AND create_time < '2025-01-16 00:00:00';
这样索引列保持原始形态,叶子节点顺序可被直接利用,优化器能走range扫描。第二个思路是如果业务里真的高频使用日期函数,直接在业务表上冗余一个日期列或者直接在创建时冗余,然后建立索引。或者也可以使用MySQL 8.0之后支持的函数索引,ALTER TABLE order_info ADD INDEX idx_create_date ((DATE(create_time)));。用函数索引相当于提前把变换后的值存好,但代价是写入时做额外计算。
2.3 LIKE通配符前置:前缀匹配才有救
关于LIKE的索引失效,我发现不少开发写SQL时,根本意识不到左右模糊匹配和右侧模糊匹配的性能差距有多大。实际上,只有后缀为%的情况(比如LIKE 'abc%')才可能走索引,前缀带%(比如LIKE '%abc')或者两侧都带%(比如LIKE '%abc%'),即便是看着很短的字符串,索引也没戏。
原因还是得回到B+树结构,索引是有序排列的,如果知道前缀,优化器就能定位到范围起点,在叶子节点链上沿着双向指针顺序扫下去。但如果你只知道中间某段或结尾某段,树上没有任何一个位置能作为起点,排序关系失效了,只能一个个遍历,从根本上绕开了索引。
实际业务里,负责模糊搜索功能的同学会经常遇到这个问题。我之前处理过一个搜索框需求,默认就要匹配任意位置的字符。当时给的建议是:能改需求就改成前缀匹配,或者用全文索引,对应中文分词也有替代方案。从产品体验角度,前缀匹配其实已经覆盖多数搜索场景,真要去支持任意位置匹配,就得用专门的搜索引擎了,数据库这个环节的设计目标就不是干这个的。
2.4 OR两侧字段条件不齐全:宁可拆分成两条
OR条件对优化器的引导作用,很多人容易忽略。之前有个线上通知类SQL长这样:
sql复制SELECT * FROM notify_record
WH
