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
WHERE user_id = 10086
OR status = 'PENDING';
user_id上有索引,status上有索引,单看这两个字段,感觉优化器可以用索引合并把两个结果集合并。但实际情况是,优化器面对OR时,要求每一侧的字段都具备索引访问的能力,任何一个分支不行,整个条件就得退化成全表扫描。
我刚说到这个例子其实每侧都有索引,理论上是能走索引合并的。但更常见的坑是这种:
sql复制SELECT * FROM notify_record
WHERE user_id = 10086
OR status = 'PENDING'
OR content LIKE '%退款%';
第三种条件写上去后,前面两个索引条件跟着一起报废,整个查询变成全表扫描。这里的教训就是:OR的所有分支,都必须确保索引可用。稳妥的做法是用UNION把不同索引条件的查询拆开:
sql复制SELECT * FROM notify_record WHERE user_id = 10086
UNION
SELECT * FROM notify_record WHERE status = 'PENDING';
UNION各分支可以独立执行,执行计划更清晰,每一条都能充分利用各自索引。避免OR条件里混入无法走索引的字段,在SQLReview时我基本会直接打回。
2.5 NOT IN、NOT LIKE和不等值判断:反向条件不友好
反向条件是索引失效的重灾区之一。NOT IN、NOT LIKE、!=这些看似很平常的条件,到底能不能走索引,我一直建议团队记住一条:正向条件通常都能走索引,反向条件大概率要全表扫描。
日常开发里,IS NOT NULL是一个特殊点,MySQL对IS NOT NULL实际上是可以使用索引的,因为索引本身记录NULL值,但NOT IN的处理逻辑是排除一批值,优化器需要在索引树里遍历大量节点,再结合回表成本来判断,算下来多数情况下不如全表扫描或者额外做排序实惠。
我也见过数据分布非常极端的情况,表中某列99%的值都是A,只有1%是B,这时候WHERE col != 'A'理论上走索引就很好。但优化器不是百分百聪明,它仍可能选择全表扫描。所以反向条件的代价,不是简单通过调参数就能解决。
针对这类需求,我的实操建议是得回到需求本身:能不能把反向条件改写成正向条件?比如NOT IN (1,2,3)有时可以改写为<1 OR (>3 AND ...)的形式,把范围条件放到正向轨道上。如果不能改写,就要接受全表扫描的风险,考虑用其他方案,比如在业务层缓存排除结果,再或者上搜索引擎。
2.6 联合索引不满足最左匹配:字段顺序决定命运
联合索引的最左匹配原则,理论上很好理解,但实际生产里非常容易忘。最典型的坑是:你建了一个(user_id, status, create_time)的联合索引,然后写SQL时直接WHERE status = 'PENDING' AND create_time >= '2025-01-01',绕过了第一个字段user_id。
失去最左匹配意味着索引里,所有叶子节点的排序都是先按user_id排序,再按status,最后按create_time。如果你上来就用status过滤,整个索引树没法快速收敛到目标范围,只能走一遍全量扫描,挨个判断是否符合条件。
还有同学可能在查询里加了一个排序字段,比如ORDER BY create_time,但order by字段并不在当前索引中,此时文件排序就出现了。这里的要点是联合索引的字段顺序,既影响过滤条件,也影响排序是否能走索引。加索引前,一定要把业务最频繁的查询条件的前缀对齐。
当然,MySQL 8.0的优化器在特定条件下引入了skip scan优化,允许跳过一个或多个前缀列直接使用后续列,但这不是银弹,需要满足较多前置条件,实际场景里未必触发。所以日常开发时,还是老老实实按最左匹配来设计索引,把skip scan当成惊喜,而不是依赖。
3. 为什么这些场景会让索引失效:底层逻辑与核心原则
3.1 B+树的有序性假设
所有失效场景,本质上都指向同一个核心:B+树的索引结构依赖列值的有序性。你建立一个索引,相当于所有数据行按照索引键值有序排列,并且以多级树形结构存储。查询时,优化器在树上从根节点逐层向下,利用键值大小比较判断目标在哪个子节点里,最后在叶子节点的有序链表上精确或范围定位。
这个模型的根基,就是比较必须在原始键值之间进行。如果查询条件中索引列被一层处理包裹,不管是函数、计算、类型转换,还是左右模糊匹配导致的排列失效,优化器就失去了直接比较的能力,于是放弃这个索引。
我用一个生活化的例子来解释:你去图书馆找书,图书馆的书架按书名的拼音首字母排序。如果你知道要找《红楼梦》,自然知道去H区域找。但如果你要找的是书名里包含“梦”字的书,你没一个固定位置可以从那里入手,只能一本本翻遍所有书架。
这就是索引失效的核心逻辑,不理解这一点,就永远是被动背场景清单。
3.2 优化器是一个估算成本的决策引擎
另一点需要建立认知的是,MySQL执行SQL时并不会按你慢SQL语句格式机械地执行,它会像一个精打细算的路由器,先对候选执行计划做成本估算,选一个它认为最便宜的方案。这里的成本主要由三个维度构成:IO成本、CPU成本和内存排序成本。
如果整张表只有1万行,一条SQL即使全表扫描,可能也就几毫秒,优化器觉得自己扫描整表比走索引再回表更划算,这时即便索引存在也可能不会用。很多人一看EXPLAIN没走索引就觉得索引建错了,其实换个数据量级,执行计划可能完全不同。
我实际见过一张表,因为大量历史数据堆积,全表扫描的基数远高于索引扫描,导致优化器选择了全表扫描,接口延迟成倍上升。后来项目组把历史归档出去,数据量下降,优化器又开始走索引了。所以理解优化器行为,得先理解它是在做成本决策,索引只是一个选项,不是必选项。
3.3 索引选择性与回表代价
索引失效之外,另个经常被忽略的概念是索引选择性。一个索引到底能不能提升查询效率,关键看它能过滤掉多少数据。选择性 = 去重后的键值数量 / 总行数。这个比值越接近1,索引的辨识度越高,优化器越愿意用;比值太低,比如性别列0和1两个值,选择性只有0.00002,优化器算一笔账后大概率选择放弃索引。
这和隐式类型转换、函数包裹的场景叠加起来,就会形成更大的坑:索引列本身的选择性低,又被函数包裹了一层,双重打击下优化器没有任何理由去选它。
回表代价也不能忽略。之前说过,索引分聚簇索引和二级索引,二级索引的叶子节点存的是主键值,通过主键再去聚簇索引里找完整数据行,这个过程叫回表。如果查询需要回表,而且数据量很大,那扫描大量二级索引后再逐个回表的成本,可能比直接全表扫描还高。这也是为什么讲索引设计时,我总强调覆盖索引的重要性——让查询的字段都在索引里,省掉回表,自然快了。
text复制成本模型相关参数(MySQL 8.0)典型设置:
- mysql.innodb_stats_persistent:控制统计信息持久化
- optimizer_switch 中的 index_merge、skip_scan 等开关
- 在线调整:SET optimizer_switch = 'skip_scan=on';
4. 排查索引失效的实操流程和关键工具
4.1 从慢查询日志到问题SQL的定位链路
排查索引失效,第一步不是打开EXPLAIN,而是先要有问题SQL的入口。我习惯从慢查询日志开始,确认哪些SQL超过阈值,以及出现的频次和执行的并发量。
先看慢查询日志怎么打开。注意,慢查询日志有开关控制,确认生产环境开启的前提下使用。我在测试环境一般是先开启再复现,生产环境必须评估日志量对磁盘IO的影响:
sql复制-- 查看当前慢查询日志状态
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';
-- 开启慢查询日志与阈值设置
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
long_query_time = 1的意思是,耗时超过1秒的SQL会记录到慢查询日志。线上如果太多,可以用2秒甚至5秒作为阈值,但我会先设为1秒,盯一周,抓出来的样本足够多,再针对高频率SQL做优化。
打开慢查询日志后,再用mysqldumpslow工具把零散日志聚合成统计报告,直接看TOP N耗时SQL是哪几条。这个工具的用法我记忆很深,因为它能按执行次数、总耗时、平均耗时聚合,比一条条翻日志方便得多。
bash复制mysqldumpslow -s t -t 10 /var/log/mysql/slow-slow.log
参数-s t表示按总耗时排序,-t 10表示只要前10条。通常情况下,TOP N列表一出来,问题基本就锁定在几条SQL上了。
4.2 使用EXPLAIN分析执行计划:四步定位法
拿到具体SQL,就用EXPLAIN看执行计划。注意,如果是慢查询里抓出来的,一定要在类似的数据量下复现,否则执行计划可能失真。
sql复制EXPLAIN SELECT * FROM order_info
WHERE user_id = '10086' AND status = 'PENDING' AND create_time >= '2025-01-01';
看执行计划,我会按四步走。
第一步看type字段。这个字段是访问类型的核心,从好到坏依次是:system、const、eq_ref、ref、range、index、ALL。其中ref和range都是可接受的,ALL是重点怀疑对象,意味着当前是全表扫描。
第二步看key字段。如果key为NULL,说明这个查询压根没用索引,结合type=ALL,索引失效问题基本坐实。如果key不为空,还要看key_len,它能反映联合索引实际用了哪几列,帮助你判断索引是不是只用了前缀。
第三步看rows字段。它不精确,是优化器的估计行数。如果估计的扫描行数接近全表行数,大概率存在失效或者选择性问题。
第四步看Extra字段。这个字段里经常藏着一锤定音的信息。我常用到的几个关键字:
Using where:表示存储引擎返回后,MySQL服务层还要再过滤,常见于索引有效但条件不完全覆盖,或索引部分失效的情况。Using filesort:文件排序,通常意味着ORDER BY字段没走索引。Using temporary:临时表。多表连接或去重场景下容易出现,代价一般较大。Using index:这是一个好消息,表示覆盖索引,不回表直接取数据。
一次典型的排查,如果看到type=ALL,key=NULL,Extra里面有Using where,基本可以确定索引失效或者压根没建对索引,再回到字段定义和SQL写法上去对。
4.3 利用OPTIMIZER_TRACE和profile钻取优化器决策
EXPLAIN能告诉你结果,但不会告诉你为什么优化器做了这个选择。如果想深挖,可以用MySQL的优化器追踪功能,看到优化器在内部评估的细节。
sql复制-- 开启优化器追踪
SET optimizer_trace = "enabled=on";
-- 执行你的SQL
SELECT * FROM order_info
WHERE user_id = '10086' AND status = 'PENDING' AND create_time >= '2025-01-01';
-- 查看追踪结果
SELECT * FROM information_schema.OPTIMIZER_TRACE;
结果集的JSON结构很长,我一般只看rows_estimation里的cost估算、considered_execution_plans里的计划选择和attached_conditions里的条件附着方式。这样就能看到,优化器是根据什么样的成本预估放弃某个索引的,是它认为扫描全表成本更低,还是索引因为表达式原因根本没有进入候选集。
另外,SHOW PROFILE可以看SQL执行各阶段的耗时分布,定位瓶颈是Sending data还是Sorting result。当排查深度追求到这种细节,基本就不会再有“玄学慢SQL”这种感觉了。
5. 从一次线上优化看完整实践过程
5.1 压测暴露的三条慢SQL与初步分析
前阵子我们做了一个订单列表功能的性能压测,目标是最差情况下接口P99延迟控制在500毫秒以内。在压测过程中,有三条SQL反复出现在慢查询日志里。
第一条:
sql复制SELECT * FROM order_list
WHERE create_time >= '2025-02-01' AND create_time < '2025-03-01'
ORDER BY total_amount DESC;
create_time有索引,但这SQL既要用范围条件过滤,又要按total_amount排序,排序字段不在索引里,Extra里出现了Using filesort,1万单数据排序就要花200多毫秒。
第二条:
sql复制SELECT * FROM order_list
WHERE status != 'CANCELLED'
AND user_id = '9527'
ORDER BY create_time DESC;
status !=是反向判断,导致优化器选了全表扫描,即便user_id单独有索引,也因为反向条件的加入让执行计划变得混乱。
第三条:
sql复制SELECT order_no, receiver_name, receiver_phone
FROM order_list
WHERE user_name = '张三';
user_name字段建了普通二级索引,但表里一万行数据有好几千个同名用户,索引选择性太低,优化器估算回表成本高于全表扫描,就直接放弃索引了。
这三条SQL非常有代表性,分别对应了索引设计缺陷、反向条件失效和低选择性索引三种常见问题。
5.2 针对三条SQL的索引调整方案
第一条SQL顺理成章可以建联合索引。原表已经有idx_create_time,但total_amount不在索引里,所以排序要走文件排序。我建议改成联合索引(create_time, total_amount)。
这样create_time范围条件过滤时,索引本身就是按金额排好序的,优化器可以直接顺序读取,省掉filesort。这个改动落地后,查询直接从需要排序变成了走索引顺序扫描,执行耗时下降了一个量级。
第二条SQL的问题比较复杂。status != 'CANCELLED'这个条件语义上是要排除已取消订单,而表中大部分订单是非取消状态,所以反查一个少数值其实代价更大。我调研业务后发现,这个接口的准确需求其实是想查“用户活跃订单”,也就是状态是PENDING或PROCESSING的订单,完全可以用正向IN条件来写:
sql复制SELECT * FROM order_list
WHERE user_id = '9527'
AND status IN ('PENDING', 'PROCESSING')
ORDER BY create_time DESC;
改写之后,我保留了原来(user_id, status, create_time)联合索引中user_id和create_time的可用性。这里有个细节值得说,如果查询条件里有IN,索引字段顺序设计依然得按最左匹配原则来:user_id在联合索引首位,status在前缀范围内过滤,create_time继续发挥作用。
第三条SQL因为是模糊需求,加上用户昵称本身没有唯一性,根治方案是增加一个user_no唯一标识列,或者让业务层先根据模糊匹配找到用户ID,再基于ID精确查询。最终上线的方案是业务侧先走搜索服务,再拿ID列表回来IN查询。这条经验很典型:索引不是万金油,低选择性的字段就不适合建二级索引,别恋战,换思路更快。
5.3 结果对比与压测前后的关键数据
拿压测结果说话,调整前三条SQL在同一条压测链路中合计耗时约1.8秒,调整后合计降到90毫秒左右,接口P99从1.6秒降到了380毫秒。当然这个数据受数据量、并发、硬件环境影响,不建议直接当基准参考,但整体量级的差异是显而易见的。
实际效果中还有几个意外收获:原来因为文件排序导致的临时表空间消耗下降了不少,数据库整体CPU负载也平稳了很多。索引优化虽然看起来是单点SQL调整,但会让整个数据库的繁忙程度都发生变化,这在我多次实践中都得到了验证。
6. 建立长效预防机制:从治标到治本
6.1 开发规范中的索引防呆设计
每次线上慢SQL优化完,我都建议团队把这条写入规范,否则不了多久问题又会回来。我对团队里的Java开发有一条硬性要求:条件列上禁止使用函数、禁止隐式类型转换、禁止对索引列做任何计算。
听起来很简单,但许多代码Review阶段很难盯住。所以我把这些规则接入了自建的SQL审查流程。值得说一下的是,市面上很多SQL审查工具支持规则引擎,可以配置禁止条件,例如Python生态里的sqlparse先做AST解析再匹配规则,如果你团队有CI系统,完全可以把这套写成一个MR检查项,让提交代码的同事在合入之前就发现风险,不用靠人肉Review。
对于历史存量代码,我也会定期用工具扫描全量SQL,把慢查询日志里疑似索引失效的SQL自动聚类,推送给对应负责人整改。这种主动巡检机制,比被动等告警强得多。
6.2 定期索引健康巡检
索引建完之后不是一劳永逸,数据在不断增长,业务查询特征也会变化。我一直保持的巡检节奏是——每两周看一次慢查询日志和索引统计信息。
MySQL的统计信息可以在几大表里查看:
sql复制SELECT
TABLE_NAME,
INDEX_NAME,
SEQ_IN_INDEX,
COLUMN_NAME,
CARDINALITY
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_db'
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
CARDINALITY字段就是基数,可以判断索引选择性。如果某个索引的基数远小于表行数,说明这个索引可能很低效,要么别建,要么换字段组合。
还有一张比较实用的表是sys.schema_unused_indexes,可以快速找出从未使用过的索引。这种索引的存在会让写入速度变慢、磁盘占用更高,应该果断删除:
sql复制SELECT * FROM sys.schema_unused_indexes;
线上环境删除索引需要谨慎,建议先在测试环境观察一段时间,确认没有任何查询依赖它后再动。
6.3 建立索引变更的评审机制
建索引这件事,表面上是DBA的活,但很多团队根本没有专职DBA。我的经验是,索引变更必须走评审,不能谁想到就加一个。
评审机制的核心就四件事:变更目的、SQL覆盖场景、字段选择性评估、回表与成本预估。文档上把这些写清楚,负责人确认没问题再执行。执行时也要选在业务低峰期,MySQL 8.0支持在线加索引,可以在线执行,但还需要考虑长事务的锁竞争和磁盘临时空间消耗,统一评估。
另外,加索引时的规范也不要忽略,比如索引名要清晰可辨,让后续接手的人能分清楚这个索引是为哪个场景服务的。idx_user_id_status_create_time这个名字,比idx_order_list_1有意义多了。索引就像代码,命名清晰能让运维效率提升一个台阶。
7. 现场排查用的参考资料与常用命令
日常排查索引问题时,我会频繁用到这些资料和命令。整理成清单,方便直接抄用。
第一部分是术语速查,几个核心概念的展开说明:
ref:非唯一索引等值匹配,返回多行range:索引范围扫描,常见于>、<、BETWEEN、INindex:全索引扫描,比全表扫描好一点,但也不理想ALL:全表扫描,通常意味着索引选择或条件写法出了问题
第二部分是常用信息查询命令:
sql复制-- 查表结构(确认字段类型和索引定义)
SHOW CREATE TABLE order_list;
-- 查看表行数和统计信息
SELECT TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'order_list';
-- 查看所有索引基数
SHOW INDEX FROM order_list;
-- 查看当前连接正在执行的SQL
SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep';
第三部分是问题的典型处置顺序。我的排查流程一般是:慢查询日志抓SQL → EXPLAIN看执行计划 → 查表结构和索引 → 尝试改写SQL或调整索引 → 回测对比。如果EXPLAIN显示type=ALL,先确认字段类型和条件写法是否正确;如果type=ref但rows很大,就得考虑选择性太低和回表成本问题。
这套流程我几乎每天都用,已经形成肌肉记忆了。对于刚接触的同学,建议把EXPLAIN的输出字段完整读一遍文档,重点关注type、key、rows、Extra四个字段,它们能完成90%的索引失效问题定位。
8. 写在最后:索引是张精密的地图,别拿它当万能钥匙
回到这次索引失效的现场,说了这么多场景和排查方法,我个人最大的感触是:索引优化没有银弹,也不能靠背场景清单解决问题。理解了B+树的排序结构,理解了优化器的成本估算,理解了执行计划的生成逻辑,你就能在面对任何一条慢SQL时,顺着原理一步步推演出问题在哪,而不是只能套用网上看来的经验。
我在实际项目里最经常踩的坑,反而是那种“看着索引都建了,执行计划却还是不走”的复杂情况。每当遇到这种问题,我会强制自己回到两个原点:第一,索引列是否保持了原始形态,不要被任何函数、计算和类型转换包裹;第二,索引选择的字段顺序是否匹配查询条件的最左前缀。只要这两点没有问题,绝大多数索引失效都能被规避掉。
最后再分享一个小技巧:加索引前,先花五分钟自己推算一下这个查询的执行路径,猜一下最终会扫描多少行、回表多少次。然后执行EXPLAIN去验证你的预判。这种“先猜后查”的习惯能大大提升你对优化器行为的敏感度,坚持下来,你会发现自己写SQL的质量有明显变化。索引这个领域,越深入越有意思,希望这篇内容能给你的实际工作带来一点帮助。
