173 lines
6.7 KiB
Markdown
173 lines
6.7 KiB
Markdown
|
|
---
|
|||
|
|
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 机制]]
|