定义
执行计划 (Execution Plan) 是数据库优化器为执行一条 SQL 语句而生成的详细操作步骤序列。它描述了数据库将如何访问数据(全表扫描还是索引扫描)、使用哪些索引、表的连接顺序、排序方法等关键信息。
通过 EXPLAIN 命令可以查看执行计划,这是 SQL 性能分析和优化的核心工具。
详细笔记
核心原理
优化器的工作流程
SQL 语句
↓
解析器(Parser):语法分析,生成解析树
↓
预处理器:语义检查,权限验证
↓
优化器(Optimizer):
- 生成多个候选执行计划
- 估算每个计划的成本(Cost)
- 选择成本最低的计划
↓
执行器(Executor):按执行计划执行
↓
返回结果EXPLAIN 基本用法
sql
-- 基本语法
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- MySQL 8.0:更详细的 JSON 格式
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = '张三';
-- PostgreSQL
EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';
-- SQL Server
SET SHOWPLAN_ALL ON;
SELECT * FROM users WHERE name = '张三';MySQL EXPLAIN 输出详解
sql
EXPLAIN SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 25
ORDER BY o.create_time DESC
LIMIT 10;典型输出:
+----+-------------+-------+------------+--------+---------------+--------------+---------+-----------------+------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+--------------+---------+-----------------+------+----------+----------------------------------------------+
| 1 | SIMPLE | u | NULL | range | PRIMARY,idx_age| idx_age | 5 | NULL | 5000 | 100.00 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | o | NULL | ref | idx_user_id | idx_user_id | 5 | test.u.id | 10 | 100.00 | Using where |
+----+-------------+-------+------------+--------+---------------+--------------+---------+-----------------+------+----------+----------------------------------------------+关键字段解读
一、id
表示查询中执行 SELECT 子句或操作表的顺序:
- id 相同:从上到下执行
- id 不同:id 越大越先执行(子查询优先)
sql
-- 示例:子查询
EXPLAIN SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
-- 输出:
-- id: 1 (外层查询)
-- id: 2 (子查询,先执行)二、select_type
查询类型:
| 值 | 说明 | 示例 |
|---|---|---|
| SIMPLE | 简单查询(无子查询、无 UNION) | SELECT * FROM users |
| PRIMARY | 外层查询(包含子查询时) | SELECT * FROM (SELECT ...) AS t |
| SUBQUERY | 子查询 | SELECT ... WHERE id = (SELECT ...) |
| DERIVED | 派生表(FROM 子句中的子查询) | SELECT * FROM (SELECT ...) AS t |
| UNION | UNION 中的第二个或后面的查询 | SELECT ... UNION SELECT ... |
| UNION RESULT | UNION 的结果 | UNION RESULT |
三、table
当前行正在访问的表名:
- 实际表名:
users,orders - 别名:
u,o - 派生表:
<derived2>(表示 id=2 的派生表) - UNION 结果:
<union1,2>
四、type(重要!)
访问类型,从最好到最差:
system > const > eq_ref > ref > range > index > ALL1. system(最优)
- 表只有一行记录(系统表)
- 极少见
2. const
- 通过主键或唯一索引等值查询
- 最多返回一行
sql
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type: const3. eq_ref
- 连接查询中,使用主键或唯一索引等值匹配
- 对于前表的每一行,后表最多返回一行
sql
EXPLAIN SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- o 表的 type: eq_ref(如果 user_id 是唯一索引)4. ref
- 使用普通索引等值查询
- 可能返回多行
sql
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- type: ref(假设 name 是普通索引)5. range
- 索引范围扫描
- 适用于:
BETWEEN,>,<,IN,LIKE '张%'
sql
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
-- type: range6. index
- 全索引扫描
- 比全表扫描快,因为索引通常比数据小
sql
EXPLAIN SELECT name FROM users;
-- type: index(扫描整个索引树)7. ALL(最差)
- 全表扫描
- 需要优化
sql
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- type: ALL(没有索引)五、possible_keys
可能使用的索引列表:
- 优化器考虑使用的索引
- 如果为 NULL,表示没有可用的索引
六、key(重要!)
实际使用的索引:
- 如果为 NULL,表示未使用索引
- 可能与
possible_keys不同(优化器最终选择的索引)
七、key_len
使用的索引长度(字节数):
- 用于判断复合索引使用了多少列
- 计算规则:
INT:4 字节BIGINT:8 字节VARCHAR(n):n * 字符集字节数 + 2DATETIME:8 字节- 允许 NULL:额外 +1 字节
sql
CREATE INDEX idx_name_age ON users(name VARCHAR(50), age INT);
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- key_len: 152 (50*3 + 2,UTF8MB3)
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;
-- key_len: 157 (152 + 4 + 1,NULL 标识)八、ref
与索引比较的列或常量:
const:与常量比较database.table.column:与其他列比较NULL:范围查询或函数
九、rows(重要!)
估算需要扫描的行数:
- 基于统计信息的估算值
- 越小越好
- 与实际行数可能有差异
十、filtered
存储引擎返回的数据在经过过滤后,剩下记录的比例:
- 百分比,最大值 100%
- 越高越好
sql
EXPLAIN SELECT * FROM users WHERE age > 25;
-- rows: 10000
-- filtered: 50.00
-- 意味着:扫描 10000 行,过滤后剩下 5000 行十一、Extra(重要!)
额外信息,常见值:
优化相关:
Using index:覆盖索引,无需回表 ✅Using index condition:索引下推(ICP) ✅Using where:服务器层进行 WHERE 过滤Using temporary:使用临时表 ⚠️Using filesort:使用文件排序(非索引排序) ⚠️Using join buffer:使用连接缓冲 ⚠️
性能警告:
Using filesort:需要优化 ORDER BYUsing temporary:需要优化 GROUP BY 或 DISTINCTRange checked for each record:索引选择不佳
EXPLAIN FORMAT=JSON
MySQL 5.6+ 支持 JSON 格式,提供更详细的信息:
sql
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = '张三';输出示例:
json
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1234.56"
},
"table": {
"table_name": "users",
"access_type": "ref",
"possible_keys": ["idx_name"],
"key": "idx_name",
"used_key_parts": ["name"],
"key_length": "152",
"ref": ["const"],
"rows_examined_per_scan": 10,
"rows_produced_per_join": 10,
"filtered": "100.00",
"cost_info": {
"read_cost": "1230.00",
"eval_cost": "4.56",
"prefix_cost": "1234.56"
},
"used_index_conditions": [
"name = '张三'"
]
}
}
}优势:
- 包含成本估算(
query_cost) - 显示实际使用的索引列(
used_key_parts) - 显示每步的成本(
cost_info)
实际优化案例
案例一:添加索引优化全表扫描
sql
-- 问题查询
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:
-- type: ALL
-- key: NULL
-- rows: 1000000
-- 优化:添加索引
CREATE INDEX idx_email ON users(email);
-- 优化后
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:
-- type: ref
-- key: idx_email
-- rows: 1案例二:优化 ORDER BY
sql
-- 问题查询
EXPLAIN SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC;
-- 输出:
-- type: ref
-- key: idx_user_id
-- Extra: Using filesort ⚠️
-- 优化:创建复合索引
CREATE INDEX idx_user_time ON orders(user_id, create_time);
-- 优化后
EXPLAIN SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC;
-- 输出:
-- type: ref
-- key: idx_user_time
-- Extra: (空,利用索引排序) ✅案例三:优化 JOIN
sql
-- 问题查询
EXPLAIN SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;
-- 输出:
-- users: type: ALL, rows: 100000
-- orders: type: ALL, rows: 1000000
-- 优化:添加索引
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_amount ON orders(amount);
-- 优化后
-- users: type: const, rows: 1
-- orders: type: ref, rows: 10不同数据库的执行计划
PostgreSQL
sql
EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';输出:
Seq Scan on users (cost=0.00..1234.56 rows=10 width=100) (actual time=5.123..10.456 rows=8 loops=1)
Filter: (name = '张三'::text)
Rows Removed by Filter: 999992
Planning Time: 0.123 ms
Execution Time: 10.567 ms特点:
ANALYZE:实际执行并显示真实时间cost:估算成本(启动成本..总成本)actual time:实际执行时间Planning Time:优化器耗时Execution Time:执行耗时
SQL Server
sql
SET SHOWPLAN_ALL ON;
SELECT * FROM users WHERE name = '张三';或通过 SSMS 图形化查看执行计划。
监控与诊断
Performance Schema(MySQL 8.0+)
sql
-- 查看最近执行的语句的性能
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT,
AVG_TIMER_WAIT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;慢查询日志
sql
-- 启用慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒的记录
-- 查看慢查询
mysqldumpslow -s t /var/lib/mysql/slow.log最佳实践
定期审查慢查询:通过慢查询日志找出性能问题
关注 type 字段:确保关键查询至少达到
range级别检查 Extra 字段:避免
Using filesort和Using temporary验证 rows 估算:与实际返回行数对比,差距过大可能需要更新统计信息
使用 EXPLAIN FORMAT=JSON:获取更详细的成本信息
结合实际情况:执行计划只是参考,最终要以实际性能测试为准
关联术语
- [[索引选择性]]
- [[回表]]
- [[最左前缀原则]]
- [[统计信息]]
- [[索引下推]]
参考资料
- MySQL 官方文档: EXPLAIN Output Format
- PostgreSQL 文档: Using EXPLAIN
- 《高性能 MySQL》第 4 章:查询性能优化
- Use The Index, Luke: Explain Your Query