4. EXPLAIN 执行计划:type、key、rows、Extra 等关键字段的调优含义

在 Go 项目数据库调优中,EXPLAIN 是分析慢查询最核心的工具。它模拟优化器如何执行一条 SQL,输出一张执行计划表。读懂这张表的每一列,才能精准定位索引失效、全表扫描、排序低效等性能瓶颈。

核心原则:执行计划不是“实际执行”,而是优化器基于统计信息估算的“执行路径”。调优的目标是让 type 从 ALL 提升到 ref 或 eq_ref,让 rows 尽可能小,让 Extra 中不出现 Using filesort 或 Using temporary。

4.1 EXPLAIN 输出概览

执行 EXPLAIN SELECT ... 后,MySQL 返回一张包含以下关键字段的表:

字段 含义 调优关注度
id 查询中每个 SELECT 的序号,id 越大越先执行 中
select_type 查询类型:SIMPLE、PRIMARY、SUBQUERY、DERIVED 等 中
table 当前行访问的表名或别名 低
partitions 匹配的分区(分区表时出现) 低
type 访问类型:ALL、index、range、ref、eq_ref、const、system 最高
possible_keys 可能使用的索引列表 高
key 实际使用的索引 最高
key_len 使用的索引字节长度 高
ref 索引列与哪个值进行比较(const、列名、函数等) 中
rows 优化器估算需要扫描的行数 最高
filtered 按表条件过滤后剩余行数的百分比 中
Extra 额外信息:Using index、Using where、Using filesort、Using temporary 等 最高

4.2 type 字段:访问类型的性能阶梯

type 是衡量查询效率最直观的指标,从好到差排列如下:

  1. system:表只有一行(系统表),几乎不出现。
  2. const:通过主键或唯一索引等值匹配,最多返回一行。
  3. eq_ref:对于每个驱动表的行,被驱动表通过主键或唯一索引等值匹配,常用于 JOIN。
  4. ref:通过普通索引等值匹配,返回多行。
  5. range:索引范围扫描(BETWEEN、>、<、IN 等)。
  6. index:全索引扫描,遍历整个索引树。
  7. ALL:全表扫描,最差。
调优目标:尽量让 type 达到 ref 或以上。如果出现 ALL 或 index,说明缺少合适的索引或查询条件无法利用索引。
EXPLAIN type 字段性能阶梯 EXPLAIN type 字段性能阶梯(从上到下性能递减) system / const(主键/唯一等值匹配,最多1行) eq_ref(JOIN 主键/唯一匹配,每驱动行最多1行) ref(普通索引等值匹配,返回多行) range(索引范围扫描:BETWEEN、>、<、IN) index(全索引扫描,遍历整个索引树) ALL(全表扫描,最差) 调优目标:让 type 达到 ref 或以上;避免 ALL 和 index

4.3 key 与 possible_keys:索引使用真相

  • possible_keys:列出查询中可能用到的索引。如果为 NULL,说明没有可用索引,必须创建。
  • key:优化器实际选择的索引。如果为 NULL,说明没有使用索引(type 通常为 ALL)。
  • key_len:使用的索引字节长度。可以判断是否使用了联合索引的全部列。例如联合索引 (a, b, c),如果 key_len 只等于 a 列的长度,说明只用了前缀。
常见陷阱:possible_keys 有值但 key 为 NULL,说明优化器认为全表扫描比使用索引更快(通常因为数据量小或索引选择性差)。此时需要检查索引统计信息是否过期,或使用 FORCE INDEX 测试。

4.4 rows 字段:扫描行数的估算

rows 是优化器估算需要扫描的行数,不是精确值。调优时关注:

  • rows 越小越好:理想情况是 rows = 1(const/eq_ref)。
  • rows 与 type 联动:ALL 时 rows 通常接近全表行数;ref 时 rows 远小于全表行数。
  • 多表 JOIN 时:rows 是每张表的估算扫描行数,乘积越大性能越差。
调优案例:某 Go 项目查询用户订单,EXPLAIN 显示 rows=50000,type=ALL。添加 user_id 索引后,rows=12,type=ref,查询耗时从 800ms 降到 3ms。

