2. 执行计划入门:EXPLAIN 命令详解
执行计划是数据库优化器为 SQL 语句生成的执行步骤描述。通过 EXPLAIN 命令,我们可以查看 MySQL 如何访问表、使用哪些索引、预估扫描多少行数据。掌握执行计划的解读能力,是慢查询优化的第一步,也是最关键的一步。
2.1 EXPLAIN 基本用法
在任意 SELECT 语句前加上 EXPLAIN 关键字,即可获取该查询的执行计划。MySQL 8.0 还支持 EXPLAIN FORMAT=JSON 和 EXPLAIN ANALYZE 两种增强格式。
-- 基础用法
EXPLAIN SELECT * FROM orders WHERE order_id = 10086;
-- JSON 格式(包含成本估算)
EXPLAIN FORMAT=JSON
SELECT * FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.user_id = 12345;
-- 真实执行计划(MySQL 8.0.18+)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE status = 'PAID' AND create_time > '2024-01-01';
实践建议:日常排查慢查询时,优先使用
EXPLAIN 快速定位索引问题;需要精确分析耗时分布时,使用 EXPLAIN ANALYZE 获取实际执行时间与循环次数。
2.2 执行计划输出列详解
EXPLAIN 输出包含 12 个列(MySQL 8.0),其中 type、rows、Extra 是判断查询性能的核心指标。下表列出所有列及其含义:
| 列名 | 含义 | 优化关注度 |
|---|---|---|
| id | 查询中 SELECT 的标识符,id 越大越先执行 | 中 |
| select_type | 查询类型:SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION 等 | 中 |
| table | 输出行所引用的表名 | 低 |
| type | 访问类型:从全表扫描到唯一索引查找,性能依次提升 | 高 |
| possible_keys | 查询可能使用的索引列表 | 中 |
| key | 实际选择的索引 | 高 |
| key_len | 使用的索引字节长度,可判断联合索引使用了几个字段 | 高 |
| ref | 索引查找时使用的列或常量 | 中 |
| rows | 优化器预估需要扫描的行数 | 高 |
| filtered | 经过表条件过滤后剩余行数的百分比 | 中 |
| Extra | 额外信息:Using index、Using filesort、Using temporary 等 | 高 |
2.3 type 列:访问类型全解析
type 列是衡量查询效率最直观的指标,从最优到最差排列如下:
- system:表只有一行(系统表),const 的特例,几乎见不到。
- const:通过主键或唯一索引等值查找,最多返回一行。速度极快。
- eq_ref:连接查询中,被驱动表通过主键或唯一索引等值匹配,每行只匹配一条。
- ref:通过普通索引等值查找,可能返回多行。
- fulltext:使用全文索引进行匹配。
- ref_or_null:ref 的基础上额外查找 NULL 值。
- index_merge:使用多个索引合并后访问(交集/并集)。
- unique_subquery:IN 子查询中使用主键或唯一索引。
- index_subquery:IN 子查询中使用普通索引。
- range:索引范围扫描,如 BETWEEN、>、<、IN 列表。
- index:全索引扫描,遍历整个索引树,比全表扫描略快。
- ALL:全表扫描,最差情况,应极力避免。
优化红线:生产环境中,
type 为 ALL 或 index 的查询必须优化。目标是将核心查询的 type 提升到 ref 及以上,至少达到 range。
2.4 rows 列:预估扫描行数的意义
rows 是优化器根据索引统计信息估算的需要扫描的行数,并非精确值。它反映了查询的"工作量":
- rows 越小越好:通常与查询响应时间正相关。
- 注意误差:当表数据频繁更新或统计信息过期时,rows 可能严重偏离实际值。此时可执行
ANALYZE TABLE更新统计信息。 - 多表关联:rows 是驱动表的扫描行数,被驱动表的扫描次数由驱动表行数决定。
-- 示例:rows 从 500 万降到 1 的优化效果
-- 优化前:type=ALL, rows=5234567
EXPLAIN SELECT * FROM orders WHERE order_no = 'ORD20240101001';
-- 优化后:type=const, rows=1(添加唯一索引 idx_order_no)
EXPLAIN SELECT * FROM orders WHERE order_no = 'ORD20240101001';
2.5 Extra 列:关键信息解读
Extra 列包含优化器执行策略的补充信息,以下是最常见的几类:
| Extra 信息 | 含义 | 优化建议 |
|---|---|---|
| Using index | 覆盖索引,查询所需字段全部在索引中,无需回表 | ✅ 理想状态,尽量保持 |
| Using where | 在存储引擎层过滤后,还需在 Server 层过滤 | 通常正常,若结合 Using filesort 需关注 |
| Using index condition | 索引条件下推(ICP),将部分 WHERE 条件下推到存储引擎 | ✅ 好现象,减少回表次数 |
| Using filesort | 无法利用索引排序,需要额外排序操作 | ⚠️ 应优化,尝试建立排序字段的索引 |
| Using temporary | 使用了临时表(常见于 GROUP BY、DISTINCT、子查询) | ⚠️ 应优化,考虑索引或改写 SQL |
| Using join buffer | 连接时未使用索引,使用了连接缓冲区 | ❌ 必须优化,为连接字段添加索引 |
| Impossible WHERE | WHERE 条件永远为假 | 检查业务逻辑或参数 |
| No tables used | 查询没有 FROM 子句(如 SELECT 1+1) | 无需关注 |
重点关注:当 Extra 中出现
Using filesort 或 Using temporary 时,意味着查询无法完全利用索引完成排序或分组,通常伴随较大的内存/磁盘开销,是慢查询的常见原因。
2.6 执行计划综合解读流程
解读执行计划时,建议按以下步骤逐一排查:
- 看 type:是否为 ALL 或 index?若是,立即考虑添加索引。
- 看 key:实际使用的索引是否与 possible_keys 一致?若不一致,检查索引选择偏差原因。
- 看 rows:预估扫描行数是否在可接受范围内?多表关联时关注驱动表的 rows。
- 看 Extra:是否存在 Using filesort、Using temporary、Using join buffer?这些是性能瓶颈信号。
- 看 key_len:联合索引是否用到了所有字段?key_len 较短说明只使用了前缀部分。
2.7 实战案例:从执行计划到优化
以下是一个真实慢查询的优化过程:
-- 原始慢查询(响应时间 3.2s)
SELECT order_id, user_id, status, total_amount
FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
LIMIT 20;
-- EXPLAIN 输出
-- type: ALL(全表扫描)
-- rows: 1,245,678
-- Extra: Using where; Using filesort
问题分析:
type=ALL:全表扫描 124 万行。Extra=Using filesort:无法利用索引排序,需要额外排序。
优化方案:建立联合索引 (status, create_time),同时覆盖 WHERE 过滤和 ORDER BY 排序。
-- 添加联合索引
ALTER TABLE orders ADD INDEX idx_status_create_time (status, create_time);
-- 优化后 EXPLAIN
-- type: ref
-- key: idx_status_create_time
-- rows: 15,234(仅扫描 PAID 状态的行)
-- Extra: Using index condition(索引条件下推,无需 filesort)
优化后查询响应时间从 3.2s 降至 12ms,性能提升超过 260 倍。
核心原则:执行计划是 SQL 优化的"体检报告"。每次上线新查询前,务必执行 EXPLAIN 确认 type 达到 ref 或 range,Extra 不出现 filesort 和 temporary。养成这个习惯,能避免 90% 以上的线上慢查询问题。