📚 Postgresql explain执行计划详解
PostgreSQL 的 EXPLAIN 命令是性能调优的核心工具,用于查看 SQL 查询的执行计划(Execution Plan)。理解执行计划能帮助你识别性能瓶颈、优化索引、调整查询结构。下面是对 PostgreSQL EXPLAIN 执行计划的详细详解。
一、基本语法
<SQL>
sql
EXPLAIN [ ( option [, ...] ) ] statement
常用选项(可组合使用):
| 选项 | 说明 |
|---|---|
ANALYZE |
真实执行查询,返回实际运行时间、行数等统计信息(推荐用于调优) |
BUFFERS |
显示 I/O 缓冲区使用情况(读取/写入的块数) |
TIMING |
显示每个节点的启动和运行时间(默认开启,ANALYZE 时自动包含) |
VERBOSE |
显示更多细节(如列名、表达式、排序键等) |
COSTS |
显示估算成本(默认开启) |
FORMAT |
输出格式:text(默认)、xml、json、yaml(推荐 json 用于程序解析) |
示例:
<SQL>
sql
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)
SELECT * FROM users WHERE age > 30 AND city = 'Beijing';
二、执行计划结构详解
一个典型的执行计划是树形结构,从下往上执行(叶子节点先执行,根节点最后)。
示例计划(text 格式):
<TEXT>
sql
Seq Scan on users (cost=0.00..45.50 rows=1000 width=64)
(actual time=0.020..1.230 rows=1050 loops=1)
Filter: ((age > 30) AND (city = 'Beijing'::text))
Rows Removed by Filter: 950
Buffers: shared read=120 Planning
Time: 0.123 ms Execution Time: 1.345 ms
1. 节点类型(Node Type)
每个节点代表一个操作,常见类型:
| 节点类型 | 说明 |
|---|---|
Seq Scan |
顺序扫描(全表扫描),效率低,应尽量避免 |
Index Scan |
使用索引扫描,根据索引定位行,比 Seq Scan 快 |
Index Only Scan |
仅使用索引,无需回表(需覆盖索引) |
Bitmap Heap Scan |
先用索引生成位图,再根据位图读取数据页(适合多条件) |
Bitmap Index Scan |
生成位图的索引扫描,常与 Bitmap Heap Scan 配对 |
Nested Loop |
嵌套循环连接(两表关联,外层每行都遍历内层) |
Hash Join |
哈希连接(常用,高效,需内存) |
Merge Join |
归并连接(需排序,适合大表有序连接) |
Sort |
排序操作 |
Limit |
限制返回行数(如 LIMIT 10) |
Aggregate |
聚合(如 COUNT, SUM) |
Subquery Scan |
子查询结果的扫描 |
Materialize |
将子查询结果物化到内存,避免重复计算 |
💡 重要提示 :
Index Only Scan是最理想的扫描方式,因为完全避免了访问表数据页,只读索引。
2. 成本估算(Cost)
格式:cost=启动成本..总成本
- 启动成本(Startup Cost):从开始执行到返回第一行所需成本(单位:任意,相对值)
- 总成本(Total Cost):执行完整个操作所需总成本
- 单位说明:PostgreSQL 内部成本单位,1 ≈ 一次顺序磁盘页读取(约 1.0),随机读取约 4.0,CPU 操作约 0.01
✅ 成本是估算值 ,由统计信息(
ANALYZE收集)计算得出,不等于真实时间。
3. 实际执行信息(ANALYZE 时显示)
actual time=0.020..1.230:实际启动时间(毫秒)到总执行时间rows=1050:实际返回行数loops=1:该节点被调用次数(嵌套循环中可能 >1)
⚠️ 如果
rows与estimated rows差距很大(如 1000 vs 10),说明统计信息过期,需执行:
<SQL>
sql
ANALYZE users;
4. Filter 条件
<TEXT>
sql
Filter: ((age > 30) AND (city = 'Beijing'::text)) Rows Removed by Filter: 950
- 表示该节点过滤掉的行数
- 如果
Rows Removed by Filter很大,说明过滤效率低,考虑:- 增加组合索引:
(age, city) - 优化查询条件顺序(PostgreSQL 会自动优化)
- 增加组合索引:
5. Buffers(重要!)
<TEXT>
sql
Buffers: shared read=120
shared read:从共享缓冲区读取的块数(1 块 = 8KB)shared written:写入的块数temp read/write:临时文件读写(说明内存不足,发生磁盘排序/哈希)
🔥 性能黄金法则 :
尽量减少shared read和temp read/write→ 优化索引、减少返回字段、避免全表扫描
6. Planning Time & Execution Time
Planning Time:查询计划生成耗时(通常 <1ms)Execution Time:实际执行耗时(你关心的核心指标)
三、常见性能问题与优化建议
| 问题 | 现象 | 优化方案 |
|---|---|---|
| 全表扫描(Seq Scan) | 表大、无索引、索引未被使用 | 为 WHERE 条件字段建索引,避免函数包裹(如 WHERE upper(name) = 'JOHN') |
| 索引未使用 | 有索引但走 Seq Scan | 检查统计信息是否更新(ANALYZE),索引列顺序是否匹配查询,是否使用了函数/类型转换 |
| 大量 Rows Removed by Filter | 过滤效率低 | 创建组合索引覆盖 WHERE 条件,或使用部分索引(Partial Index) |
| Hash Join / Merge Join 内存不足 | 出现 temp read/write |
增加 work_mem,或优化查询减少中间结果集 |
| 嵌套循环(Nested Loop)慢 | 外层表大、内层无索引 | 确保内层表有索引,或改用 Hash Join(增加 hash_mem) |
| 排序慢(Sort) | Sort 节点耗时高,有 temp |
增加 work_mem,或使用索引避免排序(如 ORDER BY idx_col) |
| 子查询效率低 | Subquery Scan 或重复执行 |
改为 JOIN,或使用 CTE(WITH)物化 |
四、实战优化案例
❌ 低效查询:
<SQL>
sql
SELECT * FROM orders WHERE extract(year from order_date) = 2023;
- 问题 :
extract()函数阻止索引使用 - 执行计划 :
Seq Scan
✅ 优化方案:
<SQL>
sql
SELECT *
FROM orders
WHERE order_date >= '2023-01-01'
AND order_date < '2024-01-01';
- 加索引:
<SQL>
sql
CREATE INDEX idx_orders_date ON orders(order_date);
- 执行计划 :
Index Scan,性能提升 10~100 倍
五、高级技巧
1. 查看索引是否覆盖查询(Index Only Scan)
<SQL>
sql
CREATE INDEX idx_users_age_city ON users(age, city, id);
-- 包含所有查询字段
SELECT id, age, city FROM users WHERE age > 30 AND city = 'Beijing';
→ 如果所有查询字段都在索引中,且无 NULL,即可触发 Index Only Scan
2. 使用 EXPLAIN (ANALYZE, BUFFERS) 比较优化前后效果
<SQL>
sql
-- 优化前 EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
-- 优化后 EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
对比 actual time、shared read、temp 等指标
3. 使用 pg_stat_statements 监控慢查询
<SQL>
sql
-- 安装扩展 CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 查看最慢的 SQL
SELECT query, total_time, calls, rows, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC LIMIT 10;
六、推荐工具
| 工具 | 用途 |
|---|---|
| https://explain.depesz.com | 在线解析 EXPLAIN 输出,高亮问题,可视化树形结构 |
pgAdmin |
图形化查看执行计划 |
psql + \x auto |
横向显示更清晰 |
✅ 推荐:把 EXPLAIN 输出粘贴到 https://explain.depesz.com,它会自动分析并标注性能风险!
七、总结:执行计划阅读口诀
🔍 "看节点、比成本、查实际、盯缓冲、找过滤、问索引"
- 看节点:识别操作类型(Seq Scan?Index Scan?Hash Join?)
- 比成本:估算成本是否合理(行数估算偏差大?)
- 查实际 :
actual time和rows是否接近估算? - 盯缓冲 :
shared read是否过高?有没有temp? - 找过滤 :
Rows Removed by Filter是否过多? - 问索引:为什么没用索引?是否可建组合索引?
✅ 最佳实践清单
- 总是用
EXPLAIN (ANALYZE, BUFFERS)而不是仅EXPLAIN - 定期
ANALYZE表(尤其数据变动频繁时) - 避免在 WHERE 条件中使用函数,除非是表达式索引
- 优先使用覆盖索引(Index Only Scan)
- 组合索引顺序 :高选择性字段放前面(如
city比status更好) - 监控
work_mem和shared_buffers,避免磁盘排序 - 使用 explain.depesz.com 分析复杂计划
掌握 EXPLAIN 是成为 PostgreSQL 性能专家的第一步。多练、多对比、多分析,你的查询会越来越快!
📚 PostgreSQL EXPLAIN 执行计划节点类型全集(完整版)
✅ 说明:
- 所有节点按执行顺序 从叶子节点向上组织(执行从下往上)
- 节点名称为 PostgreSQL 内部标识符,直接显示在
EXPLAIN输出中- 本列表基于 PostgreSQL 15/16,兼容 9.6+
- 包含 40+ 种节点类型,含新特性(如 Incremental Sort、Parallel Append 等)
🔹 一、扫描节点(Scan Nodes)------ 数据从哪里来?
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Seq Scan |
顺序扫描(全表扫描) | 无索引、索引选择性低、SELECT *、统计信息过期 |
⚠️ 最慢,高 I/O | 为 WHERE/JOIN 字段建索引;避免 SELECT *;更新统计信息 ANALYZE |
Index Scan |
使用索引扫描行 | 有索引,且条件匹配索引前导列 | ✅ 快,但需回表 | 确保索引覆盖查询字段(避免回表) |
Index Only Scan |
仅索引扫描(无需回表) | 查询字段全在索引中(覆盖索引),且无 NULL/更新事务可见性问题 | ✅✅ 最优 | 创建覆盖索引;避免频繁更新表 |
Bitmap Heap Scan |
位图堆扫描 | 多条件查询(AND/OR),先用索引生成位图,再读取数据页 | ✅ 比 Seq Scan 快,适合中等结果集 | 优化组合索引;避免宽字段 |
Bitmap Index Scan |
位图索引扫描 | 与 Bitmap Heap Scan 配对使用,生成位图 |
✅ 高效多条件索引 | 建立组合索引支持多列过滤 |
Tid Scan |
通过 TID(元组标识符)直接定位行 | 使用 ctid 或子查询返回的 TID |
✅ 极快(直接定位) | 仅用于内部优化或特殊场景,一般无需干预 |
Sample Scan |
随机采样扫描 | 使用 TABLESAMPLE SYSTEM(10) |
✅ 极快,用于估算 | 仅用于统计估算,非精确查询 |
Foreign Scan |
外部表扫描 | 使用 postgres_fdw、mysql_fdw 等外部数据包装器 |
⚠️ 依赖外部系统性能 | 优化远程查询、减少传输字段、使用 WHERE 下推 |
Custom Scan |
自定义扫描 | 插件(如 Citus、TimescaleDB、pg_stat_statements)自定义实现 | 取决于插件 | 查阅对应插件文档 |
💡 提示 :
Index Only Scan是最理想的扫描方式 ------ 完全不访问表数据页,只读索引。
🔹 二、连接节点(Join Nodes)------ 表之间如何关联?
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Nested Loop |
嵌套循环连接 | 小表驱动大表,内层有索引 | ✅ 小数据量时极快;大数据量时极慢 | 确保内层表有索引;避免大表嵌套 |
Hash Join |
哈希连接 | 两表中等以上规模,无排序,内存足够 | ✅✅ 最常用、高效 | 增加 work_mem;避免哈希表溢出到磁盘 |
Merge Join |
归并连接 | 两表已排序,且连接键有序 | ✅ 高效,适合大表有序连接 | 确保连接字段有索引;避免类型转换 |
Materialize |
物化子查询结果 | 用于 Nested Loop 的内层,避免重复计算 |
✅ 提升性能 | 若内层结果小,可提前物化为 CTE |
Gather |
收集并行结果(非排序) | 并行查询的最终收集节点 | ✅ 正常 | 检查 max_parallel_workers_per_gather |
Gather Merge |
收集并排序的并行结果 | 并行 ORDER BY 或 Merge Join 的输入 |
✅ 高效 | 确保并行 Worker 输出已排序 |
⚠️ 重要 :
Merge Join要求输入数据已排序 ,否则 PostgreSQL 会自动加Sort节点,代价飙升。
Hash Join要求内存足够 ,否则会temp read/write,性能骤降。
🔹 三、排序与去重节点(Sort & Deduplication)
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Sort |
排序 | ORDER BY、GROUP BY、DISTINCT、Merge Join 输入 |
⚠️ 高 CPU/I/O | 增加 work_mem;避免排序宽字段;用索引避免排序 |
Incremental Sort |
增量排序(PG 13+) | 输入数据部分有序(如索引前导列) | ✅ 比全排序快 | 创建索引覆盖排序字段前缀 |
Unique |
去重(流式) | DISTINCT、UNION、GROUP BY 无聚合 |
⚠️⚠️⚠️ 极高成本(尤其大数据+宽字段) | 避免 SELECT DISTINCT *;建索引;减少字段宽度 |
Group |
分组(排序后) | GROUP BY 且未使用哈希聚合 |
⚠️ 慢于 HashAggregate |
改用 HashAggregate(确保内存足够) |
HashAggregate |
哈希聚合 | GROUP BY、COUNT()、SUM() 等 |
✅✅ 最快聚合方式 | 增加 work_mem;避免聚合大字段(JSON/TEXT) |
SetOp |
集合操作 | UNION、INTERSECT、EXCEPT |
⚠️ 成本高(隐含去重) | 改用 UNION ALL(如允许重复);避免 DISTINCT |
💡
HashAggregate比Group快 3~10 倍,优先使用。
UNION=UNION ALL+Unique→ 除非必须去重,否则用UNION ALL
🔹 四、聚合与窗口节点(Aggregation & Windowing)
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Aggregate |
聚合函数(如 SUM, AVG) |
无 GROUP BY 的全局聚合 |
✅ 快 | 通常无需优化 |
WindowAgg |
窗口函数 | ROW_NUMBER(), RANK(), SUM() OVER() |
⚠️ 高内存 | 限制窗口范围(ROWS BETWEEN);避免大窗口 |
Result |
计算常量或表达式 | SELECT 1 + 2, CASE WHEN |
✅ 无成本 | 无需优化 |
⚠️ 窗口函数可能触发全表排序 + 缓存,性能敏感场景慎用。
🔹 五、子查询与物化节点(Subquery & Materialization)
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Subquery Scan |
包裹子查询结果 | 派生表(FROM (SELECT ...) AS t) |
✅ 正常 | 尽量内联子查询,避免嵌套 |
Materialize |
物化子查询结果 | 子查询被多次引用 | ✅ 提升性能 | 若结果小,可提前写入临时表 |
Recursive Union |
递归查询 | WITH RECURSIVE |
⚠️ 指数级增长 | 限制递归深度;加 LIMIT;优化终止条件 |
Append |
多表/分区合并 | UNION ALL、分区表 |
✅ 高效 | 分区剪枝(Partition Pruning)生效时极快 |
Parallel Append |
并行合并多个扫描 | 并行查询 + 多分区/多表 | ✅ 高效 | 启用并行查询(max_parallel_workers_per_gather) |
✅
Append和Parallel Append是分区表性能的关键。
🔹 六、限制与投影节点(Limit & Projection)
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
Limit |
限制返回行数 | LIMIT n、OFFSET |
✅ 快 | 避免大 OFFSET(用游标或键值分页) |
Result |
计算表达式、常量、函数 | SELECT col + 1, UPPER(name) |
✅ 低开销 | 避免在 WHERE 中使用函数(阻止索引) |
Function Scan |
函数返回表 | SELECT * FROM generate_series(1,100) |
✅ 快 | 通常无需优化 |
Values Scan |
VALUES 列表 | VALUES (1,'a'), (2,'b') |
✅ 极快 | 用于测试或小数据集 |
⚠️
WHERE UPPER(name) = 'JOHN'会阻止索引使用 → 改为WHERE name ILIKE 'john%'或建函数索引:
<SQL>
sql
CREATE INDEX idx_name_upper ON users ((upper(name)));
🔹 七、特殊节点(Advanced / Internal)
| 节点类型 | 含义 | 触发场景 | 性能影响 | 优化建议 |
|---|---|---|---|---|
LockRows |
行级锁 | SELECT ... FOR UPDATE |
✅ 正常 | 避免长时间事务 |
ModifyTable |
修改数据 | INSERT、UPDATE、DELETE |
⚠️ 高 I/O | 分批提交;避免触发器过多 |
Cte Scan |
CTE(公共表表达式)扫描 | WITH cte AS (...) SELECT ... FROM cte |
✅ 正常 | 若 CTE 被多次使用,MATERIALIZED 更优 |
Foreign Scan |
外部表 | postgres_fdw, mysql_fdw |
⚠️ 网络延迟 | 下推 WHERE、JOIN;减少字段 |
Tid Scan |
通过 TID 直接定位 | 内部使用(如子查询返回 ctid) | ✅ 极快 | 无需干预 |
Custom Scan |
插件自定义 | Citus、Timescale、pg_stat_statements | 取决于插件 | 查阅插件文档 |
🔹 八、并行执行相关节点(Parallel Execution)
| 节点类型 | 含义 | 触发场景 | 说明 |
|---|---|---|---|
Gather |
收集多个 Worker 的结果 | 并行扫描、并行聚合 | 不要求排序 |
Gather Merge |
收集并合并排序结果 | 并行 ORDER BY、Merge Join |
要求 Worker 输出有序 |
Parallel Seq Scan |
并行顺序扫描 | 大表 + max_parallel_workers_per_gather > 0 |
多进程同时扫描 |
Parallel Index Scan |
并行索引扫描 | 索引扫描 + 并行启用 | 比单进程快 |
Parallel Bitmap Heap Scan |
并行位图堆扫描 | 多条件 + 并行 | 高效大数据量查询 |
Parallel Append |
并行合并多个子计划 | 分区表 + 并行 | 每个分区由不同 Worker 扫描 |
✅ 开启并行查询条件:
- 表大小 >
min_parallel_table_scan_size(默认 8MB)max_parallel_workers_per_gather> 0(默认 2)- 查询不包含不可并行操作(如
ORDER BY非索引字段、DISTINCT、LIMIT在子查询等)
📌 总结:执行计划节点类型分类速查表
| 类别 | 节点类型 | 是否常见 | 性能建议 |
|---|---|---|---|
| ✅ 扫描 | Seq Scan, Index Scan, Index Only Scan, Bitmap Heap Scan |
⭐⭐⭐⭐⭐ | 优先用索引,避免全表扫描 |
| ✅ 连接 | Hash Join, Merge Join, Nested Loop |
⭐⭐⭐⭐⭐ | Hash Join 最常用;Merge Join 要求排序 |
| ✅ 排序/去重 | Sort, Unique, HashAggregate |
⭐⭐⭐⭐⭐ | 用 HashAggregate 替代 Group;避免 Unique 与宽字段 |
| ✅ 聚合 | HashAggregate, WindowAgg |
⭐⭐⭐⭐ | 窗口函数慎用;增加 work_mem |
| ✅ 子查询 | Subquery Scan, Materialize, CTE Scan |
⭐⭐⭐ | 尽量内联;避免重复计算 |
| ✅ 限制 | Limit, Result |
⭐⭐⭐⭐ | 避免大 OFFSET;避免函数包裹索引字段 |
| ✅ 并行 | Gather Merge, Parallel Seq Scan |
⭐⭐⭐⭐ | 启用并行,但确保 work_mem 足够 |
| ⚠️ 特殊 | ModifyTable, LockRows, Foreign Scan |
⭐⭐ | 优化写入、锁、外部连接 |
🧠 性能调优黄金法则(结合节点)
| 问题 | 节点表现 | 解决方案 |
|---|---|---|
| 慢 | Seq Scan |
建索引 |
| 慢 | Unique + 高成本 |
减少字段、建索引、避免 DISTINCT * |
| 慢 | Sort + temp read |
增加 work_mem,或用索引避免排序 |
| 慢 | Hash Join + temp |
增加 work_mem,减少中间结果 |
| 慢 | Nested Loop + 大内层 |
确保内层有索引,或改用 Hash Join |
| 慢 | Merge Join 无排序 |
检查连接字段是否有序,建索引 |
| 慢 | Subquery Scan + 高成本 |
改为 JOIN,或物化 CTE |
| 慢 | WindowAgg |
限制窗口范围,避免全表排序 |
🔗 推荐工具
| 工具 | 用途 |
|---|---|
| https://explain.depesz.com | 在线可视化分析,自动标注性能风险(类型转换、排序、临时文件) |
pgAdmin |
图形化执行计划查看器 |
psql + \x auto |
横向显示更清晰 |
pg_stat_statements |
监控最慢 SQL |
✅ 最后建议:阅读执行计划的口诀
🔍 "看节点、比成本、查实际、盯缓冲、找过滤、问索引"
- 看节点:识别操作类型(是 Seq Scan?Hash Join?Unique?)
- 比成本:估算成本是否合理?行数估算偏差大?
- 查实际 :
actual time和rows是否接近估算? - 盯缓冲 :
shared read高?temp read/write?→ 内存不足! - 找过滤 :
Rows Removed by Filter是否过多?→ 索引缺失! - 问索引:为什么没用索引?字段类型一致吗?有函数吗?
📌 掌握这些节点,你就掌握了 PostgreSQL 性能调优的全部钥匙!