PostgreSQLNotes

第 07 章:执行计划

zjc 于 2026-01-07 发布

这是《PostgreSQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 执行计划是 PostgreSQL 性能优化的地图。优化 SQL 前,先能读懂每个节点的访问方式、行数估算、循环次数、排序和内存使用。

7.1 EXPLAIN

EXPLAIN
SELECT user_id, count(*), sum(amount)
FROM orders
WHERE status = 'PAID'
GROUP BY user_id;

EXPLAIN 只显示计划,不执行。查看真实执行指标使用:

EXPLAIN ANALYZE
SELECT ...

查看缓冲:

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...

ANALYZE 会真正执行 DML 或耗时 SELECT,生产慎用。

7.2 常见扫描节点

节点 说明
Seq Scan 全表顺序扫描
Index Scan 索引扫描并回表
Index Only Scan 只读索引,需可见性映射支持
Bitmap Index Scan 生成位图
Bitmap Heap Scan 按位图回表
Function Scan 函数结果扫描
Foreign Scan 外部表扫描

Seq Scan 不一定是坏事。小表、大比例返回、没有合适索引时,它可能成本最低。

7.3 连接节点

EXPLAIN ANALYZE
SELECT u.name, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id;
节点 适合
Nested Loop 外层小、内层能走索引
Hash Join 大表等值连接
Merge Join 输入已按连接键排序

如果 Nested Loop 内层是 Seq Scan 且 loops 很大,通常是缺索引或统计信息错误。

7.4 聚合与排序

常见节点:

节点 说明
Aggregate 普通聚合
GroupAggregate 按序分组
HashAggregate 哈希分组
Sort 显式排序
Incremental Sort 部分有序输入
Unique 去重

Sort 内存超出 work_mem 会溢写临时文件:

Sort Method: external merge Disk: 123456kB

7.5 行数估算

rows=1000
actual time=... rows=1000000 loops=1

估算与实际差异过大时,优化器可能选错计划。

处理:

ANALYZE orders;

SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables;

再检查是否有统计信息、WHERE 表达式是否可估算、是否跨列相关、是否需要扩展统计、是否参数被折叠成常量。

7.6 关键指标

cost=startup..total
rows=估算行数
width=平均行宽
actual time=启动..完成
rows=实际行数
loops=执行次数
Shared Hit / Read = 缓冲命中与磁盘读取

重点看每行耗时和外层循环放大,而不是只看第一行 cost。

7.7 执行计划开关

SET enable_seqscan = off;

这类开关用于验证索引是否可用,不应作为生产“优化配置”。更好的方式是调整统计信息、索引、SQL 或代价参数。

7.8 计划管理

PostgreSQL 支持扩展的计划管理方案,如 pg_hint_plan 等工具,具体可用性取决于版本和发行版。生产引入前要确认维护成本。

原生可先做:

  1. 固化 SQL;
  2. 更新统计;
  3. 建合适索引;
  4. 避免隐式转换;
  5. 减少返回数据;
  6. 记录基线计划。

7.9 慢计划阅读流程

1. 从最内层扫描看起
2. 检查过滤条件和索引匹配
3. 比较估算 rows 与 actual rows
4. 检查 loops
5. 检查 Sort / Hash 内存
6. 检查 Rows Removed
7. 优化后对比 buffers 和时间

本章小结

执行计划回答三个问题:扫了多少数据、用了什么算法、估算是否可靠。读懂 ANALYZEBUFFERS 输出,是 PostgreSQL 优化的基本功。

思考题

  1. Index Scan 和 Index Only Scan 差异是什么?
  2. loops 很大说明什么?
  3. Sort 溢写磁盘如何处理?
  4. 估算行数偏差过大会带来什么?
  5. 为什么 enable_seqscan = off 不适合生产?