4.5 Extra 字段:隐藏的性能信号

Extra 包含大量优化器决策信息,以下是最需要关注的信号:

Extra 值 含义 调优建议
Using index 覆盖索引,查询所需列全部在索引中,无需回表 ✅ 理想状态,尽量保持
Using where 使用 WHERE 条件过滤,但可能不是索引直接过滤 检查索引是否覆盖过滤条件
Using index condition 索引条件下推(ICP),存储引擎层过滤 ✅ 好现象,减少回表
Using filesort 需要额外排序操作,无法利用索引排序 ⚠️ 添加排序字段到索引末尾
Using temporary 使用临时表(常见于 GROUP BY、DISTINCT、子查询) ⚠️ 优化查询或添加索引避免临时表
Using join buffer JOIN 时没有使用索引,使用连接缓冲区 ❌ 为 JOIN 列添加索引
Impossible WHERE WHERE 条件永远为假 检查业务逻辑
No tables used 查询没有使用表(如 SELECT 1) 无需关注
Extra 字段信号分类 Extra 字段信号分类与调优优先级 ✅ 好信号 Using index Using index condition Using where(配合索引) ⚠️ 警告信号 Using filesort Using temporary Using join buffer ❌ 严重问题 Impossible WHERE No matching row Full scan on NULL key 调优时优先消除 Using filesort 和 Using temporary,它们通常意味着额外的 CPU 和内存开销

4.6 实战:通过 EXPLAIN 定位慢查询

以下是一个 Go 项目中常见的慢查询及其 EXPLAIN 分析:

-- 慢查询 SQL
SELECT o.id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2024-01-01'
ORDER BY o.amount DESC
LIMIT 20;

-- EXPLAIN 输出(调优前)
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | rows    | Extra                       |
+----+-------------+-------+------+---------------+------+---------+-----------------------------+
|  1 | SIMPLE      | o     | ALL  | NULL          | NULL | 100000  | Using where; Using filesort |
|  1 | SIMPLE      | u     | ALL  | NULL          | NULL | 50000   | Using join buffer           |
+----+-------------+-------+------+---------------+------+---------+-----------------------------+

问题分析:

  • type=ALL:两张表都是全表扫描。
  • Extra=Using filesort:排序无法使用索引。
  • Extra=Using join buffer:JOIN 没有索引。
  • rows=100000 * 50000:笛卡尔积估算,性能极差。

优化方案:

-- 添加索引
ALTER TABLE orders ADD INDEX idx_created_at_amount (created_at, amount DESC);
ALTER TABLE users ADD INDEX idx_id (id);

-- 优化后 EXPLAIN
+----+-------------+-------+--------+---------------------+---------------------+------+------+--------------------------+
| id | select_type | table | type   | possible_keys       | key                 | rows | Extra                    |
+----+-------------+-------+--------+---------------------+---------------------+------+--------------------------+
|  1 | SIMPLE      | o     | range  | idx_created_at_amount| idx_created_at_amount| 200  | Using index condition    |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY,idx_id      | PRIMARY             | 1    | Using index              |
+----+-------------+-------+--------+---------------------+---------------------+------+--------------------------+

优化效果:

  • type 从 ALL 提升到 range 和 eq_ref。
  • rows 从 100000/50000 降到 200/1。
  • Extra 中的 Using filesort 和 Using join buffer 消失。
Go 项目集成建议:在 Go 的数据库中间件层(如 GORM、sqlx)中,可以封装一个 Explain() 方法,在开发环境自动打印慢查询的 EXPLAIN 结果,帮助开发者在代码审查阶段发现索引问题。

4.7 调优检查清单

检查项 理想值 危险值
type const / eq_ref / ref / range ALL 或 index
key 非 NULL,且与 possible_keys 匹配 NULL
rows 远小于全表行数(如 < 1000) 接近全表行数
Extra 中的 Using filesort 不出现 出现
Extra 中的 Using temporary 不出现 出现
Extra 中的 Using join buffer 不出现 出现