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_ref | JOIN 时用被驱动表的主键/唯一索引,最多匹配一行 | 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 buffer | JOIN 没吃到索引,驱动表在 buffer 里逐块匹配被驱动表。给关联字段加索引。 |
回应开头的问题:Using index 是覆盖索引(整个查询不回表),Using index condition 是索引下推(还是要回表,只是回表次数变少)——名字像,完全两码事。
8.0 彩蛋:EXPLAIN ANALYZE
EXPLAIN 给的是预估,MySQL 8.0.18 起多了一个 EXPLAIN ANALYZE:真的执行一遍,返回每一步的实际耗时和实际行数。预估与实际偏差大时,说明统计信息该更新了(ANALYZE TABLE)——这正是优化器篇的伏笔。
到这,EXPLAIN 的证词读全了。但还有个根本问题没回答:索引凭什么让 381 万行变 42 行?它的数据结构长什么样、为什么偏偏是 B+ 树?下一篇从页和扇出讲起。
咖啡凉了,记得趁热喝。
评论 (0)