Files
autumn-recruitment/02.MySQL/index/Explain 执行计划解读.md
T

173 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [mysql, explain, execution-plan, index-pushdown, filesort]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# Explain 执行计划解读
## 概述
`EXPLAIN` 是 MySQL 提供的最重要的性能诊断工具。它在 SQL 真正执行之前,模拟优化器生成 SQL 的执行计划,揭示查询是如何使用索引、如何连接表、是否有临时表和文件排序等关键信息。掌握 EXPLAIN 输出各字段的含义,是将经验式调优转化为精准打击的前提。
## 核心原理
### 执行计划各字段详解
#### type 字段 —— 访问类型(性能由好到差)
type 描述了 MySQL 如何访问表中的数据,是评估查询质量的首要指标。
| type | 名称 | 含义 | 性能 |
|------|------|------|------|
| `system` | 系统表 | 表仅有一行(system 表),常数级读取 | ★★★★★ |
| `const` | 常量 | 最多匹配一行,通过主键/唯一索引一次性定位 | ★★★★★ |
| `eq_ref` | 等值引用 | 前一个表的每一行,当前表通过唯一索引匹配一行 | ★★★★☆ |
| `ref` | 引用 | 通过非唯一索引或主键的前缀匹配多行 | ★★★☆☆ |
| `range` | 范围 | 索引范围扫描,如 BETWEEN、IN、>、< | ★★☆☆☆ |
| `index` | 全索引扫描 | 遍历整个索引树,不走表数据 | ★★☆☆☆ |
| `ALL` | 全表扫描 | 逐行扫描表数据,未使用任何索引 | ☆☆☆☆☆ |
```mermaid
graph LR
A["system"] -->|"最优"| B["const"]
B --> C["eq_ref"]
C --> D["ref"]
D --> E["range"]
E --> F["index"]
F --> G["ALL"]
G -->|"最差"| H["需要优化"]
style A fill:#90ee90
style B fill:#90ee90
style C fill:#90ee90
style D fill:#ffeb99
style E fill:#ffeb99
style F fill:#ffcc99
style G fill:#ff9999
style H fill:#ff6666
```
> [!TIP] 面试常考点
> 为什么 index 比 ALL 好?`index` 虽然是全表扫描级别,但它只扫描索引树(通常为有序排列,可利用顺序 IO),而 `ALL` 需要扫描数据行(随机 IO)。所以 `index < ALL`。
#### select_type 字段 —— 查询类型
| select_type | 含义 |
|-------------|------|
| `SIMPLE` | 简单查询,不包含子查询或 UNION |
| `PRIMARY` | 最外层查询(配合 SUBQUERY/UNION 等出现) |
| `SUBQUERY` | 子查询中的 SELECT(非 FROM 子句中) |
| `DERIVED` | 派生表(FROM 子句中的子查询) |
| `UNION` | UNION 中的第二个及以后的 SELECT |
| `UNION RESULT` | UNION 的结果集 |
```sql
-- 示例:复杂查询的 select_type
SELECT * FROM orders o
WHERE o.user_id IN (
SELECT u.id FROM users u WHERE u.status = 1
)
UNION
SELECT * FROM orders_archive WHERE archived = 1;
-- 输出行的 select_type 依次为:PRIMARY / SUBQUERY / UNION / UNION_RESULT
```
#### possible_keys vs key vs key_len
- **possible_keys**:优化器认为可能用到的索引列表(只是"候选",不代表最终会选用)
- **key**:实际选择的索引
- **key_len**:使用的索引长度(字节数),反映索引列参与的程度
```sql
-- 复合索引 (name VARCHAR(64), age INT)
-- utf8mb4 每个字符 4 字节,name 的 key_len = 64 * 4 = 256 字节
-- 加上变长标记 1 字节 + NULL 标记 1 字节 = 258
-- 如果 key_len = 258,说明用了 name 列
-- 如果 key_len = 262 (= 258 + 4),说明同时用了 name 和 age
SELECT * FROM users WHERE name = 'Alice' AND age = 25;
-- key: idx_name_age, key_len: 262 → 两个列都用上了
```
#### rows × filtered —— 行数估算
- **rows**:优化器估算的需要扫描的行数(越小越好)
- **filtered**:按表条件筛选后,留存行的百分比(0~100)
- **rows × filtered%** ≈ 实际需要处理的行数
> [!WARNING] 估算偏差
> rows 和 filtered 是统计采样估算值,并非精确计数。如果表的统计信息过期,可能导致严重偏差。执行 `ANALYZE TABLE t;` 可以更新统计信息。
### Extra 常见值解析
| Extra 值 | 含义 | 建议 |
|----------|------|------|
| `Using where` | 存储引擎返回数据后,Server 层再做 WHERE 过滤 | 正常,但如果没走索引说明需要加索引 |
| `Using index` | 覆盖索引,无需回表 | 最好的情况 |
| `Using temporary` | 使用了临时表解决查询 | 通常出现在 GROUP BY 或 DISTINCT,需要优化 |
| `Using filesort` | 需要额外排序,无法利用索引顺序 | 考虑加索引消除排序 |
| `Using index condition` | 索引下推(ICP),在存储引擎层过滤 | 好消息,说明 MySQL 自动优化了 |
| `Using sort_union()` / `Using union()` | 使用了多个索引合并后再排序 | 考虑改造成单索引 |
| `Impossible where` | WHERE 条件永远不成立 | 检查 SQL 逻辑 |
| `Select tables optimized away` | 最小化优化,无需访问表 | 最优情况 |
```sql
-- 看到 WARNING 时的排查思路
EXPLAIN SELECT * FROM orders
WHERE YEAR(created_at) = 2025;
-- Extra: Using where; Using filesort
-- 问题:YEAR() 函数作用于字段 → 无法使用索引
-- 优化:改用范围查询
-- WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'
```
## 代码示例
```sql
-- 典型 EXPLAIN 输出解读
EXPLAIN SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
ORDER BY o.created_at DESC
LIMIT 10;
-- 解读要点:
-- 第一行:u 表 → select_type=PRIMARY, type=ref (status 上有索引)
-- → key=idx_status, rows≈500, filtered=100%
-- 第二行:o 表 → type=ref (user_id 是外键索引), eq_ref
-- → 对每个 u 的行,通过 user_id 精确匹配一条订单
-- Extra: Using index condition → ICP 生效
-- Using filesort → created_at 上没有合适索引,需额外排序
-- 优化方向:给 orders(created_at) 建索引,消除 filesort
```
## 实践场景
**场景一:日常慢查询排查**
1. 开启慢查询日志(`long_query_time=1`)
2. 对慢 SQL 加 `EXPLAIN` 前缀执行
3. 检查 type 是否退化到 `ALL` 或 `index`
4. 关注 Extra 中的 `Using temporary` 和 `Using filesort`
5. 针对问题点调整索引或改写 SQL
**场景二:判断索引是否被有效利用**
```sql
-- 如果发现某张表的 key 列为 NULL,说明该查询完全没有使用索引
-- 检查 possible_keys 是否有可选索引但未被选中
-- 如果是,可能是统计信息过时,执行 ANALYZE TABLE
ANALYZE TABLE orders;
```
**场景三:JOIN 查询优化**
- 小表驱动大表:JOIN 中较小的表放在前面(EXPLAIN 靠上的表先被执行)
- 确保 JOIN 条件两侧都有对应的索引
- 避免在多列上使用 OR 连接 JOIN 条件,这会迫使优化器退化为全表扫描
## 扩展阅读
- [[B+树索引原理]]
- [[覆盖索引与回表优化]]
- [[ACID 与 MVCC 机制]]