news 2026/10/17 5:34:30

别只盯着EXPLAIN的key:索引失效的真正原因与排查方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别只盯着EXPLAIN的key:索引失效的真正原因与排查方法

排查慢SQL的时候,我见过太多人盯着 EXPLAIN 结果里的 key 字段看一眼,发现不是 NULL,就长舒一口气:“索引走了,稳了。” 结果数据量一上来,接口照样超时,数据库 CPU 照样被打满。说实话,只盯着 key 判断索引有没有生效,是新手最容易踩的坑。MySQL 的 EXPLAIN 执行计划,是一张能直接告诉你 SQL 到底怎么跑的表,但它不是只靠一列就能读懂的。这篇文章,我想带你系统地过一遍 EXPLAIN 里的关键列,重点讲清楚一个核心问题:索引为什么失效,以及我们到底该怎么从执行计划里找到真实原因。

这篇文章适合谁看?刚接触索引优化的开发同学,被慢查询折磨过几次的后端工程师,以及想理清执行计划各个字段含义的人。我会结合具体的 SQL 现场和真实排查思路,把那些文档里写得不痛不痒、实际却特别要命的细节都讲透。

1. 先搞清楚 EXPLAIN 到底在“解释”什么

1.1 为什么说“别只盯着 key”

我刚开始用 EXPLAIN 的时候,和很多人一样,只关心 key 这一列。后来遇到一个典型的线上问题:一个订单查询接口,where 条件里有状态字段,也建了联合索引,EXPLAIN 输出里 key 确实显示了那个索引,但查询依然慢得离谱。

后来仔细看才发现,type 显示的是 index,不是 ref,也不是 range。这是什么意思?type=index 表示这个查询扫描了整个索引树,而不是通过索引精确定位到某几行。相当于你有一本字典,不去按拼音查目录,而是把整本字典从头到尾读了一遍,虽然碰巧读的是“目录页”而不是“正文页”,但代价依然很大。问题就出在 where 条件里用了联合索引的非最左列,优化器只能选择全索引扫描来“硬扛”。如果我当时只看 key,这问题永远排查不出来。

所以“别只盯着 key”不是一句口号,而是执行计划分析的第一步:key 只是冰山一角,type、key_len、rows、filtered、Extra 组合起来,才能还原一条 SQL 的真实执行路径。

1.2 EXPLAIN 输出字段速查

EXPLAIN 的结果有很多列,每一列都有自己的诊断价值。我平时最关注下面几张表格里的字段。

字段含义判断要点
id查询中每个 SELECT 子句的序号多表连接时,id 相同从上往下执行;子查询时 id 递增,越大的越先执行
select_typeSELECT 的类型SIMPLE 是简单查询,SUBQUERY、DERIVED、UNION 等需要留意
table当前行访问的是哪张表多表查询时看访问顺序
partitions命中的分区分区表时需要关注
type访问类型核心中的核心,后面详细讲
possible_keys可能用到的索引只是“可能”,不代表一定用
key实际选用的索引只看这一列最容易误判
key_len实际使用的索引长度判断联合索引用到了几列,很有用
ref与索引比较的列或常量显示 const 表示等值常量匹配
rows预估扫描行数估算值,不是实际值
filtered满足条件的行数百分比5.7+ 才有,配合 rows 估算最终返回行数
Extra额外的执行信息这里藏着 Using where、Using index、Using filesort 等关键线索

不要一上来就背字段,而是先建立一个大印象:type 告诉你访问方式,rows 告诉你预估代价,Extra 告诉你有没有额外的排序、临时表、回表操作。三个配合,才能判断一个索引到底用得好不好。

2. type 列:判断索引是否真正生效的关键

2.1 type 取值从好到差怎么排

type 是执行计划里最有信息量的一列,它直接反映 MySQL 访问表数据的方式。按性能从好到差,常见值如下:

