SQL语句执行变慢,排查的第一步基本都是请出EXPLAIN。但这条命令的输出是一张包含十几个字段的表格,如果只看其中一两个字段,很容易得出错误结论。比如看到type是ref就以为没问题,结果Extra里赫然写着Using filesort;或者看到key列有值就以为索引生效了,实际却因为索引选择性太差反而更慢。这篇文章把EXPLAIN输出中的每个字段都详细过一遍,配合实际例子说明每个取值的含义,以及背后对应的优化思路。

id与select_type:看懂查询的执行结构
id列标识SELECT所属的执行单元。简单查询只有一行,id为1;包含子查询或UNION时会出现多行。id相同的行属于同一组,从上往下顺序执行;id不同时,数值越大的越先执行,这一点在读执行计划时非常关键。比如外层查询id为1,子查询id为2,那么子查询的结果会先算出来,再交给外层使用。
select_type说明这一行对应的是什么类型的查询。SIMPLE表示不包含子查询和UNION的简单查询;PRIMARY表示外层查询;SUBQUERY是子查询中第一个SELECT;DERIVED表示派生表,也就是FROM子句里的子查询;UNCACHEABLE SUBQUERY出现在子查询引用了外层字段无法缓存的场景;UNION RESULT则是UNION合并结果的标记。理解这些标记有助于判断子查询有没有被优化器改写。比如MySQL 5.6及以后,很多DERIVED查询会被下推或者合并,执行计划里可能就不再单独出现派生表行。
table列显示这一行访问的是哪个表,可能是真实表名,也可能是别名或派生表的临时命名,形如<derived2>这样的写法表示它来自id为2的派生查询结果。
type字段:访问类型的完整等级
type是整个执行计划里最重要的字段之一,它描述了MySQL找到目标行的方式,性能从好到差大致排序为:system、const、eq_ref、ref、range、index、ALL。system和const只在表最多有一行匹配时出现,典型场景是主键等值查询,例如WHERE id = 100,这类查询几乎是瞬时的。
eq_ref出现在多表关联中,被驱动表通过主键或唯一索引与驱动表关联,每个驱动行最多匹配一行,这是关联查询能达到的最好状态。ref则是通过普通二级索引的等值匹配,可能命中多行,比如WHERE status = 1配合status索引。range是索引范围扫描,BETWEEN、大于小于、IN走的基本都是这种方式。
index值得特别注意,它的名字容易让人误解。它表示扫描整个索引树,虽然是走索引,但不是点查,本质上仍是全扫描,只是扫描的体量比整张表小(覆盖了所需列时可以不回表)。ALL就是最差的情况,全表扫描。一般来说,OLTP场景的查询至少要达到range级别,出现ALL通常意味着需要补充索引或调整写法。可以通过一个例子直观对比:
-- 建立测试表 CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id INT NOT NULL, status TINYINT NOT NULL, created_at DATETIME NOT NULL, KEY idx_user (user_id), KEY idx_status_created (status, created_at) ); EXPLAIN SELECT * FROM orders WHERE id = 100; -- type = const,主键等值查询,最优 EXPLAIN SELECT * FROM orders WHERE user_id = 88; -- type = ref,走了idx_user索引 EXPLAIN SELECT * FROM orders WHERE created_at > '2024-01-01'; -- type = ALL,无法单独使用created_at,因为它是联合索引的第二列 EXPLAIN SELECT * FROM orders WHERE status = 1 AND created_at > '2024-01-01'; -- type = range,联合索引两列都能用上
上面的第三个例子说明了最左前缀原则:idx_status_created的列顺序决定了单独按created_at过滤无法走这个索引,type退化为ALL。这正是通过EXPLAIN发现索引设计问题的典型场景。
possible_keys、key、key_len与ref:索引使用的细节
possible_keys列出优化器认为可能使用的索引,key是实际选择的索引。possible_keys为空而key有值是正常的,说明优化器自己找到了可用索引;反过来possible_keys有值但key为NULL,说明优化器评估后放弃走索引,常见原因是索引选择性太差或者预估回表成本高于全表扫描。key为NULL且type为ALL时就要重点排查了。
key_len显示实际使用的索引字节数,这是判断联合索引到底用了几列的重要依据。以idx_status_created为例,TINYINT占1字节,DATETIME占5字节,如果key_len等于1,说明只用到了status一列;等于6左右才说明两列都用上了。NULL列还要额外加1字节。通过key_len可以验证最左前缀用到了哪一列,判断WHERE条件有没有被索引完全消化。
ref列显示与索引列做比较的对象,可能是常量const,也可能是另一个表的某个字段,比如ref列显示test.orders.user_id,就表示关联条件是拿user_id去匹配索引。如果ref显示的是func,说明经过了函数运算,通常需要检查写法是否阻止了索引直接匹配。
rows、filtered与Extra:成本估算与附加信息
rows是优化器预估需要扫描的行数,注意这是估算值,基于统计信息得出,可能不准,可以用ANALYZE TABLE更新统计信息。rows与实际行数偏差很大时,优化器可能做出错误的连接顺序选择。filtered表示经过表条件过滤后剩余行数的百分比,关联查询中这个值乘以rows决定了传给下一张表的数据量,两个值相乘才是预估的最终扫描成本。
Extra是附加信息的大杂烩,但往往藏着最关键的问题线索。Using index表示覆盖索引,查询所需列全部在索引里,无需回表,这是好状态;Using index condition表示索引条件下推ICP,也是正面的。需要警惕的是Using filesort和Using temporary,前者说明排序无法利用索引顺序,需要在内存或磁盘额外排序,后者说明用到了临时表,常见于GROUP BY和DISTINCT没有命中索引的场景。出现这两个提示时,考虑给ORDER BY或GROUP BY的列建立合适的联合索引通常能解决。
-- 出现Using filesort的例子 EXPLAIN SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC; -- 如果索引是 idx_created(status, created_at) 的顺序 -- 排序可以直接利用索引顺序,Extra变为Using index -- 若索引列顺序反了,排序就需要额外的filesort -- Using temporary的典型场景 EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status; -- status无索引时会提示Using temporary; Using filesort
另外还有几个常见提示:Using where表示存储引擎返回后再经过WHERE条件过滤;Using join buffer (Block Nested Loop)表示被驱动表关联字段没有索引,用了连接缓冲,出现它基本要给关联字段加索引;Impossible WHERE表示条件永远为假,比如id = NULL;Using union提示出现在index merge场景,说明优化器合并了多个索引的结果,有时index merge效率反而不高,可以考虑改写SQL引导走单一索引。
结合实际排查的思路总结
拿到EXPLAIN结果后,建议按固定顺序过一遍:先看type确认访问级别,再看key和key_len确认索引利用情况,然后看rows和filtered估算扫描量,最后细读Extra找filesort、temporary、join buffer这些负面信号。这个顺序能覆盖绝大多数慢查询问题。
还要提醒两点。一是EXPLAIN展示的是优化器基于成本估算的执行方案,统计信息过期会导致误判,必要时配合ANALYZE TABLE或直接看慢日志实际耗时;二是MySQL 8.0提供了EXPLAIN ANALYZE,会真正执行语句并输出每一步的实际耗时和行数,预估与实际对比一目了然,排查rows估算偏差问题时特别好用。把EXPLAIN各字段吃透,再配合这些工具,SQL调优就不再是凭感觉的事情了。