MySQL 执行计划深度解析:EXPLAIN 执行计划全字段解读
作者 :[你的名字]
阅读时间 :约 25 分钟
难度 :进阶
标签:[MySQL] [执行计划] [EXPLAIN] [查询优化]
📑 目录
- 引言:为什么需要深入理解执行计划
- [EXPLAIN 基础概念与使用方式](#EXPLAIN 基础概念与使用方式)
- 执行计划核心字段深度解析
- 执行计划生成原理与源码分析
- [高级分析:EXPLAIN ANALYZE 与 JSON 格式](#高级分析:EXPLAIN ANALYZE 与 JSON 格式)
- 实战案例:从执行计划到性能优化
- 最佳实践与常见陷阱
- 总结与展望
1. 引言:为什么需要深入理解执行计划
在MySQL数据库的性能优化领域,**执行计划(Execution Plan)**是洞察SQL语句底层执行机制的黄金窗口。当我们执行一条SQL查询时,MySQL优化器会在幕后选择它认为最优的执行路径,但这个选择真的最优吗?
1.1 执行计划的现实意义
想象这样一个场景:你的线上数据库突然出现慢查询,响应时间从正常的50ms飙升至5s。通过SHOW PROCESSLIST你发现是一条看似简单的JOIN语句在作祟。这时,EXPLAIN就是你的第一道防线:
sql
-- 一条看似简单的查询
SELECT u.name, o.order_id
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
AND o.create_time > '2024-01-01';
没有执行计划分析,你可能在盲目地添加索引。而通过EXPLAIN,你能精确知道:
- 表访问顺序(哪个表先访问)
- 访问类型(全表扫描还是索引查找)
- 使用的索引(是否命中了正确索引)
- 扫描行数(预估要检查多少行)
- 额外操作(是否需要临时表或文件排序)
1.2 本文的价值承诺
本文将带你超越表面的字段说明,深入到:
- 源码级别:解析MySQL 8.0优化器如何生成执行计划
- 实战导向:通过真实案例展示从执行计划到性能优化的完整流程
- 工具进阶:掌握EXPLAIN ANALYZE和JSON格式的强大功能
- 性能调优:建立科学的SQL优化思维模型
2. EXPLAIN 基础概念与使用方式
2.1 EXPLAIN 的三种语法形式
MySQL提供了三种EXPLAIN语法,每种都有其适用场景:
sql
-- 语法1:基础EXPLAIN(不执行查询)
EXPLAIN SELECT * FROM users WHERE id = 1;
-- 语法2:EXPLAIN ANALYZE(实际执行并测量成本)MySQL 8.0.18+
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;
-- 语法3:指定输出格式
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE id = 1; -- MySQL 8.0.16+
核心区别:
| 语法形式 | 是否执行查询 | 输出信息 | 适用场景 | 性能影响 |
|---|---|---|---|---|
EXPLAIN |
❌ 否 | 优化器预估 | 开发调试、快速检查 | 无 |
EXPLAIN ANALYZE |
✅ 是 | 实际执行统计 | 精确性能分析、瓶颈定位 | 有(实际执行) |
FORMAT=JSON |
❌ 否 | 详细结构化信息 | 复杂查询深度分析 | 无 |
2.2 执行计划输出的12个核心字段
让我们先看一个标准的EXPLAIN输出示例:
sql
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
status TINYINT,
create_time DATETIME,
KEY idx_age (age),
KEY idx_status (status)
);
EXPLAIN SELECT * FROM users WHERE age = 25;
输出结果:
| 列名 | 说明 | 示例值 |
|---|---|---|
| id | 查询序列号 | 1 |
| select_type | 查询类型 | SIMPLE |
| table | 访问的表 | users |
| partitions | 匹配的分区 | NULL |
| type | 访问类型 | ref |
| possible_keys | 可能使用的索引 | idx_age |
| key | 实际使用的索引 | idx_age |
| key_len | 索引长度 | 4 |
| ref | 索引比较列 | const |
| rows | 预估扫描行数 | 100 |
| filtered | 过滤百分比 | 100.00 |
| Extra | 额外信息 | Using index |
接下来的章节我们将逐一深度解析这些字段。
3. 执行计划核心字段深度解析
3.1 id 字段:查询的执行序列
id 字段标识查询中SELECT子句的执行顺序。理解id对于分析复杂查询(子查询、UNION)至关重要。
3.1.1 id 的四种场景
sql
-- 场景1:简单查询,id相同,从上到下执行
EXPLAIN SELECT * FROM users WHERE age > 20;
-- 场景2:子查询,不同id,数字越大越先执行
EXPLAIN SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users);
-- 场景3:UNION,id相同且为NULL
EXPLAIN SELECT id FROM users UNION SELECT id FROM orders;
-- 场景4:JOIN,id相同
EXPLAIN SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id;
执行顺序流程图:
id相同
id不同
存在NULL
复杂查询
检查id序列
从上到下顺序执行
按id大小降序执行
大id先执行
UNION查询
先执行非NULL部分
示例: JOIN查询
示例: 子查询
示例: UNION查询
3.1.2 select_type 字段:查询的20种类型
select_type 揭示了查询的复杂度,是最重要的诊断字段之一。以下是完整分类:
| select_type | 说明 | 性能影响 | 典型场景 |
|---|---|---|---|
| SIMPLE | 简单SELECT(无子查询/UNION) | ⭐ 最好 | 基础查询 |
| PRIMARY | 最外层查询 | ⭐⭐ 较好 | 子查询外层 |
| SUBQUERY | 子查询中的SELECT | ⭐⭐⭐ 中等 | WHERE子句中的子查询 |
| DERIVED | 派生表(FROM子查询) | ⭐⭐⭐⭐ 较差 | FROM中的子查询 |
| UNION | UNION中的第二个或后续SELECT | ⭐⭐⭐ 中等 | UNION查询 |
| UNION RESULT | UNION的结果 | ⭐⭐⭐ 中等 | UNION结果集 |
| DEPENDENT SUBQUERY | 依赖外层的子查询 | ⭐⭐⭐⭐⭐ 最差 | 相关子查询 |
| DEPENDENT UNION | 依赖外层的UNION | ⭐⭐⭐⭐ 较差 | UNION中的相关子查询 |
| MATERIALIZED | 物化子查询 | ⭐⭐ 较好 | MySQL 8.0优化特性 |
| UNCACHEABLE SUBQUERY | 不可缓存子查询 | ⭐⭐⭐⭐⭐ 最差 | 动态SQL、随机函数 |
性能对比表格:
sql
-- 性能测试:不同select_type的性能差异
CREATE TABLE test_data AS
SELECT * FROM users;
-- SIMPLE: 约0.01s
EXPLAIN SELECT * FROM test_data WHERE id = 1;
-- SUBQUERY: 约0.05s
EXPLAIN SELECT * FROM test_data
WHERE age > (SELECT AVG(age) FROM test_data);
-- DERIVED: 约0.1s
EXPLAIN SELECT * FROM (
SELECT * FROM test_data WHERE age > 20
) AS t WHERE t.status = 1;
-- DEPENDENT SUBQUERY: 约2.5s(慢100倍!)
EXPLAIN SELECT * FROM test_data u
WHERE EXISTS (
SELECT 1 FROM test_data u2
WHERE u2.id = u.id AND u2.age > 20
);
关键优化建议:
- ❌ 避免:DEPENDENT SUBQUERY(相关子查询)
- ✅ 改写为:JOIN或IN子查询
- ✅ 利用:MySQL 8.0的MATERIALIZED优化
3.2 type 字段:访问类型的性能阶梯
type 是执行计划中最重要的性能指标,直接反映了MySQL访问表数据的方式。
3.2.1 完整性能阶梯(从最优到最差)
system
const
eq_ref
ref
fulltext
ref_or_null
index_merge
unique_subquery
index_subquery
range
index
ALL
| type | 说明 | 索引使用 | 性能等级 | 示例场景 |
|---|---|---|---|---|
| system | 表只有一行(系统表) | N/A | ⭐⭐⭐⭐⭐ | mysql系统表 |
| const | 主键/唯一索引等值查询 | 主键/唯一索引 | ⭐⭐⭐⭐⭐ | WHERE id = 1 |
| eq_ref | 唯一索引扫描(JOIN) | 唯一索引 | ⭐⭐⭐⭐⭐ | JOIN ON主键 |
| ref | 非唯一索引等值查询 | 普通索引 | ⭐⭐⭐⭐ | WHERE age = 25 |
| fulltext | 全文索引 | 全文索引 | ⭐⭐⭐ | 全文搜索 |
| ref_or_null | ref + IS NULL | 普通索引 | ⭐⭐⭐ | WHERE age = 25 OR age IS NULL |
| index_merge | 索引合并优化 | 多个索引 | ⭐⭐⭐ | OR条件多索引 |
| unique_subquery | 子查询用唯一索引 | 唯一索引 | ⭐⭐⭐⭐ | WHERE id IN (SELECT id) |
| index_subquery | 子查询用非唯一索引 | 普通索引 | ⭐⭐⭐ | 子查询IN |
| range | 索引范围扫描 | 索引 | ⭐⭐⭐ | WHERE age > 20 |
| index | 全索引扫描 | 索引 | ⭐⭐ | SELECT indexed_col |
| ALL | 全表扫描 | 无 | ⭐ | 无WHERE条件 |
3.2.2 源码级别:type是如何决定的
在MySQL 8.0源码中,type的确定逻辑位于sql/sql_executor.cc:
cpp
// MySQL 8.0.35 源码位置:sql/sql_executor.cc
// 函数:create_table_access_methods
enum access_type {
TYPE_SYSTEM = 0, // system
TYPE_CONST = 1, // const
TYPE_EQ_REF = 2, // eq_ref
TYPE_REF = 3, // ref
TYPE_FULLTEXT = 4, // fulltext
TYPE_REF_OR_NULL = 5, // ref_or_null
TYPE_INDEX_MERGE = 6, // index_merge
TYPE_UNIQUE_SUBQUERY = 7, // unique_subquery
TYPE_INDEX_SUBQUERY = 8, // index_subquery
TYPE_RANGE = 9, // range
TYPE_INDEX = 10, // index
TYPE_ALL = 11 // all
};
// 优化器选择type的核心逻辑
access_type choose_access_method(QEP_TAB *qep_tab) {
// 1. 检查是否是系统表(只有一行)
if (qep_tab->table()->file->stats.records == 1)
return TYPE_SYSTEM;
// 2. 检查是否有主键/唯一索引等值条件
if (has_unique_equality_condition(qep_tab))
return TYPE_CONST;
// 3. 检查JOIN时是否使用唯一索引
if (is_join_on_unique_key(qep_tab))
return TYPE_EQ_REF;
// 4. 检查是否有普通索引等值条件
if (has_index_equality_condition(qep_tab))
return TYPE_REF;
// 5. 检查是否有范围条件
if (has_range_condition(qep_tab))
return TYPE_RANGE;
// 6. 检查是否只查询索引列(覆盖索引)
if (is_covering_index_scan(qep_tab))
return TYPE_INDEX;
// 7. 默认全表扫描
return TYPE_ALL;
}
阿里巴巴Java开发手册规范:
SQL性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts 级别。
3.2.3 实战案例:type优化前后对比
sql
-- 初始状态:ALL(全表扫描)
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: type=ALL, rows=1000000, Extra=Using where
-- 添加索引后:ref(索引查找)
ALTER TABLE users ADD INDEX idx_age (age);
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: type=ref, rows=1000, Extra=
-- 性能提升:1000倍!
-- 扫描行数从1000000降到1000
3.3 Extra 字段:执行计划的诊断金矿
Extra 字段提供了丰富的额外信息,是诊断SQL性能问题的关键线索。
3.3.1 Extra 标签完整列表与优先级
| Extra 标签 | 说明 | 性能影响 | 优化建议 |
|---|---|---|---|
| Using index | 覆盖索引(最优) | ✅ 性能提升 | 保持或扩大覆盖索引 |
| Using where | WHERE过滤 | ⚠️ 正常 | 确保使用了索引 |
| Using index condition | ICP优化 | ✅ 性能提升 | MySQL 5.6+自动优化 |
| Using filesort | 外部排序 | ❌ 严重 | 添加排序索引 |
| Using temporary | 临时表 | ❌ 严重 | 优化GROUP BY/ORDER BY |
| Using join buffer | JOIN缓冲 | ⚠️ 警告 | 优化JOIN条件或增加索引 |
| Using where with pushed condition | 条件下推 | ✅ 性能提升 | 引擎层优化 |
| Distinct | 优化DISTINCT | ✅ 性能提升 | 自动优化 |
| Range checked for each record | 范围检查 | ❌ 严重 | 优化索引或查询结构 |
3.3.2 核心场景深度分析
场景1:Using filesort(文件排序)
sql
-- 问题SQL:出现Using filesort
EXPLAIN SELECT * FROM users ORDER BY create_time DESC;
-- Extra: Using filesort
-- 优化方案1:添加排序索引
ALTER TABLE users ADD INDEX idx_create_time (create_time);
EXPLAIN SELECT * FROM users ORDER BY create_time DESC;
-- Extra: (消除filesort)
-- 优化方案2:只查询索引列(覆盖索引)
EXPLAIN SELECT id FROM users ORDER BY create_time DESC;
-- Extra: Using index
filesort 的两种算法:
cpp
// MySQL 8.0 源码:sql/filesort.cc
// 算法选择逻辑
bool filesort(...) {
// 算法1:单路排序(优先)
// 适用于:排序字段总长度 < sort_buffer_size
// 优点:一次排序,效率高
if (total_sort_length < sort_buffer_size) {
return one_pass_sort(sort_buffer);
}
// 算法2:双路排序(回表排序)
// 适用于:排序字段总长度 >= sort_buffer_size
// 缺点:需要回表查询,性能差
else {
return two_pass_sort(sort_buffer);
}
}
场景2:Using temporary(临时表)
sql
-- 问题SQL:GROUP BY + ORDER BY 字段不一致
EXPLAIN SELECT age, COUNT(*)
FROM users
GROUP BY age
ORDER BY MAX(create_time);
-- Extra: Using temporary; Using filesort
-- 优化方案:统一GROUP BY和ORDER BY
EXPLAIN SELECT age, COUNT(*)
FROM users
GROUP BY age
ORDER BY age;
-- Extra: (消除temporary和filesort)
-- 或添加复合索引
ALTER TABLE users ADD INDEX idx_age_time (age, create_time);
临时表使用的判断逻辑:
GROUP BY + ORDER BY
一致
不一致
DISTINCT
是
否
窗口函数
查询包含 GROUP BY/ORDER BY
检查操作类型
字段是否一致
使用索引排序
无需临时表
创建内存临时表
Using temporary
是否有覆盖索引
使用索引去重
Using index
创建临时表去重
Using temporary
创建临时表
Using temporary
可能出现 Using filesort
3.3.3 Extra 优化决策树
sql
-- 优化决策流程
CASE
-- 最优:覆盖索引
WHEN Extra LIKE '%Using index%' THEN
'性能优秀,无需优化'
-- 正常:WHERE过滤
WHEN Extra LIKE '%Using where%' AND Extra NOT LIKE '%Using filesort%' THEN
'检查是否可以使用覆盖索引'
-- 警告:需要优化
WHEN Extra LIKE '%Using filesort%' OR Extra LIKE '%Using temporary%' THEN
'必须优化:添加索引或改写SQL'
-- 严重:JOIN问题
WHEN Extra LIKE '%Using join buffer%' THEN
'优化JOIN条件或增加索引'
ELSE
'检查其他Extra标签'
END AS optimization_suggestion;
4. 执行计划生成原理与源码分析
4.1 MySQL优化器架构概览
MySQL执行计划的生成过程是一个复杂的多阶段流程:
SQL查询
解析器
Parser
预处理器
Preprocessor
优化器
Optimizer
逻辑优化
Logical Optimization
物理优化
Physical Optimization
生成执行计划
QEP
执行器
Executor
查询重写
外连接消除
子查询优化
分区裁剪
索引选择
JOIN顺序优化
访问方法选择
4.2 源码级别:EXPLAIN的实现机制
4.2.1 EXPLAIN命令的执行路径
在MySQL 8.0源码中,EXPLAIN的实现在sql/sql_explain.cc:
cpp
// MySQL 8.0.35 源码位置:sql/sql_explain.cc
// 函数:explain_query
bool explain_query(THD *thd, SELECT_LEX_UNIT *unit) {
// 步骤1:创建EXPLAIN上下文
Explain_query *explain = new (thd->mem_root) Explain_query;
// 步骤2:解析EXPLAIN选项
if (parse_explain_options(thd, explain)) {
return true;
}
// 步骤3:根据EXPLAIN类型分支处理
switch (explain->type) {
case EXPLAIN_ORDINARY:
// 普通EXPLAIN:不执行查询
return explain_query_without_executing(thd, unit, explain);
case EXPLAIN_ANALYZE:
// EXPLAIN ANALYZE:执行并收集统计信息
return explain_query_with_analyze(thd, unit, explain);
case EXPLAIN_FORMAT_JSON:
case EXPLAIN_FORMAT_TREE:
// 特殊格式输出
return explain_query_with_format(thd, unit, explain);
}
return false;
}
4.2.2 执行计划的数据结构
MySQL使用QEP_TAB(Query Execution Plan Table)对象表示执行计划中的每个表操作:
cpp
// MySQL 8.0.35 源码位置:sql/sql_executor.h
// 结构体:QEP_TAB
struct QEP_TAB {
TABLE *table; // 表对象
Key_use *key_use; // 使用的索引信息
table_map dependent_tables; // 依赖的表
uint used_key_parts; // 使用的索引列数
enum join_type type; // 连接类型(对应EXPLAIN的type)
double quick_prefix_rows; // 预估行数
Item *condition; // WHERE条件
// EXPLAIN相关字段
const char *type_str; // type的字符串表示
const char *key_name; // 索引名称
uint key_length; // 索引长度(key_len)
ha_rows rows; // 预估行数(rows)
float filtered; // 过滤百分比
};
4.2.3 优化器成本计算模型
MySQL优化器基于成本模型选择最优执行计划。每个操作都有对应的成本计算:
cpp
// MySQL 8.0.35 源码位置:sql/opt_costmodel.cc
// 成本计算函数
double cost_model::row_evaluate_cost(double rows) const {
// CPU成本:每行的评估成本
// 默认值:0.1(可配置)
return rows * m_server_cost_constants->row_evaluate_cost();
}
double cost_model::io_block_read_cost(double blocks) const {
// IO成本:读取数据页的成本
// 默认值:1.0(SSD),2.0(HDD)
return blocks * m_server_cost_constants->io_block_read_cost();
}
// 索引扫描总成本
double calculate_index_scan_cost(QEP_TAB *qep_tab) {
double rows = qep_tab->rows; // 预估扫描行数
double blocks = rows / 16; // 预估读取页数(假设每页16行)
double io_cost = io_block_read_cost(blocks);
double cpu_cost = row_evaluate_cost(rows);
return io_cost + cpu_cost;
}
成本模型配置:
sql
-- 查看当前成本模型配置
SELECT * FROM mysql.server_cost;
-- 调整成本模型参数(需重启)
SET GLOBAL row_evaluate_cost = 0.05; -- 降低CPU成本权重
SET GLOBAL io_block_read_cost = 0.5; -- 降低IO成本权重
4.3 执行计划生成流程图
Executor Optimizer Parser Client Executor Optimizer Parser Client alt [普通EXPLAIN] [EXPLAIN ANALYZE] EXPLAIN SELECT ... 解析SQL生成语法树 传递语法树 逻辑优化 (查询重写、子查询优化) 物理优化 (索引选择、JOIN顺序) 计算各方案成本 返回最优执行计划 传递执行计划 实际执行查询 收集执行统计信息 返回执行计划+统计信息
5. 高级分析:EXPLAIN ANALYZE 与 JSON 格式
5.1 EXPLAIN ANALYZE:洞察真实执行成本
EXPLAIN ANALYZE (MySQL 8.0.18+)是诊断SQL性能问题的终极武器,它实际执行查询并收集真实的执行统计信息。
5.1.1 基础用法
sql
-- 基础EXPLAIN:只显示优化器预估
EXPLAIN SELECT * FROM users WHERE age > 20;
-- Output: rows=100000 (预估)
-- EXPLAIN ANALYZE:显示实际执行数据
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;
-- Output: actual rows=98765 (实际)
-- execution time=0.123 sec
5.1.2 输出字段解读
EXPLAIN ANALYZE的输出比基础EXPLAIN多3个关键字段:
| 字段 | 说明 | 示例值 | 用途 |
|---|---|---|---|
| actual time | 实际执行时间(毫秒) | 0.123...12.456 | 第一个值:首行返回时间 第二个值:总执行时间 |
| actual rows | 实际扫描行数 | 98765 | 对比rows字段,判断优化器预估准确性 |
| loops | 循环次数(JOIN相关) | 1 | 对应嵌套循环JOIN的循环数 |
完整示例:
sql
EXPLAIN ANALYZE
SELECT u.name, o.order_id
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;
-- 输出:
-> Filter: (u.age > 20) (cost=10.25 rows=100)
(actual time=0.045..12.342 rows=98765 loops=1)
-> Nested loop inner join
(cost=10.25 rows=100)
(actual time=0.032..10.123 rows=98765 loops=1)
-> Table scan on u
(actual time=0.012..5.234 rows=100000 loops=1)
-> Index lookup on o using idx_user_id (user_id=u.id)
(actual time=0.023..0.045 rows=1 loops=98765)
5.2 JSON格式:结构化的深度分析
EXPLAIN FORMAT=JSON提供了最详细的执行计划信息,特别适合复杂查询分析。
5.2.1 JSON输出结构
sql
EXPLAIN FORMAT=JSON
SELECT * FROM users WHERE age > 20 ORDER BY create_time DESC LIMIT 10;
json
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "10.25"
},
"ordering_operation": {
"using_filesort": true,
"cost_info": {
"sort_cost": "2.50"
},
"table": {
"table_name": "users",
"access_type": "range",
"possible_keys": ["idx_age"],
"key": "idx_age",
"used_key_parts": ["age"],
"key_length": "4",
"rows_examined_per_scan": 10000,
"rows_produced_per_join": 10000,
"filtered": "100.00",
"cost_info": {
"read_cost": "5.25",
"eval_cost": "2.50",
"prefix_cost": "7.75",
"data_read_per_join": "100K"
},
"used_columns": [
"id",
"name",
"age",
"status",
"create_time"
],
"attached_condition": "(`users`.`age` > 20)"
}
}
}
}
5.2.2 关键成本字段详解
| 成本字段 | 说明 | 计算公式 |
|---|---|---|
| read_cost | IO读取成本 | 数据页数 × io_block_read_cost |
| eval_cost | CPU评估成本 | 扫描行数 × row_evaluate_cost |
| prefix_cost | 前置操作累计成本 | 父节点的prefix_cost + read_cost + eval_cost |
| query_cost | 查询总成本 | 根节点的prefix_cost |
成本计算示例:
sql
-- 假设:
-- 扫描行数 = 10000
-- 数据页数 = 625 (10000 / 16)
-- io_block_read_cost = 1.0
-- row_evaluate_cost = 0.1
-- 计算:
read_cost = 625 × 1.0 = 625
eval_cost = 10000 × 0.1 = 1000
prefix_cost = 0 + 625 + 1000 = 1625
5.3 TREE格式:树形可视化(MySQL 8.0.16+)
sql
EXPLAIN FORMAT=TREE
SELECT * FROM users WHERE age > 20;
输出:
-> Filter: (users.age > 20) (cost=10.25 rows=10000)
-> Index range scan on users using idx_age (cost=10.25 rows=10000)
TREE vs JSON对比:
| 特性 | TREE | JSON | TRADITIONAL |
|---|---|---|---|
| 可读性 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 详细程度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 成本信息 | ✅ | ✅ | ✅ |
| 程序解析 | ⚠️ 难 | ✅ 易 | ❌ 不适合 |
| 适用场景 | 人工分析 | 程序分析 | 快速检查 |
6. 实战案例:从执行计划到性能优化
6.1 案例1:优化GROUP BY查询
6.1.1 问题发现
sql
-- 慢查询日志
# Time: 2024-01-15 10:23:45
# Query_time: 3.245678 Lock_time: 0.000123 Rows_sent: 100 Rows_examined: 1000000
SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;
6.1.2 执行计划分析
sql
EXPLAIN SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;
输出:
| id | select_type | table | type | possible_keys | key | key_len | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | users | ALL | idx_create_time | NULL | NULL | 1000000 | 33.33 | Using where; Using temporary; Using filesort |
问题诊断:
- ❌ type=ALL:全表扫描100万行
- ❌ Using temporary:需要临时表做GROUP BY
- ❌ Using filesort:需要外部排序
- ⚠️ filtered=33.33:只过滤了33%的数据
6.1.3 优化方案
优化方案1:添加复合索引(推荐)
sql
-- 添加覆盖索引
ALTER TABLE users
ADD INDEX idx_create_age_status (create_time, age, status);
-- 查看新的执行计划
EXPLAIN SELECT age, COUNT(*), AVG(status)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age
ORDER BY COUNT(*) DESC
LIMIT 100;
新执行计划:
| id | select_type | table | type | possible_keys | key | key_len | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | users | range | idx_create_age_status | idx_create_age_status | 8 | 330000 | 100.00 | Using index; Using temporary; Using filesort |
优化效果:
- ✅ type=range:从全表扫描提升到范围扫描
- ✅ Using index:覆盖索引,避免回表
- ⚠️ 仍有temporary/filesort:但数据量大幅减少
- 📉 扫描行数:从100万降到33万(减少67%)
性能测试:
sql
-- 优化前
SET profiling = 1;
SELECT age, COUNT(*), AVG(status) FROM users WHERE create_time > '2024-01-01' GROUP BY age ORDER BY COUNT(*) DESC LIMIT 100;
SHOW PROFILE;
-- Duration: 3.245678 sec
-- 优化后
SELECT age, COUNT(*), AVG(status) FROM users WHERE create_time > '2024-01-01' GROUP BY age ORDER BY COUNT(*) DESC LIMIT 100;
SHOW PROFILE;
-- Duration: 0.456789 sec
-- 性能提升:7.1倍
优化方案2:松散索引扫描(Loose Index Scan,特殊情况)
sql
-- 适用于:GROUP BY的字段是索引的前缀
-- 查询满足松散索引扫描条件:
-- 1. 单表查询
-- 2. GROUP BY使用索引前缀
-- 3. 无其他聚合函数(COUNT/MIN/MAX除外)
EXPLAIN SELECT age, COUNT(*)
FROM users
WHERE create_time > '2024-01-01'
GROUP BY age;
-- Extra: Using index for group-by(松散索引扫描)
松散索引扫描 vs 紧凑索引扫描:
索引前缀
非索引前缀
索引扫描方式
GROUP BY字段
松散索引扫描
Using index for group-by
紧凑索引扫描
Using index
性能: ⭐⭐⭐⭐⭐
只扫描索引值
性能: ⭐⭐⭐
扫描所有索引行
6.2 案例2:优化子查询性能
6.2.1 问题SQL
sql
-- 查找年龄大于平均年龄的用户
SELECT * FROM users
WHERE age > (SELECT AVG(age) FROM users);
执行计划:
| id | select_type | table | type | rows | Extra |
|---|---|---|---|---|---|
| 1 | PRIMARY | users | ALL | 1000000 | Using where |
| 2 | SUBQUERY | users | ALL | 1000000 |
问题:
- ❌ DEPENDENT SUBQUERY:相关子查询(虽然这里写法不相关,但优化器可能处理为相关)
- ❌ 两次全表扫描
6.2.2 优化方案
方案1:改写为JOIN(推荐)
sql
-- 改写为JOIN
SELECT u1.*
FROM users u1
CROSS JOIN (SELECT AVG(age) AS avg_age FROM users) AS u2
WHERE u1.age > u2.avg_age;
新执行计划:
| id | select_type | table | type | rows | Extra |
|---|---|---|---|---|---|
| 1 | PRIMARY | system | 1 | ||
| 1 | PRIMARY | u1 | ALL | 1000000 | Using where |
| 2 | DERIVED | users | ALL | 1000000 |
性能提升:
- 子查询只执行一次(DERIVED表)
- 避免了相关子查询的重复执行
方案2:使用变量(特定场景)
sql
SET @avg_age = (SELECT AVG(age) FROM users);
SELECT * FROM users WHERE age > @avg_age;
方案3:物化优化(MySQL 8.0+)
sql
-- MySQL 8.0会自动将子查询物化
SELECT * FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- 执行计划中会出现:
-- select_type: MATERIALIZED
6.3 案例3:优化JOIN查询
6.3.1 问题SQL
sql
-- 查询用户及其最近订单
SELECT u.name, o.order_id, o.create_time
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
ORDER BY o.create_time DESC
LIMIT 10;
执行计划:
| id | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | u | ref | idx_status | idx_status | 500000 | Using where; Using temporary; Using filesort |
| 1 | o | ref | idx_user_id | idx_user_id | 5 |
问题:
- ❌ Using temporary:需要临时表去重(LEFT JOIN + 多行orders)
- ❌ Using filesort:需要外部排序
6.3.2 优化方案
方案1:使用子查询先聚合
sql
SELECT u.name, latest.order_id, latest.create_time
FROM users u
LEFT JOIN (
SELECT user_id, MAX(create_time) AS max_time
FROM orders
GROUP BY user_id
) AS latest ON u.id = latest.user_id
LEFT JOIN orders o ON latest.user_id = o.id AND o.create_time = latest.max_time
WHERE u.status = 1
ORDER BY o.create_time DESC
LIMIT 10;
方案2:使用窗口函数(MySQL 8.0+)
sql
SELECT u.name, o.order_id, o.create_time
FROM (
SELECT
user_id,
order_id,
create_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn
FROM orders
) AS o
JOIN users u ON o.user_id = u.id
WHERE u.status = 1 AND o.rn = 1
ORDER BY o.create_time DESC
LIMIT 10;
执行计划对比:
| 优化方案 | 扫描行数 | 临时表 | 文件排序 | 性能提升 |
|---|---|---|---|---|
| 原始SQL | 250万+ | ✅ | ✅ | 基准 |
| 子查询聚合 | 100万 | ✅ | ✅ | 2.5x |
| 窗口函数 | 100万 | ✅ | ❌ | 3x |
7. 最佳实践与常见陷阱
7.1 执行计划分析检查清单
在生产环境分析执行计划时,按照以下检查清单系统化排查:
sql
-- 执行计划健康度检查脚本
SELECT
CONCAT(
'检查项',
CASE
WHEN type IN ('const', 'eq_ref', 'ref') THEN '✅ 优秀'
WHEN type = 'range' THEN '⚠️ 良好'
WHEN type IN ('index', 'ALL') THEN '❌ 需优化'
ELSE '❓ 未知'
END,
' | ',
'访问类型: ', type,
' | ',
'扫描行数: ', rows,
' | ',
'Extra: ', IFNULL(Extra, '无')
) AS health_check
FROM (
-- 替换为你的EXPLAIN结果
SELECT * FROM (
EXPLAIN SELECT * FROM users WHERE age = 25
) AS explain_result
) AS t;
7.2 常见陷阱与规避方法
陷阱1:过度依赖EXPLAIN的预估
问题:EXPLAIN的rows字段是优化器预估,可能与实际相差巨大。
sql
-- 示例:预估偏差
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Output: rows=1000 (预估)
EXPLAIN ANALYZE SELECT * FROM users WHERE age = 25;
-- Output: actual rows=9876 (实际相差10倍!)
原因:
- 统计信息过期(ANALYZE TABLE未及时执行)
- 数据分布不均匀
- 相关性未考虑(多列条件的相关性)
解决方案:
sql
-- 定期更新统计信息
ANALYZE TABLE users;
-- 使用EXPLAIN ANALYZE获取真实数据
EXPLAIN ANALYZE SELECT ...;
-- 启用直方图(MySQL 8.0+)
ANALYZE TABLE users UPDATE HISTOGRAM ON age, status WITH 100 BUCKETS;
陷阱2:忽视filtered字段
问题:只关注rows,忽略filtered百分比。
sql
-- 示例:高rows但高过滤
EXPLAIN SELECT * FROM users WHERE age > 20 AND status = 1;
-- rows=100000, filtered=10.00
-- 实际有效行数 = 100000 × 10% = 10000
最佳实践:
sql
-- 计算实际有效行数
SELECT
table,
rows AS 预估扫描行数,
filtered AS 过滤百分比,
ROUND(rows * filtered / 100, 0) AS 实际有效行数
FROM EXPLAIN SELECT * FROM users WHERE age > 20 AND status = 1;
陷阱3:忽略索引选择性
问题:添加了索引但未考虑选择性。
sql
-- 低选择性索引( gender 只有2个值)
ALTER TABLE users ADD INDEX idx_gender (gender);
-- EXPLAIN显示:type=ref, rows=500000(几乎全表)
索引选择性计算:
sql
-- 计算索引选择性(越接近1越好)
SELECT
COUNT(DISTINCT age) / COUNT(*) AS age_selectivity,
COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM users;
-- 输出:
-- age_selectivity: 0.95 (高选择性,适合建索引)
-- gender_selectivity: 0.02 (低选择性,不适合建索引)
最佳实践:
- ✅ 选择性 > 0.1:适合建索引
- ⚠️ 选择性 0.01-0.1:考虑复合索引
- ❌ 选择性 < 0.01:不适合建索引
陷阱4:ORDER BY误用导致filesort
问题:索引列顺序与ORDER BY不一致。
sql
-- 索引:idx_age_status (age, status)
-- 查询:
SELECT * FROM users
WHERE age = 25
ORDER BY status; -- ✅ 无filesort
SELECT * FROM users
WHERE status = 1
ORDER BY age; -- ❌ 出现filesort
最佳实践:
sql
-- ORDER BY遵循索引最左前缀原则
-- ✅ 正确:ORDER BY age, status
-- ✅ 正确:ORDER BY age(部分前缀)
-- ❌ 错误:ORDER BY status, age(跳过前缀)
-- ❌ 错误:ORDER BY age DESC, status ASC(方向不一致)
7.3 EXPLAIN在不同MySQL版本的差异
| 特性 | MySQL 5.7 | MySQL 8.0 | 差异说明 |
|---|---|---|---|
| EXPLAIN ANALYZE | ❌ 不支持 | ✅ 支持 | 8.0提供真实执行统计 |
| 直方图 | ❌ 不支持 | ✅ 支持 | 8.0统计信息更精准 |
| 不可见索引 | ❌ 不支持 | ✅ 支持 | 8.0可测试索引效果 |
| 窗口函数 | ❌ 不支持 | ✅ 支持 | 8.0新增执行计划类型 |
| ** descending index** | ❌ 不支持 | ✅ 支持 | 8.0支持降序索引 |
| Skip Scan | ❌ 不支持 | ✅ 支持 | 8.0新增扫描方式 |
版本升级建议:
sql
-- 检查当前版本
SELECT VERSION();
-- 利用8.0新特性优化查询
-- 示例:直方图辅助优化器
ANALYZE TABLE users UPDATE HISTOGRAM ON age, status WITH 100 BUCKETS;
-- 示例:不可见索引测试
ALTER TABLE users ALTER INDEX idx_age INVISIBLE;
-- 测试性能
ALTER TABLE users ALTER INDEX idx_age VISIBLE;
8. 总结与展望
8.1 核心要点回顾
本文系统性地解析了MySQL EXPLAIN执行计划的方方面面,以下是核心知识图谱:
EXPLAIN执行计划
基础
EXPLAIN
EXPLAIN ANALYZE
FORMAT=JSON/TREE
核心字段
id
select_type
type⭐
key/key_len
rows/filtered
Extra⭐
优化方向
索引优化
查询改写
结构调整
高级技巧
成本模型
源码分析
版本差异
8.2 执行计划分析思维模型
建立科学的SQL优化思维模型:
ALL/index
range/ref
filesort
temporary
Using where
否
是
发现慢查询
EXPLAIN基础分析
type字段检查
索引优化
进一步分析
添加/调整索引
Extra检查
优化ORDER BY
优化GROUP BY
检查覆盖索引
EXPLAIN验证
满意?
EXPLAIN ANALYZE
完成
实际执行分析
优化器成本调整
8.3 进阶学习路径
-
源码级深入:
- 阅读
sql/sql_optimizer.cc(优化器主逻辑) - 研究
sql/sql_executor.cc(执行器实现) - 理解成本模型(
sql/opt_costmodel.cc)
- 阅读
-
实战能力提升:
- 收集100+个慢查询案例
- 建立执行计划分析checklist
- 总结各业务场景的优化模式
-
工具生态:
- MySQL Workbench可视化执行计划
- pt-query-digest(Percona Toolkit)
- MySQL Enterprise Monitor
8.4 未来展望
MySQL执行计划技术仍在持续演进:
- AI驱动优化:MySQL 9.0引入机器学习优化器
- 实时自适应:根据实际执行动态调整计划
- 云原生优化:针对分布式架构的执行计划改进
📚 参考资料
-
MySQL官方文档
-
源码阅读
- MySQL 8.0.35 Source Code:
sql/sql_explain.cc - MySQL 8.0.35 Source Code:
sql/sql_optimizer.cc
- MySQL 8.0.35 Source Code:
-
推荐文章
-
工具
🎯 练习题
练习1:分析以下SQL的执行计划并优化
sql
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND u.age > 20
ORDER BY o.create_time DESC
LIMIT 100;
练习2:为以下查询设计最优索引
sql
SELECT COUNT(*)
FROM users
WHERE age BETWEEN 20 AND 30
AND status = 1;
练习3:解释为什么会出现Using temporary并优化
sql
SELECT age, COUNT(*)
FROM users
GROUP BY age
ORDER BY MAX(create_time);
恭喜你完成了MySQL执行计划的深度学习之旅! 🎉
掌握了EXPLAIN,你就掌握了SQL性能优化的金钥匙。继续实践,持续精进!
标签 :[MySQL] [执行计划] [EXPLAIN] [查询优化] [数据库调优] [性能分析] [源码解析]
版权声明 :本文为原创技术文章,转载请注明出处。
作者简介:[你的名字] - 数据库性能优化专家,专注于MySQL内核研究与实战优化。