type 值含义典型场景
system表只有一行系统表的特殊场景,业务基本见不到
const主键或唯一索引等值查询,最多返回一行where 主键 = 1
eq_ref被驱动表通过唯一索引或主键等值匹配多表 join 中,驱动表每行对应被驱动表一行
ref普通索引等值查询,返回多行where user_id = 100,user_id 有普通索引
range索引范围扫描between、<、>、like 'abc%'
index扫描整个索引树联合索引非最左前缀查找
ALL全表扫描基本等于噩梦

日常优化目标,至少要让 SQL 达到 range 级别,最好能到 ref。如果看到 index 或 ALL,就要警惕,哪怕 key 字段有值。我见过不少 case,key 写着 idx_xxx,慢查询日志里也一样,type 却是 ALL,说明索引根本没被利用起来,可能是函数计算、类型隐式转换、统计信息偏差等问题。

2.2 key_len 与 rows:看懂索引到底“用了几分力”

key_len 是判断联合索引使用程度的好帮手。举个例子,你建了一个联合索引 idx_user_status(user_id, status),其中 user_id 是 BIGINT(8 字节),status 是 TINYINT(1 字节),两个字段都 NOT NULL。那么:

  • 只用到 user_id 一列时,key_len 大约是 8。
  • 两列都用上时,key_len 大约是 9。

也就是说,通过 key_len 的变化,你能精准推断出优化器到底用了联合索引的哪几个字段。如果一个联合索引有三四列,但 EXPLAIN 显示 key_len 只覆盖了第一列,那就得想想 where 条件里是不是没带全。

rows 是优化器估算的扫描行数,filtered 表示根据 where 条件过滤后剩余的比例。两者配合,可以粗估真实返回行数:rows × filtered / 100。如果 rows 很大,filtered 又很低,说明查询扫描了大量行才筛出极少结果,这种情况即使 key 有值,效率也好不到哪去。这一点在慢查询优化里特别常见。

3. 索引失效的常见场景与现场还原

3.1 对索引列做函数运算,优化器直接放弃索引

最典型的失效场景,就是对索引列使用函数。比如一条统计语句:

SELECT * FROM order_info WHERE DATE(create_time) = '2024-01-15';

create_time 明明建了索引,但执行计划通常会显示:

字段值
typeALL
keyNULL
rows约等于全表行数
ExtraUsing where

原因很直接:索引是按原始值排序存储的,DATE(create_time)已经对列里的每个值做了一次函数变换,原本的索引顺序完全打乱了,优化器没法通过 B+ 树直接定位。这就像你把所有人的生日都格式化成“今天是星期几”,然后让系统按星期几去查原始生日,索引根本帮不上忙。

正确写法是改成范围查询:

SELECT * FROM order_info WHERE create_time >= '2024-01-15 00:00:00' AND create_time < '2024-01-16 00:00:00';

这样 create_time 本身没有发生任何运算,优化器就能正常走索引范围扫描。

3.2 隐式类型转换,索引“假装没看见”

隐式类型转换是另一种很容易踩的坑。假设 order_no 是 VARCHAR 类型,查询条件写成:

SELECT * FROM order_info WHERE order_no = 123456;

MySQL 会把字符串列和数字进行比较时,尝试把字符串转成数字,或者把数字转成字符串。关键在于:如果转换发生在索引列这一侧,索引就失效了。执行计划会从 key=uk_order_no 变成 key=NULL,type=ALL。

这也是为什么我经常建议:写 SQL 的时候,参数类型一定要和字段类型保持一致。ORM 框架有时候会自动处理类型,但处理不完全可靠,尤其在拼接原生 SQL 时,类型问题只能靠人眼盯出来。排查的时候,用 SHOW WARNINGS 能看到优化器重写后的查询,那里面有非常明显的结果。

提示:在 EXPLAIN 之后执行 SHOW WARNINGS,可以看到 MySQL 优化后的 SQL 文本,能帮你快速发现隐式转换这类问题。

3.3 联合索引不满足最左前缀,等于白建

联合索引是业务里最常用也最容易被理解错的索引。假设我建了 idx_user_status(user_id, status),那么查询:

