作为一个在数据库一线折腾了快十年的老码农,我有个习惯:每次面试新人,我都会把“MySQL索引失效”这六个字单独拎出来问。倒不是说这道题有多刁钻,而是它特别能检验一个人的基本功是否扎实——很多同学能把索引结构、B+树原理背得滚瓜烂熟,但一落到具体的SQL写法上,就说不清楚“为什么这个查询没走索引”。
这篇文章就围绕这道高频面试题展开,把索引失效的典型场景、排查手段和优化思路完整过一遍。内容不光是应付面试用的八股,更偏向我在生产环境里真实踩过的坑和复盘经验。如果你是刚接触索引的初学者,可以把它当作一份避坑清单;如果你已经有两三年开发经验,我建议重点看排查方法论和优化思路那部分,这两块才是实际工作中拉开差距的地方。
1. 内容整体设计与思路拆解
1.1 先从索引失效的本质说起
索引失效这四个字,字面意思是“索引没起作用”,但严谨点说,是“优化器没有选择使用索引,或者索引使用方式不符合预期。” MySQL里真正执行SQL的是优化器,它负责根据统计信息、成本模型和当前表数据情况,决定走全表扫描还是走某个索引。索引失效,本质上是优化器认为“走索引的成本比全表扫描更高”,或者干脆是“索引本身没办法被用来加速这条SQL”。
理解了这一点,你就会明白:索引失效不一定是索引坏了,也不一定是SQL写错了,而是你的SQL写法、数据类型匹配、索引设计这几者之间出现了错位。这也是我在面试时最想听到的回答方向——不是死记硬背“模糊查询前置百分号会失效”,而是能讲清楚背后的成本判断逻辑。
常见的索引失效场景,其实都可以归结为三类:
- 索引列被“加工”了(计算、函数、类型转换)
- 查询条件破坏了索引的有序性或连续性(范围查询、跳过列)
- 优化器的成本判断导致它放弃索引(小表全扫更快、基数过低)
这三类归纳起来,就是接下来要展开的主线。
1.2 为什么这道面试题值得反复研究
MySQL索引失效场景,表面看是一道SQL语法题,实际上牵扯到索引数据结构、优化器工作原理、执行计划解读、表设计合理性四个层面的知识。很多同学能说出一两个失效场景,但连不成体系,原因就在于只记住了“现象”,没有理解“机制”。
我建议把这道题当作一个知识锚点来复习。围绕“为什么优化器不选索引”,你可以往外延伸出不少问题:B+树索引的叶子节点存了什么?联合索引的排序规则如何影响范围查询?什么是回表成本?MRR优化在什么场景下有效?这些追问,才是面试官真正想看的深度。
而在工程层面,索引失效带来的问题往往不是“慢”,而是“突然慢”。很多系统平时跑得挺顺,某天数据量涨到一定规模,一条SQL的执行时间从几十毫秒飙到几秒,你一查执行计划,发现原本走得好好的索引突然不走了。这种问题最磨人,因为它不是宕机那种显性故障,而是性能悄悄劣化。所以把索引失效场景吃透,对日常性能巡检和应急排查都有直接帮助。
2. 核心细节解析与实操要点
2.1 隐式类型转换:最常见也最容易被忽略
隐式类型转换是我遇到最多的失效场景,往往出现在字符串字段没加引号,或者字段类型和查询条件类型不一致的时候。举个例子:
-- 假设 user_id 是 varchar 类型,且建有索引 SELECT * FROM user_info WHERE user_id = 10086;这条SQL看起来人畜无害,但user_id字段是varchar,传入的是数字10086。MySQL会先把varchar字段转换成数字,再进行比较。对索引列做类型转换,相当于在索引列上套了一层隐式函数,索引自然就失效了。
你可能会说:“我平时也这么写,没见变慢啊。”这就是隐蔽的地方——当表数据量小的时候,全表扫描也就几毫秒,完全感觉不出来。等表数据量过千万,这条SQL就会变成定时炸弹。
正确写法很简单:
SELECT * FROM user_info WHERE user_id = '10086';排查时我有个习惯:用EXPLAIN看执行计划,如果发现type列是ALL,而possible_keys里明明有索引,第一反应就是查一下字段类型和查询值类型是否一致。如果你用的是Python的MySQL驱动,参数化查询通常能避免这个问题,因为驱动会按字段类型正确绑定参数。但如果你习惯拼SQL字符串,就得格外小心。
2.2 函数处理索引列:让B+树无从下手
B+树索引的叶子节点存储的是索引列原始值的有序排列。一旦你对索引列做了函数处理,比如DATE()、LENGTH()、SUBSTRING(),优化器就没办法直接利用索引的有序性去做范围匹配,因为索引里存的是原始值,不是函数处理后的值。
一个很典型的场景是日期查询:
-- order_time 上有索引,这条SQL会导致索引失效 SELECT * FROM order_info WHERE DATE(order_time) = '2024-05-20';这个SQL的语义是“取出order_time日期部分等于2024-05-20的行”。但DATE()函数破坏了order_time的原始值,优化器无法利用索引快速定位,只能把整列数据都取出来,逐个执行DATE()再和条件比较。
我推荐的改写方式有两种。第一种是范围查询,直接利用B+树的有序性:
SELECT * FROM order_info WHERE order_time >= '2024-05-20 00:00:00' AND order_time < '2024-05-21 00:00:00';第二种是在表结构层面做冗余,比如单独加一个order_date日期字段,专门存日期部分。这样既不影响查询语义,又能利用索引。我曾经在一个订单系统里帮团队把几十条类似SQL做了改造,高峰期接口响应时间直接降了一个数量级。
注意:不是所有函数都会让索引失效。比如
ORDER BY子句里对索引列使用MAX()、MIN(),MySQL有专门的优化路径。但作为通用规则,索引列尽量保持“裸奔”状态,不要给它包任何函数。
2.3 前导模糊查询:%开头的后果比你想象的更严重
模糊查询是业务开发里的家常便饭,但“%关键字”这种写法会让索引失效,这几乎是常识级别的考点:
SELECT * FROM product WHERE name LIKE '%手机%';索引失效的原因很直观:B+树索引是按照从左到右的字符顺序有序存储的。LIKE '手机%'这种以固定前缀开头的查询,MySQL可以沿着索引树定位到第一个以“手机”开头的记录,然后顺序扫描到最后一个,这一整段在索引里是连续的。但LIKE '%手机%'的前缀部分是任意的,索引没办法快速定位起点,只能做全索引扫描甚至全表扫描。
不过,这个场景有个例外值得注意:如果查询只需要返回索引列本身,覆盖索引可以避免回表,但%前缀仍然会让索引定位失效。换句话说,覆盖索引只是救回了回表成本,救不了索引扫描本身。
实际业务中对付“包含”查询,我通常给出三个方案:
- 用
LIKE '关键字%'替代%关键字%,如果你业务上可以接受前缀匹配的结果 - 引入全文索引,MySQL自带的
FULLTEXT索引能有效应对文本检索 - 数据量比较大的场景,上搜索引擎(比如ElasticSearch),把模糊查询从数据库里彻底摘出去
这三个方案的成本依次递增,按业务规模选就行。日常开发里,我最反感的是在核心链路SQL里硬写%关键字%查大表,这种设计迟早会出事。
2.4 联合索引的最左前缀原则被破坏
联合索引的失效规则,是我面试时必问的进阶题。假设我们建了(a, b, c)联合索引,那么以下查询都会因为跳过最左列而失效:
-- 失效:跳过了a,直接用b SELECT * FROM t WHERE b = 2; -- 失效:跳过了a、b,直接用c SELECT * FROM t WHERE c = 3; -- 部分失效:a和c之间跳过了b SELECT * FROM t WHERE a = 1 AND c = 3;这里有个容易混淆的点:联合索引的顺序就是索引建树时的顺序。索引先按a排序,a相同的情况下才按b排序,b相同的情况下再按c排序。所以你在查询时如果跳过了前面的列,后面列的有序性就无从利用,索引自然失效。
顺带说一句,a = 1 AND c = 3这种写法,MySQL优化器在较新版本里会做一个优化:先用a命中索引,把a=1的记录都捞出来,再在索引层面过滤c=3,这叫作“索引条件下推”(Index Condition Pushdown,ICP)。虽然最终扫描的索引范围还是以a=1为准,但c字段的过滤被下推到存储引擎层,效率比把c过滤扔到服务层好得多。严格来说,这种SQL不算完全失效,但也没能最大程度发挥索引的威力。
所以最理想的做法,是让查询条件的顺序尽量和联合索引的列顺序保持一致:
WHERE a = 1 AND b = 2 AND c = 32.5 OR连接导致的索引失效:最多只能“部分命中”
OR的场景和联合索引不太一样,它属于另一种典型失效模式:
-- id 有主键索引,name 没有索引 SELECT * FROM user_info WHERE id = 1 OR name = '张三';这条SQL的结果集是“id等于1的记录”并上“name等于张三的记录”。id字段有索引,MySQL可以很快找到id=1的行;但name没有索引,MySQL只能把name等于张三的记录全表扫一遍。优化器一算账:反正我总要全表扫一次,不如干脆全表扫描完,然后统一过滤出两个条件的结果。于是id这个索引也被放弃了。
这个场景的最佳解法是避免OR,改用UNION或者UNION ALL拆开两条独立查询。
SELECT * FROM user_info WHERE id = 1 UNION ALL SELECT * FROM user_info WHERE name = '张三';但这里有个前提:两个查询不会产生重复数据,或者你不在乎重复。如果业务上允许重复,用UNION ALL效率远高于UNION,因为UNION去重有额外成本。
另外一个思路是给name也建上索引,这样优化器在两个条件都能命中索引的情况下,通常会用index_merge把两个索引的结果集合并。但这会引入索引维护成本,慎用。
2.6 范围查询与排序字段的边界值问题
范围查询导致的索引失效,常见于联合索引的中间列做了范围条件。继续拿(a, b, c)联合索引来举例:
WHERE a = 1 AND b > 10 AND c = 3这条SQL里,a能精准定位,b用范围查询也能利用索引的有序性,但c的等值条件就没办法利用了。原因还是B+树的有序性:当b是一个范围的时候,这个范围内c并不是有序排列的,所以c作为后续列,索引用不上。
这种场景我在实际业务里遇到得特别多,比如订单表“按用户查某时间段内的订单,还要过滤订单状态”,如果联合索引是(user_id, order_time, status),那么order_time的范围条件会让status的过滤被迫在回表后进行。
解决思路有几个方向:
- 调整联合索引顺序,把范围查询的列放在最后一个,让前面的等值条件都能利用索引
- 如果范围查询列本身就是高频必查条件,就接受这种部分失效的结果,同时尽量让索引覆盖更多查询字段,减少回表
- 如果范围查询的列值分布比较均匀,可以考虑改成等值条件枚举,比如把“最近7天”拆成“等于具体日期”的几个查询条件合并
这里没有标准答案,核心是理解索引有序性的原理,再针对业务访问模式做取舍。
2.7 其他容易踩的坑:运算、NOT IN、IS NOT NULL
剩下的几个场景,写成一条条速记清单:
- 索引列参与运算:
WHERE age + 1 > 20,这种虽然逻辑上等价于age > 19,但索引列age被“+1”加工了,索引失效。应尽量把运算挪到条件值的这一侧。 IS NOT NULL走索引的迷思:MySQL对IS NULL有特殊优化,但IS NOT NULL很多时候会让优化器放弃索引。尤其当表中NULL值占比很小时,IS NOT NULL匹配的行数太多,优化器更倾向于全扫。反过来,如果NULL值占比高,走索引的性价比就更低。NOT IN:这个和OR有点像。如果子查询或IN列表里大部分值都存在,NOT IN要过滤出来的行太少,优化器干脆全扫。用NOT EXISTS改写,或者改成LEFT JOIN ... WHERE xx IS NULL,往往效果更好。!=或<>:这类不等于操作符通常无法利用索引的等值定位能力,和NOT IN的逻辑类似。如果业务上确有必要,还是考虑改写,或者看是否能拆成大于和小于两个条件用UNION合并。
提示:这些规则有一条好处——它们的记忆成本很低,只要抓住“索引列要保持原始值”、“查询要能利用有序性”这两条核心原则,绝大多数失效场景你都能自己推导出来。
3. 实操过程与核心环节实现
3.1 用EXPLAIN快速诊断:一个完整案例
纸上谈兵没意思,我直接模拟一个真实的排查过程。假设某电商平台的订单表order_info有5000万行数据,结构大概是order_id主键、user_id、order_time、status、pay_amount。业务反馈:有一个订单查询接口最近变慢,SQL长这样:
SELECT order_id, user_id, order_time, status, pay_amount FROM order_info WHERE user_id = 123456 AND order_time BETWEEN '2024-01-01' AND '2024-03-31' ORDER BY order_time DESC;这个查询的意图很清晰:查某个用户在某个时间段的订单。按照常规设计,我们建了联合索引idx_user_time(user_id, order_time)。但DBA发现这条SQL的执行计划是type = ref,扫描行数却高达几十万。乍一看索引走了,但性能依旧很差。
实际上问题出在ORDER BY order_time DESC。虽然WHERE条件里的user_id和order_time都能利用联合索引,但排序字段和索引的顺序不完全匹配。idx_user_time(user_id, order_time)的默认顺序是user_id升序、order_time升序。当查询要求order_time DESC时,MySQL发现索引的顺序和自己要求的顺序不一致,就会额外做一个文件排序(Using filesort)。
排查过程分三步走。
第一步看执行计划:
EXPLAIN SELECT order_id, user_id, order_time, status, pay_amount FROM order_info WHERE user_id = 123456 AND order_time BETWEEN '2024-01-01' AND '2024-03-31' ORDER BY order_time DESC;输出里Extra字段出现Using filesort,问题定位到这里。
第二步看索引结构。由于这个查询的排序方向是DESC,MySQL 8.0支持降序索引,可以走(user_id ASC, order_time DESC)。但如果还是5.7版本,没有降序索引,就得考虑用(user_id, order_time)然后去掉ORDER BY DESC,让应用层反转结果集;或者接受文件排序,但要确保参与排序的行数不要太多。
第三步实测验证。我拆出两个方案分别跑:
方案A:把索引改成(user_id, order_time DESC),同时保留查询不变。
方案B:查询里去掉ORDER BY order_time DESC,应用层拿到结果后自行倒序。
两个方案在5000万数据量的测试环境下,方案A的接口耗时从450ms降到80ms,方案B也降到了120ms。方案A能把排序压力完全转移到索引,是最理想的,但前提是生产环境的MySQL版本是8.0。
3.2 手上没有Percona Toolkit,怎么快速定位慢SQL
生产环境排查索引失效,最直接的手段就是慢查询日志。我在上一家公司接手过一个历史项目,某核心接口的P99延迟一直在往上走,打开慢查询日志一查,发现一个有意思的现象:相同语义的SQL,有时候走索引,有时候不走。后来定位到是查询参数不同导致优化器的选择不同——参数值在某个分布区间内,走索引更划算;超出区间,优化器判断全扫更快。
这时候光看单条SQL的EXPLAIN是不够的,更有效的办法是把同一条SQL换成不同参数值,多跑几次EXPLAIN对比key和rows列的变化。如果发现key在某个参数阈值附近来回切换,基本就是统计信息不准或者成本模型偏差。
应对办法通常是两条:
- 用
ANALYZE TABLE重新收集统计信息,让优化器拿到更准的数据分布 - 业务侧对这个查询使用
FORCE INDEX,强制指定索引,但要明确这是一种“短期止血”手段,长期来看还是要优化业务查询模式。我见过有些团队图省事,对一批慢查询都加上FORCE INDEX,结果表结构一变更,索引名变了,SQL直接报错。所以FORCE INDEX用在关键位置可以,但一定要做好变更记录。
3.3 一个真实场景的索引优化:从失效到覆盖
继续上面的订单查询案例,我发现业务方其实还有另一个隐藏需求:查询结果只需要展示订单号、用户ID、订单时间和状态,不需要pay_amount。那么我直接把SQL改成:
SELECT order_id, user_id, order_time FROM order_info WHERE user_id = 123456 AND order_time BETWEEN '2024-01-01' AND '2024-03-31' ORDER BY order_time DESC;如果索引是(user_id, order_time),那么这条查询需要的所有列都在索引里,确实可以做到覆盖索引,不需要回表。在5000万行的大表上,减少几十万次随机回表,性能提升是非常可观的。
这里教你一个小技巧:看EXPLAIN输出时,如果Extra字段出现Using index,说明使用了覆盖索引;如果出现Using index condition,说明用了索引条件下推;如果什么都没写,大概率回表了。这几个标志性的关键词,能帮你快速判断索引使用的层级。
3.4 一张索引设计自检表
我整理了一张表格,按场景把索引设计的关键点列出来,方便你日常开发时对照检查。
| 场景 | 索引失效风险点 | 推荐做法 |
|---|---|---|
| 字符串等值查询 | 类型转换隐式发生 | 查询参数用字符串格式 |
| 日期范围查询 | DATE()函数处理索引列 | 改成范围查询,或冗余日期字段 |
| 模糊查询 | 前导百分号 | 前缀匹配 / 全文索引 / 外部搜索引擎 |
| 联合索引 | 跳过最左列或中间列做范围 | 调整查询列序,让等值条件在前 |
| OR条件 | 某分支无索引 | 拆UNION / 补齐缺失索引 |
| 排序需求 | 索引列排序方向与ORDER BY不一致 | 使用降序索引 / 覆盖索引兜底 |
| NOT IN / != | 选择性太差 | 改写JOIN / NOT EXISTS |
这张表不是万能药,但能在你设计索引时提个醒。真正的索引设计没有银弹,必须根据实际查询模式不断迭代。
4. 常见问题与排查技巧实录
4.1 为什么我用EXPLAIN看到了索引,查询还是慢
这是我在技术社群里被问得最多的问题。看到key列有索引,不等于索引效率高。EXPLAIN输出里有几个容易忽略的细节:
rows列是优化器预估扫描行数。如果rows很大,比如几十万,即使你用了索引,实际性能也好不到哪去。Extra列出现了Using filesort或Using temporary,说明排序或分组绕过了索引。filtered列表示存储引擎返回的数据在服务层再过滤的比例。如果这个值很低,意味着索引帮你定位的行后面又被大量过滤掉,同样浪费时间。
有时候索引确实在跑,但SQL查询的是SELECT *,回表几千行甚至上万行,每次都走随机I/O,这个成本可能比全表顺序扫描还要高。这种情况下,key列有索引反而是假象。
真实工作里,我见过不少“索引优化”变成“索引负担”的案例:一个表上建了七八个索引,每个字段组合都建了一个,结果写入性能被拖垮,查询也没快多少。所以要定期审视:这个索引到底服务了哪些查询?如果没有它,查询会不会慢得不能接受?
4.2 统计信息不准引发的索引失效,如何应急
有一个常见事故场景:一个大表平时查询都走索引,某天突然全扫,DBA一看,表数据量其实也没涨多少,但统计信息过期了。MySQL的优化器依赖统计信息来估算扫描行数,统计信息跟不上数据变化,估算偏差就会越来越大,最终导致优化器做错决策。
应急处理就三步:
- 执行
ANALYZE TABLE table_name刷新统计信息 - 重新
EXPLAIN确认执行计划是否恢复 - 复盘为什么统计信息会过期那么快——是否表频繁写入但
innodb_stats_on_metadata参数设置不当,或表数据量达到阈值但没有触发自动更新
这里要提醒一句:对大表执行ANALYZE TABLE会临时占用一些I/O,线上操作最好放到低峰期。
4.3 执行计划里的“Using index condition”和“Using index”区别
很多初学者会把这两个混淆。我用自己的话解释一下:
Using index表示“所有需要返回的数据都从索引里拿到了”,连回表都不需要,这就是覆盖索引。这是效率最高的状态。Using index condition表示存储引擎层先用索引把匹配的行定位,再在索引层面做条件过滤(比如联合索引的后续列过滤),最后如果还需要非索引列,还是要回表。它比完全回表省一点,但没到覆盖索引的级别。
拿刚才的(a, b, c)联合索引举例:
WHERE a = 1 AND c = 3;如果优化器启用了ICP,a=1用来定位索引范围,c=3会在存储引擎层过滤掉,减少回表次数。所以Extra字段就会显示Using index condition。知道了这个细节,你在做SQL优化时就能很清楚地判断,索引还有多少提升空间。
4.4 一个大坑:字符集不同导致索引失效
这是非常隐蔽的一个场景,网上讲的人不多,但我在实际项目中踩过。某业务表字段用的是utf8mb4,另一个关联表的字段用的是utf8,当两张表做JOIN并关联这两个字段时,MySQL需要把两边都转成同一种字符集再做比较。一旦发生隐式转换,索引就失效了。
排查这个问题的标志是EXPLAIN输出里key列空掉,或者rows数量异常大。解决思路有两种:一种是把两边的字符集统一起来,在建表时就定好规范;另一种是在SQL里用CONVERT显式转换,但要注意转换的“方向”——尽量把带索引的字段留在不转换的那一侧。
这类问题最坑的地方在于,联表查询在开发环境数据量小,根本感受不到差异,等上了生产、数据量一大,性能问题立刻暴露。所以建议团队在开发规范里强制约束字符集统一,不要给后续留隐患。
4.5 索引失效排查SOP
经过这些年的实战,我把索引失效排查整理成了一个标准操作流程,分享出来:
- 拿到慢SQL,先做语义理解。搞清楚查询想返回什么、过滤条件是什么、排序条件是什么。
- 用
EXPLAIN看执行计划。重点看type、key、rows、Extra四列。 - 如果
key为NULL,检查:是否有索引?查询条件是否破坏了索引列原始值?是否存在类型转换? - 如果
key有值但rows很大,检查:是否SELECT *导致大量回表?是否排序字段和索引顺序冲突?是否统计信息不准确? - 用
EXPLAIN ANALYZE(MySQL 8.0+)或SHOW PROFILE拿到实际的执行耗时和扫描行数,验证判断。 - 综合以上信息,选择合适的改写方式:调整SQL写法、调整索引结构、还是用覆盖索引兜底。
- 上线前在主从或测试环境做压测,对比改前改后的执行计划,确认无副作用。
这个SOP不是什么高深理论,但确实能帮你在面对问题时不慌,一步步往下走。
5. 索引优化的进阶思路与避坑心得
5.1 覆盖索引:性价比最高的优化武器
在索引优化里,覆盖索引是性价比极高的手段。它不需要改SQL,只需要调整索引字段,让查询要返回的列全部落在索引里。比如这张表:
CREATE TABLE product ( id INT PRIMARY KEY, name VARCHAR(64), category_id INT, price DECIMAL(10,2), KEY idx_category (category_id) );如果有一条高频查询:
SELECT id, name FROM product WHERE category_id = 5;现有索引idx_category(category_id)只能定位到category_id,返回id和name都得回表。那我把索引改成idx_category_name(category_id, name),或者idx_category_id_name(category_id, id, name),这条查询的所有列都从索引里拿,杜绝回表。
这就是“用空间换时间”的思路。多存几个字段在索引里,索引体积会变大,但查询效率的提升往往能覆盖这部分代价。需要注意,索引列数量不要贪多,一般建议覆盖到高频查询的字段即可,否则写入性能和索引维护成本都会上来。
5.2 联合索引设计:等值在前,范围在后的经验法则
联合索引设计,有一个比较简单通用的经验法则:让等值条件的列放在最前面,让范围条件或排序条件的列放在最后面。
继续拿电商订单举例。如果查询模式是:
WHERE user_id = ? AND status = ? AND order_time BETWEEN ? AND ?那么联合索引优先考虑(user_id, status, order_time)。因为user_id和status是等值条件,能精准定位,order_time是范围条件,放最后,B+树的有序性还能帮上排序或范围扫描的忙。
这里有个容易犯的错误:有些同学一看到order_time范围条件就把它放在联合索引第一列,觉得“时间用得多,必须建第一个”,结果user_id这个等值条件就得在回表之后才过滤,行数哗啦啦地多出好几个数量级。设计索引时一定要追着WHERE等值条件走,不要被字段使用频率迷惑。
5.3 生产环境索引优化的避坑心得
最后分享几个我在生产环境里踩出来的心得,希望你能避开同样的坑。
第一个:不要对热表频繁加索引。每次加索引,MySQL除了更新数据字典,还要重建相关的统计信息和索引结构。大表加索引的执行时长可能以分钟计,期间对写入有一定影响。我一般会先评估清楚:这个索引到底解决什么问题?有没有别的更轻量方案?确认之后,再选在业务低峰期执行。
第二个:把索引失效的排查前置到代码评审阶段。我在团队里推行过一个简单规则:凡是超过百万行的核心表,新SQL必须附上EXPLAIN截图。评审的时候重点看有没有全表扫描,有没有字段类型不匹配,有没有前导模糊查询。这套规则执行下来,线上突发的“慢SQL”量明显减少,很多问题在代码阶段就被拦住了。
第三个:不要轻视FORCE INDEX的副作用。我之前提到过一次,这里再强调:FORCE INDEX的确能在优化器抽风时强行纠正执行计划,但它的耦合度很高。一旦后续索引更名、删除、重建,SQL就很可能报错。所以FORCE INDEX只作为短期止血手段,长期一定要从索引设计或SQL写法上解决根因。
第四个:善用performance_schema和慢查询日志做周期性巡检。索引失效不是一次性的问题,随着表数据量增长、业务查询模式变化,原来好用的索引可能慢慢变得不再高效。我习惯每两周拉一次慢查询日志,筛查Top N慢SQL,用EXPLAIN重新审视一遍执行计划,必要时做索引调整。这套周期性巡检,比等用户投诉再处理要舒服得多。
6. 最后一个提醒:面试表达与工程落地的平衡
既然标题是“每日一道面试题”,免不了多说一句关于面试表达的事。你可以把各种失效场景背得很熟,但面试官更想听的是你如何“诊断”一条慢SQL。我建议面试时按这个框架来回答:
- 先概括索引失效的本质:优化器认为索引成本更高,或者索引无法被用于加速
- 再分类列举典型场景,每类场景附一个SQL例子和一个改写方案
- 最后讲一个自己踩过的真实案例,重点描述排查过程和最终方案
这样回答的好处是:有原理、有案例、有工程落地,面试官很容易对你的系统能力留下印象。而工程落地部分,恰恰是很多只背八股的同学最欠缺的。
在实际工作中,我也见过不少同事对索引细节一知半解就匆匆优化SQL,结果越改越乱。与其这样,不如先花点时间理解B+树索引的有序性,理解优化器成本模型,理解执行计划里每一列的含义。这几个基础扎实了,索引失效的问题对你就不是“背场景”,而是“看穿机制”。我自己带团队的经验是,能在半小时内把一条慢SQL从执行计划分析到索引设计建议的工程师,基本都能扛住大部分线上性能问题的拷问。
这次分享的内容比较长,但从索引失效的本质、常见场景、排查技巧到优化思路都覆盖到了。如果你最近也在准备面试,建议把文中这张自检表打印出来,平时写SQL之前对照一遍。如果你已经在一线写业务代码,我更推荐把那段排查SOP存下来,下次遇到慢SQL时直接按步骤走。MySQL索引这事儿,理论是骨架,实践才是血肉。希望这篇内容能帮你把骨架搭得更稳。