Postgresql explain执行计划详解

📚 Postgresql explain执行计划详解

PostgreSQL 的 EXPLAIN 命令是性能调优的核心工具,用于查看 SQL 查询的执行计划(Execution Plan)。理解执行计划能帮助你识别性能瓶颈、优化索引、调整查询结构。下面是对 PostgreSQL EXPLAIN 执行计划的详细详解


一、基本语法

<SQL>

sql 复制代码
EXPLAIN [ ( option [, ...] ) ] statement

常用选项(可组合使用):

选项 说明
ANALYZE 真实执行查询,返回实际运行时间、行数等统计信息(推荐用于调优)
BUFFERS 显示 I/O 缓冲区使用情况(读取/写入的块数)
TIMING 显示每个节点的启动和运行时间(默认开启,ANALYZE 时自动包含)
VERBOSE 显示更多细节(如列名、表达式、排序键等)
COSTS 显示估算成本(默认开启)
FORMAT 输出格式:text(默认)、xmljsonyaml(推荐 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)

⚠️ 如果 rowsestimated 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 readtemp 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 timeshared readtemp 等指标

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 timerows 是否接近估算?
  • 盯缓冲shared read 是否过高?有没有 temp
  • 找过滤Rows Removed by Filter 是否过多?
  • 问索引:为什么没用索引?是否可建组合索引?

✅ 最佳实践清单

  1. 总是用 EXPLAIN (ANALYZE, BUFFERS) 而不是仅 EXPLAIN
  2. 定期 ANALYZE(尤其数据变动频繁时)
  3. 避免在 WHERE 条件中使用函数,除非是表达式索引
  4. 优先使用覆盖索引(Index Only Scan)
  5. 组合索引顺序 :高选择性字段放前面(如 citystatus 更好)
  6. 监控 work_memshared_buffers,避免磁盘排序
  7. 使用 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_fdwmysql_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 BYMerge Join 的输入 ✅ 高效 确保并行 Worker 输出已排序

⚠️ 重要
Merge Join 要求输入数据已排序 ,否则 PostgreSQL 会自动加 Sort 节点,代价飙升。
Hash Join 要求内存足够 ,否则会 temp read/write,性能骤降。


🔹 三、排序与去重节点(Sort & Deduplication)

节点类型 含义 触发场景 性能影响 优化建议
Sort 排序 ORDER BYGROUP BYDISTINCTMerge Join 输入 ⚠️ 高 CPU/I/O 增加 work_mem;避免排序宽字段;用索引避免排序
Incremental Sort 增量排序(PG 13+) 输入数据部分有序(如索引前导列) ✅ 比全排序快 创建索引覆盖排序字段前缀
Unique 去重(流式) DISTINCTUNIONGROUP BY 无聚合 ⚠️⚠️⚠️ 极高成本(尤其大数据+宽字段) 避免 SELECT DISTINCT *;建索引;减少字段宽度
Group 分组(排序后) GROUP BY 且未使用哈希聚合 ⚠️ 慢于 HashAggregate 改用 HashAggregate(确保内存足够)
HashAggregate 哈希聚合 GROUP BYCOUNT()SUM() ✅✅ 最快聚合方式 增加 work_mem;避免聚合大字段(JSON/TEXT)
SetOp 集合操作 UNIONINTERSECTEXCEPT ⚠️ 成本高(隐含去重) 改用 UNION ALL(如允许重复);避免 DISTINCT

💡 HashAggregateGroup 快 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

AppendParallel Append 是分区表性能的关键。


🔹 六、限制与投影节点(Limit & Projection)

节点类型 含义 触发场景 性能影响 优化建议
Limit 限制返回行数 LIMIT nOFFSET ✅ 快 避免大 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 修改数据 INSERTUPDATEDELETE ⚠️ 高 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 BYMerge 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 非索引字段、DISTINCTLIMIT 在子查询等)

📌 总结:执行计划节点类型分类速查表

类别 节点类型 是否常见 性能建议
扫描 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 timerows 是否接近估算?
  • 盯缓冲shared read 高?temp read/write?→ 内存不足!
  • 找过滤Rows Removed by Filter 是否过多?→ 索引缺失!
  • 问索引:为什么没用索引?字段类型一致吗?有函数吗?

📌 掌握这些节点,你就掌握了 PostgreSQL 性能调优的全部钥匙!

相关推荐
九章-1 小时前
金仓数据库迁移工具链:Oracle平滑迁移的技术实践
数据库·oracle·数据库迁移工具·oracle迁移工具
空空潍1 小时前
PostgreSQL保姆级下载安装指南(win版)
数据库·postgresql
海山数据库1 小时前
移动云大云海山数据库(He3DB)与PolarDB架构深度对比(一)
数据库·架构·he3db·大云海山数据库·移动云数据库
dapeng28701 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
TG_yunshuguoji1 小时前
阿里云代理商:怎么创建和连接RDS云数据库?
数据库·阿里云·云计算
小陳参上2 小时前
持久化数据库实现:确保数据持久性与可靠性
java·jvm·数据库
不是株2 小时前
Redis(实战篇)
数据库·redis·缓存
Anastasiozzzz2 小时前
放弃原生 C 语言字符串:深度解析 Redis SDS 的设计艺术
数据库·redis·缓存
鸽芷咕2 小时前
海量时序数据选型指南:从大数据架构演进看 Apache IoTDB 的崛起
大数据·数据库·架构·apache