SELECT * FROM order_info WHERE status = 1;

大概率走不了这个索引——因为联合索引的排序规则是先按 user_id 排,再按 status 排。没有 user_id 的条件,SQL 根本无法快速定位到指定 status 所在的索引区间。之前的实操里,这种查询 EXPLAIN 后可能出现 type=index、Extra 带 Using where;如果查询列恰好都在索引里,甚至会出现 Using index,但它仍然是整个索引树全扫。

理解最左前缀,只需要记住一个类比:联合索引像一本先按“姓氏”排、再按“名字”排的通讯录。你想找所有名字叫“伟”的人,却不让系统先用姓氏定位,就得把整本通讯录翻一遍。

3.4 模糊查询、OR、范围条件等特殊场景

除了函数运算和隐式类型转换,索引失效还高发在这些场景:

场景失效原因处理建议
LIKE '%关键词'前导通配符导致无法按索引前缀匹配改成 LIKE '关键词%',或考虑全文索引
OR 连接非索引列OR 两侧条件无法合并扫描路径改写为 UNION,或给 OR 两侧都建索引
范围查询之后接等值条件联合索引中 range 后续列无法用于过滤把范围字段放在联合索引最后
IS NOT NULL / NOT IN优化器往往认为走索引回表代价太高结合数据分布判断,必要时改写法

这里面'%关键词'的问题我反复强调:B+ 树索引天然支持前缀匹配,不支持中缀和后缀匹配。好比字典的拼音目录,你能按“bei”快速翻到“北京”,却不能直接按“京”字翻出所有含“京”的词条,除非另建一个倒排索引。

4. 实操演示:从建表到执行计划全解读

4.1 建表与造数

纸上谈兵没用,我们实际建一张表,把上面说的现象都复现一遍。表结构如下:

CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, KEY idx_user_status (user_id, status), KEY idx_create_time (create_time), UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为了模拟真实场景,我写了一个存储过程批量插入约 50 万行数据,数据和统计信息都不算太干净,这更贴近线上实际情况。夜深人静的时候跑一下,大概几十秒。

DELIMITER $$ CREATE PROCEDURE init_order_info() BEGIN DECLARE i INT DEFAULT 1; WHILE i <= 500000 DO INSERT INTO order_info(order_no, user_id, goods_id, status, create_time) VALUES ( CONCAT('NO', LPAD(i, 10, '0')), i % 5000, i % 1000, i % 3, DATE_SUB(NOW(), INTERVAL (i % 365) DAY) ); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL init_order_info();

4.2 四条 SQL 的 EXPLAIN 结果解读

SQL 1:联合索引正常命中

EXPLAIN SELECT * FROM order_info WHERE user_id = 12345 AND status = 1;

预期输出:

typekeykey_lenrowsExtra
refidx_user_status9约 30无临时表,无额外排序

这里 key_len=9,说明 user_id(8 字节)和 status(1 字节)两列都用到了,属于联合索引的完整使用方式。ref 列会显示 const,const。这个 SQL 是模范生。

SQL 2:函数导致索引失效

EXPLAIN SELECT * FROM order_info WHERE DATE(create_time) = '2024-01-15';

预期输出:

typekeykey_lenrowsExtra
ALLNULLNULL约 50 万Using where

这个例子就是典型的“看着字段有索引,实际全表扫描”。只要 create_time 被 DATE() 包裹,索引就形同虚设。修法前面说过了,用区间查询替代函数。

SQL 3:联合索引不满足最左前缀

EXPLAIN SELECT user_id, status FROM order_info WHERE status = 1;

预期输出:

typekeykey_lenrowsExtra
indexidx_user_status9约 50 万Using where; Using index

这里很有意思:key 有值、key_len 还很完整,甚至 Extra 出现了 Using index,看起来是很完美的执行计划。但 type=index,意思是扫描了整棵索引树。因为我查询列恰好只有 user_id 和 status,都在联合索引里,所以不需要回表,可代价依然是全索引扫描。如果这张表有千万级数据,这种 SQL 同样能把资源耗尽。这就是“别只盯着 key”的最有力实证。

