SQL 执行计划解析:从 EXPLAIN 到性能优化的完整指南 - 指南

SQL 执行计划解析:从 EXPLAIN 到性能优化的完整指南

一、为什么要理解执行计划

SQL 性能瓶颈的常见原因

SQL性能瓶颈
全表扫描
索引失效
JOIN顺序不当
临时表使用
排序文件外

执行计划的价值​​:

  • ​​提前发现问题​​:在SQL执行前预测性能

  • ​​​​优化依据​​:提供具体的优化方向

​​- ​​学习工具​​:理解数据库工作原理

性能问题统计

根据对生产环境SQL问题的分析

  • ​​​​65%​​ 的慢查询源于索引使用不当

​​- ​​20%​​ 由于JOIN操作效率低下

​​- ​​10%​​ 因为数据量过大缺乏分页

​​- ​​5%​​ 其他原因(锁竞争、配置问题等)

⚡ 二、EXPLAIN 命令详解

基本用法与输出格式

​​EXPLAIN 基本命令​​

-- 基本用法
EXPLAIN SELECT * FROM users WHERE age >
25;
-- 详细格式(MySQL 8.0+)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age >
25;
-- 实际执行计划(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE age >
25;

​​典型EXPLAIN输出​​

+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 1000 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

执行计划解读框架

EXPLAIN输出
查询类型分析
访问方法分析
索引使用分析
额外信息分析
select_type
type
key, rows
Extra

三、关键字段深度解析

id - 查询标识符
​​id 字段的含义​​:

  • ​​相同id​​:同一查询层级

  • ​​递增id​​:子查询或UNION操作

  • ​​NULL id​​:派生表或临时结果

​​示例分析​​

-- 复杂查询示例
EXPLAIN
SELECT u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u
WHERE u.status = 'active';

​​输出解读​​

+----+--------------------+-------+------------+------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | PRIMARY | u | NULL | ref | idx_status | idx_status | 102 | const| 500 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | o | NULL | ref | idx_user_id | idx_user_id | 5 | func | 10 | 100.00 | Using index |
+----+--------------------+-------+------------+------+---------------+---------+---------+------+------+----------+--------------------------+

select_type - 查询类型

​​常见 select_type 值​​

类型描述出现场景
SIMPLE简单查询不含子查询或UNION
PRIMARY最外层查询复杂查询的主查询
SUBQUERY子查询WHERE子句中的子查询
DERIVED派生表FROM子句中的子查询
UNIONUNION查询UNION第二个及以后的查询
UNION RESULTUNION结果UNION结果的临时表

️ type - 访问类型(性能关键)

​​type 性能等级​​(从优到劣):

type性能等级
system
const
eq_ref
ref
range
index
ALL

​​详细解释​​

-- 最佳情况:const(主键或唯一索引等值查询)
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type: const
-- 良好情况:ref(非唯一索引等值查询)
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
-- type: ref
-- 最差情况:ALL(全表扫描)
EXPLAIN SELECT * FROM users WHERE name LIKE '%john%';
-- type: ALL

key - 实际使用的索引

​​索引使用分析​​:

-- 创建测试索引
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_city ON users(city);
-- 查看索引选择
EXPLAIN SELECT * FROM users WHERE age >
25 AND city = 'Beijing';

​​可能输出​​:

+----+-------------+-------+------------+------+-------------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-------------------+---------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | users | NULL | ref | idx_age,idx_city | idx_city| 102 | const | 100 | 50.00 | Using where |
+----+-------------+-------+------------+------+-------------------+---------+---------+-------+------+----------+-------------+

rows - 预估扫描行数

​​rows 字段的重要性​​

  • 数值越小越好​​:表示需要处理的数据量少

  • ​​基于统计信息​​:可能不准确,需要定期分析表

  • ​​优化参考​​:帮助识别全表扫描

-- 优化前:全表扫描
EXPLAIN SELECT * FROM users WHERE name LIKE '%john%';
-- rows: 10000(扫描所有行)
-- 优化后:索引扫描
EXPLAIN SELECT * FROM users WHERE email = 'john@example.com';
-- rows: 1(只扫描1行)

Extra - 额外信息(关键提示)

​​重要 Extra 值解读​​

Extra 值含义优化建议
Using where使用WHERE过滤正常,无需优化
Using index覆盖索引优秀,无需回表
Using temporary使用临时表考虑优化查询或索引
Using filesort文件排序添加合适的索引
Range checked for each record索引选择困难优化查询条件

️ 四、实战案例分析

案例一:单表查询优化

​​问题查询​​:

SELECT * FROM products
WHERE category = 'electronics'
AND price BETWEEN 1000 AND 2000
ORDER BY created_at DESC;

​​初始执行计划​​:

EXPLAIN SELECT * FROM products
WHERE category = 'electronics'
AND price BETWEEN 1000 AND 2000
ORDER BY created_at DESC;

​​输出分析​​

+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
| 1 | SIMPLE | products | NULL | ALL | NULL | NULL | NULL | NULL | 5000 | 5.00 | Using where;
Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+

问题诊断​​

  • type: ALL:全表扫描

  • Using filesort:文件排序,性能差

  • 缺少合适索引

​​优化方案​​

-- 创建复合索引
CREATE INDEX idx_category_price_created
ON products(category, price, created_at);
-- 再次查看执行计划
EXPLAIN SELECT * FROM products
WHERE category = 'electronics'
AND price BETWEEN 1000 AND 2000
ORDER BY created_at DESC;

​​优化后输出​​

+----+-------------+----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-------+
| 1 | SIMPLE | products | NULL | range | idx_category_price_created| idx_category_price_created| 108 | NULL | 50 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-------+

案例二:多表 JOIN 优化

​​问题查询​​​​:

SELECT u.name, o.order_date, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.city = 'Beijing'
AND o.status = 'completed'
ORDER BY o.order_date DESC;

​​​​初始执行计划​​​​:

EXPLAIN SELECT u.name, o.order_date, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.city = 'Beijing'
AND o.status = 'completed'
ORDER BY o.order_date DESC;

​​​​输出分析​​​​:

+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+---------------------------------+
| 1 | SIMPLE | u | NULL | ALL | PRIMARY | NULL | NULL | NULL | 1000 | 10.00 | Using where;
Using filesort |
| 1 | SIMPLE | o | NULL | eq_ref | PRIMARY | PRIMARY | 4 | test.u.id | 1 | 10.00 | Using where |
+----+-------------+-------+------------+--------+---------------+---------+---------+-------------------+------+----------+---------------------------------+

问题诊断​​:

  • u表全表扫描

  • 连接顺序可能不是最优

  • 缺少合适的索引

​​优化方案​​

-- 为users表添加城市索引
CREATE INDEX idx_city ON users(city);
-- 为orders表添加状态索引
CREATE INDEX idx_status_user ON orders(status, user_id);
-- 使用STRAIGHT_JOIN强制连接顺序
EXPLAIN SELECT STRAIGHT_JOIN u.name, o.order_date, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.city = 'Beijing'
AND o.status = 'completed'
ORDER BY o.order_date DESC;

​​优化后输出​​:

+----+-------------+-------+------------+------+---------------------+---------------------+---------+--------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+---------------------+---------+--------------------+------+----------+---------------------------------+
| 1 | SIMPLE | u | NULL | ref | PRIMARY,idx_city | idx_city | 102 | const | 100 | 100.00 | Using temporary;
Using filesort |
| 1 | SIMPLE | o | NULL | ref | idx_status_user | idx_status_user | 104 | test.u.id,const | 5 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------------+---------------------+---------+--------------------+------+----------+---------------------------------+

案例三:索引覆盖优化

​​问题查询​​​​:

SELECT COUNT(*) FROM user_actions
WHERE user_id = 1001 AND action_date >
'2023-01-01';

​​​​初始情况​​​​:

-- 现有索引:INDEX (user_id)
EXPLAIN SELECT COUNT(*) FROM user_actions
WHERE user_id = 1001 AND action_date >
'2023-01-01';

​​​​输出分析​​​​:

+----+-------------+-------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | user_actions| NULL | ref | idx_user_id | idx_user_id | 5 | const | 500 | 33.33 | Using where |
+----+-------------+-------------+------------+------+---------------+---------+---------+-------+------+----------+-------------+

优化方案​​:

-- 创建覆盖索引
CREATE INDEX idx_user_id_action_date
ON user_actions(user_id, action_date);
-- 再次执行
EXPLAIN SELECT COUNT(*) FROM user_actions
WHERE user_id = 1001 AND action_date >
'2023-01-01';

​​优化后输出​​:

+----+-------------+-------------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | user_actions| NULL | range | idx_user_id_action_date | idx_user_id_action_date | 8 | NULL | 100 | 100.00 | Using where;
Using index |
+----+-------------+-------------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+--------------------------+

优化效果​​

  • Using index:实现索引覆盖,无需回表

  • 查询性能提升 3-5 倍

五、总结与优化指南

执行计划分析 checklist

​​快速诊断流程​​:

查看执行计划
type = ALL?
全表扫描警告
Extra有filesort?
添加合适索引
优化排序字段索引
rows值过大?
优化查询条件
查询已优化

⚡ 优化黄金法则

​​索引优化策略​​

  • 最左前缀原则​​:复合索引的顺序很重要

  • ​​覆盖索引​​:尽可能让索引包含查询字段

  • ​​索引选择性​​:选择高区分度的列建索引

​​查询优化技巧​​:

-- 避免 SELECT *
SELECT id, name, email FROM users;
-- 只选择需要的字段
-- 优化 WHERE 条件
SELECT * FROM products WHERE price >
1000;
-- 使用范围查询
SELECT * FROM products WHERE id IN (1,2,3);
-- 使用IN而不是OR
-- 合理使用 LIMIT
SELECT * FROM large_table LIMIT 1000;
-- 限制返回行数

高级优化工具

​​MySQL 8.0 新特性​​

-- 执行计划可视化
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE age >
25;
-- 实际执行分析
EXPLAIN ANALYZE SELECT * FROM users WHERE age >
25;
-- 索引建议
SELECT * FROM sys.schema_index_statistics;

​​性能监控脚本​​:

-- 查找全表扫描的查询
SELECT * FROM sys.statements_with_full_table_scans;
-- 查找需要索引的查询
SELECT * FROM sys.statements_with_errors_or_warnings;

持续学习资源

​​推荐分析命令​​

-- 定期分析表统计信息
ANALYZE TABLE users;
-- 检查索引使用情况
SHOW INDEX FROM users;
-- 监控慢查询
SHOW VARIABLES LIKE 'slow_query_log%';
posted @ 2025-10-11 13:47  yxysuanfa  阅读(30)  评论(0)    收藏  举报