连载中 2/22

EXPLAIN 精读:type 等级、rows 与 Extra 的暗语

2026-05-01 · 5120 阅读 · 0 评论 · 0 赞

EXPLAIN 的每个字段都是证词

上一篇我们用 EXPLAIN 抓住了那条 8 秒的慢 SQL,但只挑了五个列粗讲。评论区有人追问:Using index 和 Using index condition 到底差在哪?key_len 那串数字怎么读?这篇就把执行计划表逐列精读——EXPLAIN 的每一列都是执行引擎的证词,会读证词才能断案。

id 与 select_type:谁先执行

多表 JOIN 和子查询的执行计划会有多行,先看谁先跑:id 相同的行从上往下执行;id 不同的,数字大的先执行(子查询先跑)。select_type 标记每行的角色:SIMPLE 是无子查询的普通查询,PRIMARY 是最外层,DERIVED 是派生表(FROM 里的子查询物化出来的临时表),DEPENDENT SUBQUERY 是依赖外层的子查询——最后这个要格外留神,它可能对外层的每一行都执行一次。

type:七档访问等级

type 是执行计划里最先看的列,按性能从好到差分档:

type含义典型场景
const主键或唯一索引等值查询,最多一行WHERE id = 9527
eq_refJOIN 时用被驱动表的主键/唯一索引,最多匹配一行JOIN users u ON o.user_id = u.id
ref普通索引等值查询,可能多行WHERE user_id = 9527
range索引范围扫描WHERE create_time > 某时刻
index扫整棵索引树,比 ALL 好在不用回表但数据没少扫SELECT count(1) 全索引扫
ALL全表扫描没有可用索引

经验线:线上查询至少要到 range,最好 ref 及以上;core 链路上的 SQL 出现 ALL,基本可以直接立个案。补充一个冷知识:system 是 const 的特例(表只有一行),实际几乎见不到。

key_len:联合索引用到了第几列

key_len 最容易被忽略,却最有用——它告诉你联合索引实际用了几列。算式很简单:列的基础长度 + 1 字节 NULL 标记(列允许 NULL 才有)+ 变长列的 1-2 字节长度前缀。

  • bigint:8 字节;int:4 字节。
  • varchar(n) utf8mb4:4n + 2(长度前缀)+ 1(NULL 标记,可空才有)。varchar(50) 可空列就是 4×50+2+1 = 203 字节。
  • datetime:5 字节 + 小数秒精度,datetime(0) 不带 NULL 标记就是 5,datetime(3) 是 8。

拿上一篇的 idx_user_status_time (user_id, status, create_time) 验算:user_id bigint not null = 8,status tinyint not null = 1,create_time datetime(3) not null = 8(5+3)。计划里 key_len 显示 9,就是只用到 user_id + status 两列;显示 17 才是三列全用上。ORDER BY 能否免排序,取决于联合索引用到哪一列——这正是下一篇最左前缀的主题。

rows 与 filtered:成本估算

rows 是优化器估算的扫描行数,基于统计信息,不是精确值(下一篇讲它什么时候不准)。filtered 是这批扫描结果里预计满足剩余条件的百分比:rows=381 万、filtered=0.001,意味着最终约 42 行。rows × filtered 就是驱动下一张表(JOIN)的行数,JOIN 越靠前的表被扫描越多——小表驱动大表说的就是让 rows 小的表当驱动表。

Extra:附加动作的暗语

取值含义与对策
Using index覆盖索引:查询的列全在索引里,不用回表。好信号。
Using index condition索引下推(ICP):把 WHERE 条件下推到引擎层在索引上先过滤,减少回表次数。5.6+ 的优化。
Using where服务层过滤。出现在 range 之后属正常,出现在 ALL 上就要留意。
Using filesort需要额外排序(内存装不下还会落盘)。ORDER BY 没吃到索引有序性,重点优化对象。
Using temporary用临时表处理(常见于 GROUP BY、DISTINCT 没吃到索引)。比 filesort 更糟。
Using join bufferJOIN 没吃到索引,驱动表在 buffer 里逐块匹配被驱动表。给关联字段加索引。

回应开头的问题:Using index 是覆盖索引(整个查询不回表),Using index condition 是索引下推(还是要回表,只是回表次数变少)——名字像,完全两码事。

8.0 彩蛋:EXPLAIN ANALYZE

EXPLAIN 给的是预估,MySQL 8.0.18 起多了一个 EXPLAIN ANALYZE:真的执行一遍,返回每一步的实际耗时和实际行数。预估与实际偏差大时,说明统计信息该更新了(ANALYZE TABLE)——这正是优化器篇的伏笔。

到这,EXPLAIN 的证词读全了。但还有个根本问题没回答:索引凭什么让 381 万行变 42 行?它的数据结构长什么样、为什么偏偏是 B+ 树?下一篇从页和扇出讲起。

咖啡凉了,记得趁热喝。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#MySQL#EXPLAIN#执行计划#索引#性能优化

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