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 是衡量查询效率最直观的指标,从好到差排列如下:
- system:表只有一行(系统表),几乎不出现。
- const:通过主键或唯一索引等值匹配,最多返回一行。
- eq_ref:对于每个驱动表的行,被驱动表通过主键或唯一索引等值匹配,常用于 JOIN。
- 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) | 无需关注 |
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 | 不出现 | 出现 |