SQL 执行计划(Execution Plan)是数据库优化器生成的、描述 SQL 语句如何被执行的“路线图”。它告诉你:数据库会用什么顺序访问表、走不走索引、怎么 JOIN、是否排序/分组、预计成本是多少,是性能调优的核心依据。
下面按「概念 → 怎么看 → 关键指标 → 常见算子 → 实战解读 → 调优思路」系统讲一遍。
一、什么是执行计划
执行计划本质是一棵操作符树:
- 每个节点是一个“执行算子”(Table Scan、Index Seek、Nested Loop Join 等)
- 数据从叶子节点向上流动,经过过滤、连接、排序等操作
- 优化器会在多个候选计划中,选一个“代价最低”的
作用:
- 判断 SQL 是否走索引
- 发现不合理的 JOIN 顺序
- 定位慢的根本原因(全表扫描、大排序、临时表等)
二、如何查看执行计划
1. MySQL / MariaDB
-- 实际执行 + 成本信息(推荐)
EXPLAIN ANALYZE SELECT ...;
-- 仅预估计划
EXPLAIN SELECT ...;
-- 格式化输出(8.0+)
EXPLAIN FORMAT=JSON SELECT ...;
2. PostgreSQL
EXPLAIN SELECT ...; -- 预估
EXPLAIN ANALYZE SELECT ...; -- 实际执行(会真的跑)
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
3. Oracle
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
4. SQL Server
SET STATISTICS PROFILE ON;
-- 执行 SQL
SET STATISTICS PROFILE OFF;
-- 或用 SSMS:Ctrl + L 显示预估计划
三、执行计划中的关键字段(以 MySQL 为例)
| 字段 | 含义 |
|---|---|
id |
执行顺序,id 越大越先执行;相同 id 从上到下 |
select_type |
查询类型(SIMPLE、PRIMARY、SUBQUERY 等) |
table |
当前操作的表 |
type |
访问类型(最重要之一) |
possible_keys |
可能使用的索引 |
key |
实际使用的索引 |
rows |
预估扫描行数 |
filtered |
条件过滤后的行占比 |
Extra |
额外信息(Using index、Using temporary、Using filesort 等) |
四、type(访问类型)优劣排序(MySQL)
从好到坏大致是:
system
const
eq_ref
ref
range
index
ALL
- const / eq_ref:主键 / 唯一索引等值查询,极快
- ref:普通索引等值查询
- range:索引范围扫描(BETWEEN、IN、>、<)
- index:全索引扫描(比 ALL 好一点)
- ALL:全表扫描 ❌(通常要优化)
五、常见执行算子 & Extra 信息
1. 扫描类
ALL:全表扫描,数据量大时必须警惕index:扫描整个索引树,通常出现在覆盖索引但无过滤条件range:范围扫描,常见于WHERE id > 100
2. JOIN 算法
- Nested Loop Join
- 驱动表一行一行去匹配被驱动表
- 被驱动表必须有高效索引
- Hash Join(MySQL 8.0+、PG、Oracle)
- 适合大表 JOIN,内存中建哈希表
- Merge Join
- 两个表都按 JOIN 列排序后合并
3. Extra 中的“危险信号”
| Extra | 含义 | 建议 |
|---|---|---|
| Using filesort | 外部排序(无可用索引) | 加 ORDER BY 索引 |
| Using temporary | 使用临时表 | 优化 GROUP BY / DISTINCT |
| Using join buffer | JOIN 没用到索引 | 给被驱动表加索引 |
| Using where | 在存储引擎层之后过滤 | 正常,但可结合索引优化 |
六、一个完整解读示例
EXPLAIN ANALYZE
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.create_time >= '2025-01-01'
ORDER BY o.create_time;
理想情况:
orders.type = range,走idx_create_timecustomers.type = eq_ref,走主键idExtra: Using index(覆盖索引)- 无
Using filesort、无Using temporary
问题情况:
orders.type = ALL→ 全表扫描customers.type = ALL→ JOIN 无索引Using filesort→create_time未参与索引顺序
七、基于执行计划的调优思路
1. 减少扫描行数
- 确保 WHERE 条件能用索引
- 避免对索引列做函数操作:
-- 不好 WHERE DATE(create_time) = '2025-01-01' -- 好 WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
2. 合理设计联合索引
遵循 最左前缀原则:
INDEX (a, b, c)
-- 能用到:a | a,b | a,b,c
-- 不能用:b | b,c
将 等值条件字段放前面,范围条件放后面。
3. 控制 JOIN 顺序
- 小表驱动大表
- 确保被驱动表的 JOIN 字段有索引
4. 避免隐式转换
-- customer_id 是 VARCHAR
WHERE customer_id = 123; -- 会导致索引失效
5. 利用覆盖索引
-- 只查索引列,避免回表
SELECT id, status FROM orders WHERE status = 'PAID';
-- INDEX(status, id)
八、不同数据库的差异(简要)
| 数据库 | 特点 |
|---|---|
| MySQL | 优化器相对简单,索引设计影响极大 |
| PostgreSQL | 支持 Hash Join、并行执行,统计信息更重要 |
| Oracle | CBO 非常成熟,Hint 丰富 |
| SQL Server | 图形化执行计划强,关注“实际行数 vs 预估行数” |
九、一个实用口诀
先看 type,再看 key,rows 多少心里知;
filesort 和 temp,Extra 里找问题;
JOIN 看驱动表,被驱要有索引;
范围放最后,等值放前头。