MySQL 性能分析神器:一文读懂 EXPLAIN 执行计划
开篇引子: 线上系统突然告警,用户反馈操作卡顿?DBA一看监控,CPU 使用率飙升,慢查询日志里全是"罪魁祸首"?别慌,这背后往往藏着一个被忽视的 SQL 语句。掌握
EXPLAIN这把"瑞士军刀",你就能化身数据库侦探,精准定位性能瓶颈。本文将带你从零开始,彻底搞懂 MySQL 的执行计划,让你的 SQL 优化之路畅通无阻!
你将收获什么?
读完这篇文章,你将能够:
- 深入理解
EXPLAIN命令输出的每一个字段及其深层含义。 - 熟练解读 典型的执行计划,快速判断 SQL 的好坏。
- 学以致用,通过实际案例,掌握常见的 SQL 性能问题诊断与优化方法。
- 从容应对 技术面试中关于 MySQL 执行计划的高频问题。
1. EXPLAIN 简介:SQL 的"体检报告"
EXPLAIN 是 MySQL 提供的一个非常实用的命令,它能模拟优化器执行 SQL 语句,并返回一个详细的报告。这份报告就像 SQL 的"体检报告",告诉我们这条 SQL 语句是如何被执行的,它扫描了多少行数据,是否使用了索引,以及查询的关联方式等关键信息。通过解读这份报告,我们就能知道 SQL 是否高效,瓶颈在哪里,从而进行针对性的优化。
如何使用?
在你的 SELECT 语句前面加上 EXPLAIN 关键字即可。例如:
sql
EXPLAIN SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 'active';
执行后,你会得到一张包含多个字段的表格,接下来我们逐一解读。
2. 核心字段解读:逐个击破
EXPLAIN 的输出结果包含了多个字段,每个字段都提供了关于查询执行的不同侧面的信息。我们重点关注以下几个核心字段:
id: 查询序列号
id 用来标识查询中执行的 SELECT 语句的顺序。它有以下几种情况:
- 相同
id,执行顺序从上到下: 当 SQL 中有多个简单的查询(如UNION之外的多表 JOIN)时,它们的id相同。优化器会按照从上到下的顺序执行。 - 不同
id,id值越大越先执行: 在包含子查询的 SQL 中,最里层的子查询id值最大,最先被执行。外层查询的id值依次递减。
select_type: 查询类型
这个字段告诉我们当前这一行的查询是属于哪种类型的查询。
SIMPLE: 最简单的 SELECT 查询,不包含 UNION 或子查询。PRIMARY: 包含复杂查询时,最外层的 SELECT 会被标记为 PRIMARY。SUBQUERY: 在 SELECT 或 WHERE 列表中包含的子查询。DERIVED: 在 FROM 列表中包含的子查询,MySQL 会将结果物化(即创建一个临时表)。UNION: 在 UNION 语句中的第二个或后面的 SELECT 语句。UNION RESULT: UNION 查询的结果。
table: 表名
这一列显示了当前这一行正在访问的表名。在多表 JOIN 的情况下,这里会显示出每次关联操作涉及的具体表。
partitions (分区)
如果查询的表是分区表,这里会显示命中的分区。对于非分区表,该列为空。
type: 关联类型(最重要的性能指标 ⭐⭐⭐)
type 列是判断查询性能好坏的最关键字段之一。它表示 MySQL 在表中找到所需行的方式,或者说访问类型。从最优到最差的排序如下:
-
system: 表只有一行(等于系统表),这是const类型的特例。 -
const: 通过主键或唯一索引(UNIQUE KEY)进行等值查询,最多只返回一行数据。因为只匹配一行,所以速度非常快。sql-- 假设 id 是主键 EXPLAIN SELECT * FROM users WHERE id = 1; -- type: const -
eq_ref: 在多表 JOIN 中,对于前一个表的每一行,在当前表中通过主键或唯一索引(UNIQUE KEY)最多只匹配一行。这是高效的 JOIN 方式。sql-- 假设 order.user_id 是一个唯一索引 EXPLAIN SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id; -- type for 'u': eq_ref -
ref: 非唯一索引扫描,返回匹配某个单个值的所有行。也是常见的高效 JOIN 方式。sqlEXPLAIN SELECT * FROM users WHERE user_name = 'John'; -- type: ref -
range: 使用索引获取范围值,例如BETWEEN,IN,>,<等操作。sqlEXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- type: range -
index: 索引全扫描,遍历整个索引来查找匹配的行。虽然只扫描索引,但如果索引很大,性能也可能不佳。 -
ALL: 全表扫描! 这是最糟糕的情况,MySQL 会扫描整张表来找到匹配的行。优化时,必须坚决避免ALL类型。
possible_keys: 可能用到的索引
这一列显示了 MySQL 在查询时,理论上可以使用哪些索引来提高效率。如果该列为 NULL,则表示没有相关的索引。这种情况需要考虑为相关列创建索引。
key: 实际使用的索引
这一列显示了 MySQL 在执行查询时,实际 选用的索引。如果为 NULL,则表示没有使用任何索引。
key_len: 索引长度
key_len 表示 MySQL 在索引中实际使用的字节数。通过这个值,我们可以判断索引的使用情况。例如,一个复合索引 (col1, col2, col3),如果 key_len 只等于 col1 的长度,说明只用到了索引的第一列,col2 和 col3 的联合部分并未被利用(违反了最左前缀原则)。
ref: 索引参照列
ref 列显示了在使用索引进行查找时,与索引列进行等值匹配的值或列。它可以帮助我们理解 JOIN 的关联关系。
rows: 扫描的行数(重要参考)
rows 是一个非常关键的性能指标,它表示 MySQL 为了找到所需的行,大约需要扫描多少行数据。这个数字越小越好。需要注意的是,rows 是一个估算值,并非精确数字,但它能很好地反映扫描数据量的趋势。
filtered: 表示返回结果的行数占读取行数的百分比
filtered 的计算方式是 rows 乘以 filtered 的结果,大致等于最终返回给客户端的行数。例如,rows=1000, filtered=50,意味着 MySQL 读取了 1000 行,但经过 WHERE 条件过滤后,只有大约 500 行数据被返回。
Extra: 额外信息(性能优化的线索)
Extra 列包含了 MySQL 解决查询的详细信息,是优化的重要依据。常见的值有:
-
Using index: 覆盖索引。表示查询的列完全被索引覆盖,不需要回表查询数据行,这是非常高效的。sql-- 假设 (name, age) 有复合索引 EXPLAIN SELECT name, age FROM users WHERE name = 'John'; -- Extra: Using index -
Using where: 表示 MySQL 服务器将在存储引擎检索行后再进行 WHERE 条件的过滤。如果type是ALL并且有Using where,说明进行了全表扫描后又做了过滤,性能很差。 -
Using temporary: 表示 MySQL 需要创建一个临时表来处理查询,常见于GROUP BY或ORDER BY操作。应尽量避免。 -
Using filesort: MySQL 无法利用索引完成ORDER BY操作,需要额外的排序步骤。这也是一个需要优化的信号。
3. 实战案例:从诊断到优化
光说不练假把式,我们来看几个真实的案例,看看 EXPLAIN 如何帮助我们发现问题并解决问题。
案例一:全表扫描的陷阱
问题场景: 查询所有姓"张"的用户信息,发现查询非常慢。
原始 SQL:
sql
-- 假设表中有100万条用户数据
SELECT * FROM users WHERE last_name = 'Zhang';
执行计划:
sql
EXPLAIN SELECT * FROM users WHERE last_name = 'Zhang';
-- type: ALL
-- rows: 1000000
-- Extra: Using where
分析: type 为 ALL,rows 高达100万,这是一个典型的全表扫描,效率极低。
优化方案: 为 last_name 列添加索引。
sql
CREATE INDEX idx_lastname ON users(last_name);
优化后执行计划:
sql
EXPLAIN SELECT * FROM users WHERE last_name = 'Zhang';
-- type: ref
-- key: idx_lastname
-- rows: 100 (假设姓张的用户有100个)
效果: type 从 ALL 变为 ref,扫描行数从100万骤降到100,性能得到巨大提升。
案例二:覆盖索引的妙用
问题场景: 业务只需要查询用户的姓名和年龄,但 SQL 却选择了 SELECT *。
原始 SQL:
sql
SELECT * FROM users WHERE status = 'active';
执行计划:
sql
EXPLAIN SELECT * FROM users WHERE status = 'active';
-- type: ref
-- key: idx_status
-- rows: 10000
-- Extra: NULL (或 Using where)
分析: 看起来 type 和 key 都没问题,但 SELECT * 意味着即使使用了 idx_status 索引找到了数据,仍然需要回表去读取 users 表的其他所有列(如 email, address 等),造成了不必要的 I/O。
优化方案: 只查询需要的列。
sql
SELECT name, age FROM users WHERE status = 'active';
优化后执行计划:
sql
EXPLAIN SELECT name, age FROM users WHERE status = 'active';
-- type: ref
-- key: idx_status
-- rows: 10000
-- Extra: Using index
效果: Extra 变成了 Using index,这意味着查询所需的所有数据(name, age, status)都在索引 idx_status 中,无需回表,效率更高。
总结
EXPLAIN 是我们理解和优化 SQL 查询的基石。通过仔细分析 type(最重要的性能指标)、key(是否命中索引)、rows(扫描行数)和 Extra(额外信息)这几个核心字段,我们就能对 SQL 的执行效率了如指掌。
记住,优化是一个持续的过程,需要我们不断实践和总结。希望这篇文章能帮助你更好地驾驭 EXPLAIN,成为一名 SQL 性能优化高手!
互动时刻: 你在分析
EXPLAIN时遇到过哪些有趣或棘手的问题?欢迎在评论区分享你的经验和疑问,大家一起交流学习!
关注我,获取更多技术干货!
如果你想了解更多后端开发、系统设计、面试技巧等知识,欢迎关注我的微信公众号 【卷毛的技术笔记】 。我会持续分享一线技术实践和面试心得,陪你一起成长!