面试官:谈谈你对 MySQL 执行计划的理解?

11 阅读9分钟

MySQL 性能分析神器:一文读懂 EXPLAIN 执行计划

开篇引子:  线上系统突然告警,用户反馈操作卡顿?DBA一看监控,CPU 使用率飙升,慢查询日志里全是“罪魁祸首”?别慌,这背后往往藏着一个被忽视的 SQL 语句。掌握 EXPLAIN 这把“瑞士军刀”,你就能化身数据库侦探,精准定位性能瓶颈。本文将带你从零开始,彻底搞懂 MySQL 的执行计划,让你的 SQL 优化之路畅通无阻!

你将收获什么?

读完这篇文章,你将能够:

  • 深入理解 EXPLAIN 命令输出的每一个字段及其深层含义。
  • 熟练解读 典型的执行计划,快速判断 SQL 的好坏。
  • 学以致用,通过实际案例,掌握常见的 SQL 性能问题诊断与优化方法。
  • 从容应对 技术面试中关于 MySQL 执行计划的高频问题。

1. EXPLAIN 简介:SQL 的“体检报告”

EXPLAIN 是 MySQL 提供的一个非常实用的命令,它能模拟优化器执行 SQL 语句,并返回一个详细的报告。这份报告就像 SQL 的“体检报告”,告诉我们这条 SQL 语句是如何被执行的,它扫描了多少行数据,是否使用了索引,以及查询的关联方式等关键信息。通过解读这份报告,我们就能知道 SQL 是否高效,瓶颈在哪里,从而进行针对性的优化。

如何使用?

在你的 SELECT 语句前面加上 EXPLAIN 关键字即可。例如:

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 相同。优化器会按照从上到下的顺序执行。
  • 不同 idid 值越大越先执行:  在包含子查询的 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)进行等值查询,最多只返回一行数据。因为只匹配一行,所以速度非常快。

    -- 假设 id 是主键
    EXPLAIN SELECT * FROM users WHERE id = 1;
    -- type: const
    
  • eq_ref: 在多表 JOIN 中,对于前一个表的每一行,在当前表中通过主键或唯一索引(UNIQUE KEY)最多只匹配一行。这是高效的 JOIN 方式。

    -- 假设 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 方式。

    EXPLAIN SELECT * FROM users WHERE user_name = 'John';
    -- type: ref
    
  • range: 使用索引获取范围值,例如 BETWEENIN>< 等操作。

    EXPLAIN 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=1000filtered=50,意味着 MySQL 读取了 1000 行,但经过 WHERE 条件过滤后,只有大约 500 行数据被返回。

Extra: 额外信息(性能优化的线索)

Extra 列包含了 MySQL 解决查询的详细信息,是优化的重要依据。常见的值有:

  • Using index覆盖索引。表示查询的列完全被索引覆盖,不需要回表查询数据行,这是非常高效的。

    -- 假设 (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:

-- 假设表中有100万条用户数据
SELECT * FROM users WHERE last_name = 'Zhang';

执行计划:

EXPLAIN SELECT * FROM users WHERE last_name = 'Zhang';
-- type: ALL
-- rows: 1000000
-- Extra: Using where

分析:  type 为 ALLrows 高达100万,这是一个典型的全表扫描,效率极低。

优化方案:  为 last_name 列添加索引。

CREATE INDEX idx_lastname ON users(last_name);

优化后执行计划:

EXPLAIN SELECT * FROM users WHERE last_name = 'Zhang';
-- type: ref
-- key: idx_lastname
-- rows: 100 (假设姓张的用户有100个)

效果:  type 从 ALL 变为 ref,扫描行数从100万骤降到100,性能得到巨大提升。

案例二:覆盖索引的妙用

问题场景:  业务只需要查询用户的姓名和年龄,但 SQL 却选择了 SELECT *

原始 SQL:

SELECT * FROM users WHERE status = 'active';

执行计划:

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。

优化方案:  只查询需要的列。

SELECT name, age FROM users WHERE status = 'active';

优化后执行计划:

EXPLAIN SELECT name, age FROM users WHERE status = 'active';
-- type: ref
-- key: idx_status
-- rows: 10000
-- Extra: Using index

效果:  Extra 变成了 Using index,这意味着查询所需的所有数据(nameagestatus)都在索引 idx_status 中,无需回表,效率更高。


总结

EXPLAIN 是我们理解和优化 SQL 查询的基石。通过仔细分析 type(最重要的性能指标)、key(是否命中索引)、rows(扫描行数)和 Extra(额外信息)这几个核心字段,我们就能对 SQL 的执行效率了如指掌。

记住,优化是一个持续的过程,需要我们不断实践和总结。希望这篇文章能帮助你更好地驾驭 EXPLAIN,成为一名 SQL 性能优化高手!

互动时刻:  你在分析 EXPLAIN 时遇到过哪些有趣或棘手的问题?欢迎在评论区分享你的经验和疑问,大家一起交流学习!


关注我,获取更多技术干货!

如果你想了解更多后端开发、系统设计、面试技巧等知识,欢迎关注我的微信公众号  【卷毛的技术笔记】 。我会持续分享一线技术实践和面试心得,陪你一起成长!