EXPLAIN 是 MySQL 提供给开发者最实用的性能诊断工具,没有之一。它不关心你的 SQL 写了多长、嵌套了多少层,只回答一句:“数据库准备怎么执行你这条语句?” 把 EXPLAIN 放到任意 SELECT、INSERT、UPDATE、DELETE 前执行,就能获得一张包含多个字段的执行计划表。读懂它,是区分“会写 SQL”和“能写好 SQL”的分水岭。
11.2.1 执行计划整体结构
一条 SQL 语句在 MySQL 中通常会被拆解成若干个基本步骤执行,如先扫描索引 A、再回表读取数据行、然后与另一张表的扫描结果做连接。EXPLAIN 输出的每一行,代表计划中的一个步骤,从上到下大致对应执行顺序。字段包括:
id:步骤编号select_type:查询类型table:操作的表名partitions:匹配的分区type:访问类型(最重要)possible_keys:可能使用的索引key:实际选择的索引key_len:使用的索引长度ref:索引比较的对象rows:优化器估算的扫描行数filtered:按条件过滤后的行数百分比Extra:额外信息(非常重要)
下面逐一拆解每个字段,附带实战解读。
11.2.2 核心字段详解
id:步骤标识
- 相同
id表示从上到下顺序执行。 id值越大,优先级越高,越先执行(常用于子查询)。- 对于
UNION结果集合并,id为NULL,表示临时表操作。
select_type:查询类型
表示每个步骤属于哪一类查询,常见值有:
SIMPLE:简单查询,不包含子查询或UNION。PRIMARY:最外层查询。SUBQUERY:SELECT中的子查询(不在FROM后)。DERIVED:FROM后的子查询(派生表)。UNION:UNION中的第二个或之后的查询。UNION RESULT:UNION的合并结果。
这些信息帮助理解多表、多层查询的内部执行顺序。
type:访问类型(性能核心指标)
type 描述 MySQL 如何查找数据行,是判断 SQL 是否高效的核心指标。性能从优到劣大致排序为:
NULL > system > const > eq_ref > ref > range > index > ALL
理解每种类型的真实含义与出现条件至关重要:
NULL:优化阶段就能确定结果,无需访问表或索引。例如SELECT 1;或从常量表获取聚合函数值。system:表只有一行数据(系统表),是const的特例,极少出现。const:通过主键或唯一索引等值匹配一条记录,速度极快。例如WHERE id = 1。查询在优化阶段即可完成大部分工作。eq_ref:连接查询时,驱动表的每一行,在被驱动表中使用主键或唯一索引进行等值匹配,返回至多一行。这是联表查询中最好的访问方式。ref:使用非唯一索引进行等值匹配,可能返回多行。例如WHERE category_id = 10且该列有索引。这是最常见的“好”访问类型。range:索引范围扫描,如>、<、BETWEEN、IN等。性能尚可,但扫描行数通常多于ref。注意,IN如果值太多可能会退化为全表扫描。index:全索引扫描,即扫描整个索引树。虽然它比ALL(全表扫描)稍好(因为索引通常比数据文件小),但如果出现在核心查询中仍需警惕。常见于ORDER BY或覆盖索引的完整扫描。ALL:全表扫描,逐行读取数据页,是性能最差的访问方式,必须尽可能避免。
possible_keys 与 key:候选索引与实际选择
possible_keys:优化器评估后认为可能会用到的索引列表。如果为NULL,说明你的查询条件未能匹配任何索引,这是一个危险信号。key:优化器最终实际选用的索引。如果possible_keys有值而key为NULL,说明优化器发现走全表扫描成本更低,这种情况通常需要人工干预。
key_len:索引使用的长度
表示优化器实际使用了索引的多少字节。对于联合索引,key_len 可以明确反映出查询使用了索引的哪几列。例如,一个联合索引 (col1 INT, col2 VARCHAR(20)),如果条件中只用到 col1,key_len 为 4;如果同时用到 col1 和 col2 完全匹配,则 key_len 会大得多。该字段常用于验证“联合索引最左前缀”是否真正生效。
ref:索引比较的对象
显示索引值与什么进行比较,常见值有 const(常量)或另一个表的列。例如 WHERE t1.col = 'abc' 会显示 const;而连接条件如 t1.id = t2.user_id 会显示 db.t2.user_id。
rows:优化器估算的扫描行数
这是优化器基于统计信息预估的每个步骤需要读取的行数。注意:它不是实际执行后的准确值,而是索引统计信息(innodb_stats_persistent)基础上的估算。 如果预估与实际差异巨大(例如千万行表却显示几十行),很可能是统计信息过期,需要执行 ANALYZE TABLE 更新。
filtered:过滤百分比
表示经过表条件过滤后,剩余满足条件的数据比例(百分比)。例如 rows=100,filtered=10.00,意味着最终大约有 10 行参与下一步操作。这个字段在多表连接时尤其有用,能够反映驱动表过滤效果好坏。
Extra:额外关键信息(必须逐字阅读)
Extra 字段包含大量细节,对排查问题极为关键:
Using index:覆盖索引,查询所需数据全部从索引中获取,无需回表。这是高性能查询的标志,应尽量追求。Using where:表示 MySQL 在存储引擎返回数据后,还额外进行了WHERE条件过滤。通常出现在某些条件无法完全使用索引时,如范围查询后的额外AND条件。Using index condition:索引下推(ICP),将部分WHERE条件下推到引擎层,先过滤再回表,减少随机 I/O。MySQL 5.6+ 默认开启,这是非常有益的特性。Using temporary:查询过程中需要创建临时表来存放中间结果,常见于GROUP BY、DISTINCT、UNION等操作。出现这个信息时要检查是否可以加索引避免临时表。Using filesort:MySQL 需要进行额外的排序操作,无法利用索引的顺序直接返回结果。当ORDER BY不能借助索引有序性时出现。如果排序数据量大,会对性能造成较大影响,应尽量优化成Using index替代。Using join buffer:连接时使用了连接缓冲(如 Block Nested Loop),通常意味着被驱动表没有合适的索引,每行驱动表数据都要做全表扫描。Impossible WHERE:WHERE条件永远为假,优化器直接返回空结果。No tables used:查询不涉及任何表。
11.2.3 实战解读示例
假设有两张表:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
INDEX idx_name (name)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
INDEX idx_user (user_id)
);
执行以下查询并分析:
EXPLAIN
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.name = 'Alice';
执行计划可能如下:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|----|-------------|-------|-------|---------------|----------|---------|-------|------|----------|-------------|
| 1 | SIMPLE | u | ref | idx_name | idx_name | 203 | const | 1 | 100.00 | Using where |
| 1 | SIMPLE | o | ref | idx_user | idx_user | 5 | db.u.id | 15 | 100.00 | NULL |
逐行解读:
- 先查
users表,使用idx_name索引通过name='Alice'等值匹配,type=ref,预估扫描 1 行,性能良好。 - 然后将
u.id传给orders表,orders使用idx_user索引进行等值匹配,type=ref,预估每个用户有 15 个订单,性能同样不错。 - 整体没有
Using filesort或Using temporary,查询质量高。
如果执行计划中出现 orders 表 type=ALL,Extra 显示 Using join buffer,则说明 orders.user_id 可能没有索引(或未被使用),这时就需要检查索引是否存在。
11.2.4 常见风险执行计划与应对
type=ALL:全表扫描,立刻检查possible_keys是否为空,是否需要创建合适的索引。Extra=Using filesort:检查ORDER BY的列是否在索引中,并且顺序符合最左前缀原则。Extra=Using temporary:检查GROUP BY、DISTINCT是否可以利用索引,或者DISTINCT与ORDER BY混合导致无法优化。key_len小于预期:联合索引只用了一半,条件列不符合最左前缀,往往需要调整查询条件顺序或索引列顺序。rows估算严重不准:执行ANALYZE TABLE更新统计信息,或者考虑调整innodb_stats_persistent_sample_pages采样页数。
EXPLAIN 并不是只能看一遍,对于慢查询,可以先 EXPLAIN 查看优化器的默认选择,然后尝试 EXPLAIN FORMAT=JSON 获取更多成本细节,或者用 EXPLAIN ANALYZE(MySQL 8.0.18+)实际执行并测量每一步的消耗时间,彻底锁定瓶颈。掌握这些字段的解读,你就拥有了诊断 SQL 性能的核心能力。