SQL 4:优化器放弃索引

EXPLAIN SELECT * FROM order_info WHERE user_id > 0;

预期输出:

typekeykey_lenrowsExtra
ALLNULLNULL约 50 万Using where

这不是写错了,而是优化器故意选择全表扫描。user_id > 0 几乎能命中全表所有记录,如果走 idx_user_status 索引再回表,代价比全表扫描高得多。执行计划的生成依据是成本模型,遇到这样的条件,索引确实“没被选择”,但本质上不是索引失效,而是优化器认为“用了更亏”。这种场景需要结合业务实际考虑,比如按 user_id 分页是否合理、是否需要加其他过滤条件。

4.3 EXPLAIN ANALYZE 进一步验证

传统 EXPLAIN 输出的是估算值,不是实际执行数据。MySQL 8.0.18+ 提供了 EXPLAIN ANALYZE,可以直接看真实执行时间和循环次数:

EXPLAIN ANALYZE SELECT * FROM order_info WHERE user_id = 12345 AND status = 1;

输出会带着 actual time、rows、loops 等真实数据。我在定位慢 SQL 时,经常先用 EXPLAIN 看结构,再用 EXPLAIN ANALYZE 确认实际行数和耗时,两者差距大时,多半是统计信息不准,需要 ANALYZE TABLE 刷新。

5. 常见误判与排查技巧实录

5.1 三个最常见的误判

第一个误判:key 是 NULL 就认为没救了。其实 key 为 NULL 只是说明这步执行没选索引,不代表没有可用索引,可能是函数运算、类型隐式转换,也可能是优化器觉得全表扫描更划算。此时应该先看 type 和 Extra。

第二个误判:key 有值就认为索引生效了。这个前面反复说过,type=index 就是反例。还有一种情况,联合索引只用了前缀部分,key_len 很短,说明后面的字段没用上,索引生效程度也要打个问号。

第三个误判:rows 小就认为快。rows 是优化器的估算值,不是精确值,而且它没算上回表代价。如果 Extra 里没有 Using index,说明还要回表,回表次数多了,即使 rows 看起来不大,实际查询也很慢。

我把几个关键结论整理成速查表:

现象可能原因排查方向
key=NULL + type=ALL函数运算、隐式转换、统计信息偏差SHOW WARNINGS,调整 SQL
key有值 + type=index整树扫描,联合索引最左前缀缺失调整查询条件或索引顺序
key_len 异常偏短联合索引只用到前缀列检查 where 条件是否包含后续列
rows 很大 + filtered 很低先大范围筛选再小比例过滤考虑增加更精准的过滤列
Extra 出现 Using filesort排序字段未能使用索引顺序让 order by 字段与索引顺序一致

5.2 排查索引失效的实操流程

我在线上排查慢查询时,有一套相对固定的流程,分享出来供参考。

第一步,先拿到慢 SQL,用 EXPLAIN 看 type 级别。如果 type 是 ALL 或 index,进入第二步;如果是 range 或更好,再看 Extra 里的其他信息。

第二步,看 key 和 key_len。key 是否为 NULL,key_len 是否符合预期,判断联合索引是否用全。这里要熟悉自己的表结构和索引设计,能通过 key_len 快速判断。

第三步,看 Extra 里的额外操作。Using filesort 意味着排序没走索引,需要检查 order by 字段是否在索引中;Using temporary 意味着临时表参与运算,对大结果集非常致命;Using index condition 则说明用上了索引条件下推,通常不算坏消息。

第四步,用 SHOW WARNINGS 看优化器重写后的 SQL,确认有没有隐式类型转换、子查询改写之类的隐性操作。

第五步,如果 SQL 看着没问题,但优化器就是选了全表扫描,可以考虑刷新统计信息:

ANALYZE TABLE order_info;

如果刷新后依然如此,可能是数据分布太偏,优化器基于成本模型做出的判断确实是有道理的,此时别死磕索引,应该考虑改写 SQL 或调整索引设计。

