This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/19-EXPLAIN 完全指南.md
T
2026-05-17 00:06:11 +08:00

329 lines
12 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, 执行计划, 性能分析]
create time: 2026-05-16 00:00
---
# EXPLAIN 完全指南
## 概述
`EXPLAIN` 是 MySQL 性能分析的入口工具——它展示 SQL 语句的执行计划,告诉你优化器打算怎么用索引、怎么 JOIN、会不会用临时表和文件排序。
> [!QUESTION] 为什么不能直接靠"写了索引就一定能用到"?
> 因为 MySQL 使用的是 **Cost-Based Optimizer (CBO)**:优化器会根据统计信息(行数、页大小、聚簇因子等)自行决定最优执行路径。有时优化器认为全表扫描比走索引更快(比如要查整张表 80% 的数据),这时即使有索引也不会使用。`EXPLAIN` 就是用来观察和优化器"想法"是否一致的镜子。
## 基本用法
```sql
-- 标准 EXPLAIN
EXPLAIN SELECT * FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 1 AND o.amount > 100;
-- JSON 格式(信息最全,推荐)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE email = 'a@x.com';
-- 实际执行后再看成本(含统计信息)
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0.18+
```
## 关键字段速查
| 字段 | 含义 | 关注点 |
|------|------|--------|
| **type** | JOIN 类型 | 是否走索引(ref/range 优,ALL 差)|
| **key** | 实际使用的索引 | NULL = 没用索引 |
| **rows** | 预估扫描行数 | 越小越好 |
| **Extra** | 附加信息 | 重点关注 Using where/index/filesort/temporary |
| **cost** | 预估成本(JSON 格式) | optimizer_cost 越低越好 |
## type 详解(最重要)
```mermaid
flowchart LR
system["system<br/>只有1行"] --> const["const<br/>常量级访问"]
const --> eq_ref["eq_ref<br/>唯一索引 × 驱动行"]
eq_ref --> ref["ref<br/>等值匹配多行"]
ref --> range["range<br/>索引范围"]
range --> index["index<br/>全索引扫描"]
index --> ALL["ALL<br/>全表扫描 🔴"]
style system fill:#00D866,color:#fff
style const fill:#00D866,color:#fff
style eq_ref fill:#00D866,color:#fff
style ref fill:#00B6BC,color:#fff
style range fill:#FF9F43,color:#000
style index fill:#C44569,color:#fff
style ALL fill:#EE5A24,color:#fff
```
### 各级别详解
| 级别 | 名称 | 含义 | 示例 |
|------|------|------|------|
| **system** | 系统表 | 表只有一行(MyISAM 等引擎)| `SELECT * FROM (SELECT 1) t`|
| **const** | 常量 | 最多一行匹配 | `WHERE primary_key = 1`|
| **eq_ref** | 唯一索引 | 唯一索引扫描,每行匹配被驱动表一行 | `JOIN ON pk = ?` |
| **ref** | 索引查找 | 等值匹配多行(非唯一索引 / 最左前缀的部分匹配)| `WHERE email_prefix = 'a@'` |
| **range** | 范围扫描 | 索引上的范围查询 | `WHERE id > 100` |
| **index** | 索引全扫 | 扫描整个索引树 | `SELECT COUNT(*)` |
| **ALL** | 全表扫描 | 无可用索引,逐行扫描 | **需要优化** |
> [!WARNING] 不要盲目追求 ref 以上级别
> `range` 在某些场景(如范围不大)完全可以接受。关键是看 `rows` 字段和 `Extra` 的组合。
## Extra 关键字解读
> [!SUCCESS] Using index — 覆盖索引(Index Only Scan)
> 数据在索引中全部找到,**无需回表**。这是最优的 Extra 信息。
> [!INFO] Using where — 服务器层过滤
> 读完索引后还需 WHERE 条件过滤。**正常现象**,不代表性能问题。
> - **Using where + Using index** = 覆盖索引 + WHERE 过滤 → 最佳实践 ✅
> - **Using where 无 Using index** = 回表后才过滤 → 可考虑加索引优化 ⚠️
> [!TIP] Using index condition — 索引下推(ICP)
> MySQL 5.6+ 引入,在存储引擎层预过滤,减少回表次数 ✅
### 其他 Extra 信息速查
| 关键词 | 含义 | 处理 |
|--------|------|------|
| `Impossible where` | WHERE 永远为假(如 `status = 1 AND status = 2`)| 检查 SQL 逻辑是否正确 |
| `Select tables optimized` | 优化器发现子查询可展开 | 无需处理,已自动优化 ✅ |
| `Using distinct` | 内部去重,类似 DISTINCT | 看能否改用 GROUP BY + 索引 |
| `Scan & filter` | **InnoDB** 特有:全索引扫描后逐行过滤 | 考虑加更精确的索引 |
### Using temporary:何时出现
```sql
-- 常见触发场景
EXPLAIN SELECT city, AVG(age) FROM users GROUP BY city;
-- Extra: Using temporary; Using filesort
-- 需要用临时表存储每个 city 的聚合结果
EXPLAIN SELECT DISTINCT city FROM users;
-- Extra: Using temporary
-- DISTINCT 内部用临时表去重
```
> [!WARNING] Using temporary + Using filesort
> 当 GROUP BY 的分组列和 ORDER BY 的排序列不一致时,MySQL 会先建临时表再额外排序。
> **解决思路**:让索引同时满足 GROUP BY + ORDER BY 的顺序要求。
### Using filesort:深度分析
```mermaid
flowchart TD
A["WHERE status='pending'"] --> B["idx_status 索引扫描<br/>拿到所有 pending 订单的 pk"]
B --> C{"created_at 能在索引中找到吗?"}
C -->|能:<br/>联合索引 idx_status_created| D["直接有序返回<br/>No filesort ✅"]
C -->|不能:<br/>单列索引 idx_status| E["取 pk 列表 → 回表拿完整行<br/>→ 内存中排序<br/>Filesort ⚠️"]
D --> F["可能的解决方案"]
E --> F
F --> F1["添加联合索引 (status, created_at)"]
F --> F2["调整 WHERE 条件使索引生效"]
F --> F3["加大 sort_buffer_size"]
style D fill:#00D866,color:#fff
style E fill:#EE5A24,color:#fff
```
> [!QUESTION] 思考:ORDER BY 一定会触发 filesort 吗?
> 不一定!如果查询使用了索引且排序字段与索引顺序一致,MySQL 可以直接按索引序扫描,跳过排序步骤。关键看 **索引是否天然有序**。
```sql
-- ✅ No filesort — 走联合索引天然有序
EXPLAIN SELECT * FROM orders
WHERE status = 'pending'
ORDER BY status, created_at;
-- key: idx_status_created, Extra: Using where
-- ❌ Using filesort — WHERE 用了另一个索引,无法利用排序
EXPLAIN SELECT * FROM orders
WHERE user_id = 42
ORDER BY created_at DESC;
-- key: idx_user_id, Extra: Using where; Using filesort
```
## 实战案例分析
```sql
-- 原始查询(慢)
EXPLAIN SELECT u.username, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
-- 典型 bad result:
-- type: ALL, rows: 1000000, Extra: Using where; Using filesort; Using temporary
-- → 全表扫描 + 临时表 + 文件排序
-- 修复方案
-- 1. 创建复合索引
ALTER TABLE orders ADD INDEX idx_created_user (created_at, user_id);
-- 2. 查询改写(反向排序优化)
-- 先用索引定位 top 20 的 pk,再 JOIN 拿数据
SELECT u.username, o.amount
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
-- 新的 EXPLAIN:
-- type: range on orders, ref on users
-- key: idx_created_user
-- Extra: Using index condition; Using where
-- → 索引范围扫描 + 快速消除 filesort
```
## EXPLAIN FORMAT=JSON 精读
`FORMAT=JSON` 输出最完整的执行计划,包含嵌套子查询、Cost 信息、索引选择细节。
```json
{
"query_block": {
"select_id": 1,
"table": {
"table_name": "orders",
"access_type": "range",
"possible_keys": ["idx_created_user"],
"key": "idx_created_user",
"key_length": "8",
"rows": 5000,
"filtered": 100.0,
"index_condition": "orders.created_at >= '2026-01-01'",
"cost_information": {
"total_cost": "53000", ← 总成本(越小越好)
"optimizer_cost": "53000" ← 优化器计算的成本值
}
}
}
}
```
### 关键字段说明
| JSON 字段 | 含义 | 实战要点 |
|-----------|------|----------|
| **access_type** | 与 `type` 等价 | range/ref 优先,ALL 需关注 |
| **key_length** | 实际使用索引的字节数 | 越短说明用的列越少,可优化 |
| **rows** | 预估扫描行数 | 估算值,可能与实际情况偏差 |
| **filtered** | WHERE 筛选率 (%) | rows × filtered% = 进入下一步的行数 |
| **index_condition** | ICP 预过滤条件 | 有 = 使用了索引下推 ✅ |
| **using_temporary** / **using_filesort** | 布尔值 | true = 会触发对应操作 |
### Cost 分析:如何判断查询是否健康?
> [!TIP] Cost 评估经验法则
> - **cost < 1000**:通常没问题 ✅
> - **cost 1000 ~ 10000**:中等负载可接受,关注高频 SQL ⚠️
> - **cost > 10000**:大概率需要优化 🔴
关键点:**optimizer_cost 是绝对值而非相对值**,不同版本 MySQL 的计算方式可能变化。更有价值的是 **对比两种方案的 cost 差值**——比如加了一个索引后 cost 从 50000 降到 5300,这就是有效的索引设计。
```mermaid
flowchart LR
A["编写慢 SQL"] --> B["EXPLAIN 看执行计划"]
B --> C{"access_type?"}
C -->|ALL 全表扫描| D["检查 possible_keys<br/>添加合适的索引"]
C -->|range/ref/optimal| E{"Extra 中有<br/>filesort/temporary?"}
E -->|无 → 健康✅| F["无需额外处理"]
E -->|有 → 优化⚠️| G["调整索引顺序或改写 SQL"]
style F fill:#00D866,color:#fff
style G fill:#FF9F43,color:#000
style D fill:#C44569,color:#fff
```
## EXPLAIN ANALYZE:看到真实执行代价
`EXPLAIN ANALYZE` 是 MySQL 8.0.18+ 引入的功能——它会**真正执行一次 SQL**,然后返回实际运行时间、每行的实际扫描数等统计信息。
> [!WARNING] 注意事项
> EXPLAIN ANALYZE **会执行 SQL**。如果有写入操作(如 INSERT/UPDATE),需要谨慎;但对于纯 SELECT 查询可以放心使用。
```sql
-- 传统方式 vs 现代方式
EXPLAIN SELECT * FROM orders WHERE user_id = 42;
-- → 只有预估数据,可能不准
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
-- → 预估 + 实际运行结果
-- Extra: Rows examined: 42 (vs. rows estimate: 100)
```
### 典型输出解读
```
-> Index lookup on orders using idx_user_id (user_id=42)
(actual time=0.034..0.156 rows=42 loops=1)
```
- **actual time**:首行耗时 .. 总耗时(微秒级)
- **rows**:实际扫描行数(与预估 `rows` 对比,差距大说明统计信息过时)
- **loops**:循环次数,JOIN 场景尤其重要
> [!QUESTION] 什么时候用 EXPLAIN vs EXPLAIN ANALYZE?
> - **EXPLAIN**:快速预览,不执行 SQL,适合批量排查和 CI 门禁
> - **EXPLAIN ANALYZE**:精准诊断,需要实际执行,适合深入分析某个慢查询
> - 日常开发建议先用 `EXPLAIN` 快速筛查,再对问题 SQL 用 `EXPLAIN ANALYZE` 定位根因
## 优化策略速查
遇到问题 SQL,按以下步骤排查:
```mermaid
flowchart TD
A["拿到慢 SQL"] --> B["EXPLAIN 看执行计划"]
B --> C{"type = ALL?"}
C -->|是| D["检查 possible_keys → 加索引"]
C -->|否| E{"Extra 有 filesort?"}
E -->|是| F["调整索引顺序,覆盖 ORDER BY"]
E -->|否| G{"Extra 有 temporary?"}
G -->|是| H["让 GROUP/DISTINCT 走索引"]
G -->|否| I{"rows 很大?"}
I -->|是→ rows × filtered% 仍大| J["优化 WHERE 条件或加更精确的索引"]
I -->|否| K["查询健康 ✅"]
style K fill:#00D866,color:#fff
style D fill:#C44569,color:#fff
style F fill:#FF9F43,color:#000
style H fill:#FF9F43,color:#000
style J fill:#FF9F43,color:#000
```
### 常见场景与对策
| 症状 | 根因 | 对策 |
|------|------|------|
| `type=ALL, Extra=Using where` | 无可用索引 | 添加 WHERE 列的索引 |
| `Using filesort` | ORDER BY 字段不在可用索引中 | 创建 (WHERE列, ORDER BY列) 联合索引 |
| `Using temporary; Using filesort` | GROUP BY 和 ORDER BY 不一致 | 索引顺序同时满足两者 |
| `rows 预估 >> 实际行数` | 统计信息过时 | `ANALYZE TABLE 表名` 更新统计信息 |
| `possible_keys` 有但 `key` 为 NULL | 优化器选了全表扫描 | 用 `FORCE INDEX` 强制指定,或改写查询 |
### 别忘了维护统计信息
```sql
-- 当 EXPLAIN 预估严重偏离实际时
ANALYZE TABLE orders;
-- InnoDB 支持自动分析,但大批量写入后建议手动触发
SET GLOBAL innodb_stats_auto_recalc = ON;
```
## 关联笔记
- [[hhs/MySQL/16-B+Tree 索引原理]] — type=ALL vs type=range 的底层差异
- [[hhs/MySQL/19-慢查询日志分析]] — 如何用 pt-query-digest 配合 EXPLAIN
- [[hhs/MySQL/18-联合索引与最左前缀]] — 索引设计不当导致的 EXPLAIN 问题