5.3 平时容易忽略的细节

有几个细节,属于平时很容易忽略、但坑很深的点。

一是字符集和排序规则不一致。如果两张表 join 的字段一个是 utf8mb4,一个是 gbk,MySQL 需要做字符集转换,这个转换会阻止索引使用。建表和设计字段时就要统一字符集。

二是统计信息过期。大批量删改数据后,统计信息不会实时更新,优化器可能拿着旧的估算值做出错误决策。定期 ANALYZE TABLE 是非常有必要的。

三是范围查询在联合索引中的位置。联合索引里,范围条件后面的字段无法参与索引过滤。比如 idx(a, b, c),where a=1 and b>10 and c=2,c 的过滤条件在索引定位时用不上,只能通过回表后的 where 再过滤。设计联合索引时,尽量把范围查询的字段放在靠后的位置。

四是关于 order by 的隐藏索引需求。order by 字段如果不在索引中,就会出现 Using filesort。有时候查询本身不慢,但加上 order by 之后突然变慢,多半是排序没有索引可用。解决方式之一是让 order by 字段与现有联合索引顺序匹配。

我个人在实际操作中的体会是:EXPLAIN 不是用来“应付面试”的,而是用来做决策的。拿到一条慢 SQL,先把执行计划打印出来,逐列看一遍,再对照索引定义推演一遍 B+ 树的查找路径,基本就能找到问题症结。这种方法论,比记住一百个“索引失效例子”有用得多。

最后再分享一个实用习惯:每次优化完 SQL,我会对比优化前后两次 EXPLAIN 的结果,重点看 type 是否提升、rows 是否明显下降、Extra 是否少了 filesort。如果这三项没有明显好转,那这个优化方案多半是无效的,不要自我安慰。慢 SQL 优化这条路,说到底就是不断用执行计划验证假设的过程,坚持下来,你也会形成自己的排查直觉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:33:56

UltraEdit查看文本编码类型:用TaoToken统一Key排查乱码与编码识别

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:33:44

UG/NX二次开发:坐标系创建与WCS设置避坑指南

在UG/NX二次开发里&#xff0c;“创建坐标系、设置工作坐标系”看着像是最基础不过的两句话&#xff0c;但真上手做项目时你会发现&#xff0c;这里面的坑比想象中多得多。我在实际开发里遇到过不少次&#xff1a;坐标建出来了、WCS也切过去了&#xff0c;结果批量导出的点位数…

作者头像 李华
网站建设 2026/10/17 5:32:59

PostgreSQL连接全解析:从连接池到pg_hba.conf的排查实践

1. 从一条报错说起&#xff1a;连接问题到底卡在哪先讲个我自己的经历。有次线上系统半夜报警&#xff0c;业务方反馈"数据库连不上了"&#xff0c;我登上服务器看了一眼日志&#xff0c;满屏都是FATAL: sorry, too many clients already。当时的第一反应是查max_con…

作者头像 李华
网站建设 2026/10/17 5:32:12

2026前沿科技盘点:AI原生、量子纠错与具身智能的跃迁

每年年初我都会做一轮跨领域的技术盘点&#xff0c;四年下来&#xff0c;2026年这轮的视角和以往明显不同。过去聊AI&#xff0c;核心是模型参数、算力规模这些“军备竞赛”指标&#xff1b;过去聊量子计算&#xff0c;核心是比特数量、保真度这类“物理器件进度条”。而今年围…

作者头像 李华
网站建设 2026/10/17 5:31:28

AutoML如何落地?自动化机器学习实战与避坑指南

如果说传统机器学习建模像做一顿饭——买菜、洗菜、切菜、配菜、掌握火候全得亲力亲为&#xff0c;那AutoML想做的就是从"全程手作"变成"预制菜加开放厨房"&#xff0c;把人从重复劳动里捞出来&#xff0c;把精力留给真正需要判断力的事情。AutoML这个自动…

作者头像 李华