PostgreSQL查询执行阶段 — 总结与执行计划选择指南

PostgreSQL查询执行阶段 --- 总结与执行计划选择指南

一、查询执行全流程

复制代码
SQL 文本
  │
  ▼
[解析] 词法/语法分析 → 抽象语法树 (AST)
  │
  ▼
[转换] 视图展开 / 行级安全 / 规则重写 → 查询树
  │
  ▼
[规划] 基于成本的优化器 → 计划树
  │
  ▼
[执行] 执行器流水线拉取数据 → 结果集

两种协议对比:

阶段 简单查询协议 扩展查询协议
解析 每次执行都解析 预备阶段解析一次,缓存语法树
规划 每次执行都规划 前 5 次 custom plan,之后可切换 generic plan
参数安全 需手动转义 参数绑定,天然防 SQL 注入
结果获取 一次性全量返回 支持批次获取(类似游标)

二、规划器核心机制

2.1 基于成本的优化

规划器枚举候选计划,为每个计划计算成本(cost),选择成本最低的计划执行。

成本由两部分组成:

复制代码
cost=<启动成本>..<总成本>
  • 启动成本:产出第一行之前必须付出的代价(如排序需先读完所有数据)
  • 总成本:获取全部结果的总代价

成本是无量纲的相对值,仅用于同一查询不同计划之间的比较,跨查询比较无意义。

2.2 计划选择策略

规划器根据查询使用方式选择优化目标:

场景 优化目标 选择依据
普通查询(无游标) 最小化总成本 选 total cost 最低的计划
游标查询 最小化前 N 行成本 startup + cursor_tuple_fraction × (total - startup) 最小的计划

cursor_tuple_fraction 默认值为 0.1,表示假设客户端只取前 10% 的行。

2.3 基数估算

基数(cardinality)= 节点输出的预估行数,是成本估算的基础。

估算流程(递归):

  1. 估算叶节点基数(依赖表统计信息:行数、列分布等)
  2. 估算过滤条件选择率(selectivity)
  3. 节点基数 = 输入行数 × 选择率

逻辑运算的选择率公式(假设谓词独立):

复制代码
sel(x AND y) = sel(x) × sel(y)
sel(x OR y)  = sel(x) + sel(y) - sel(x) × sel(y)

注意:谓词相关性会导致估算偏差,这是计划选择错误的主要根源之一。

2.4 连接顺序搜索

参数 作用 默认值
join_collapse_limit 超过此值时,显式 JOIN 顺序不再展平 8
from_collapse_limit 超过此值时,子查询不再展平到主查询 8
geqo 是否启用遗传算法优化 on
geqo_threshold 超过此表数量时启用遗传算法 12

连接数量与搜索策略:

复制代码
连接表数 ≤ geqo_threshold  →  动态规划(精确,但指数级复杂度)
连接表数 > geqo_threshold  →  遗传算法(快速,但不保证最优)

三、Custom Plan vs Generic Plan

切换逻辑

复制代码
执行次数 1~5  →  始终使用 custom plan(基于实际参数值规划)
执行次数 ≥ 6  →  比较 generic plan 与 custom plan 平均成本
               generic plan 更优  →  切换并固定使用 generic plan
               custom plan 更优   →  继续使用 custom plan

识别方式

sql 复制代码
-- custom plan:计划中显示具体参数值
Filter: ((aircraft_code)::text = '319'::text)

-- generic plan:计划中显示参数占位符
Filter: ((aircraft_code)::text = $1)

查看统计

sql 复制代码
SELECT name, generic_plans, custom_plans
FROM pg_prepared_statements;

手动控制

sql 复制代码
-- 强制使用 custom plan(参数选择性差异大时推荐)
SET plan_cache_mode = 'force_custom_plan';

-- 强制使用 generic plan(规划开销大、参数分布均匀时)
SET plan_cache_mode = 'force_generic_plan';

-- 恢复自动选择(默认)
SET plan_cache_mode = 'auto';

四、EXPLAIN 解读

基本用法

sql 复制代码
-- 查看估算计划
EXPLAIN SELECT ...;

-- 查看实际执行信息(真实行数、时间)
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

输出字段含义

复制代码
Sort (cost=21.03..21.04 rows=1 width=128)
       ↑启动成本  ↑总成本  ↑预估行数  ↑每行宽度(bytes)

EXPLAIN ANALYZE 额外输出:

复制代码
Sort (cost=21.03..21.04 rows=1 width=128)
     (actual time=0.123..0.124 rows=3 loops=1)
                  ↑实际启动时间  ↑实际总时间  ↑实际行数  ↑循环次数

常见节点类型

节点 说明 适用场景
Seq Scan 全表顺序扫描 大比例数据读取、无合适索引
Index Scan 索引扫描 + 回表 高选择性条件,少量行
Index Only Scan 仅索引扫描(覆盖索引) 查询列全在索引中
Bitmap Index Scan + Bitmap Heap Scan 位图索引扫描 中等选择性,批量回表
Nested Loop 嵌套循环连接 外表小、内表有索引
Hash Join 哈希连接 大表等值连接
Merge Join 归并连接 两侧已排序或可利用索引顺序
Sort 排序 ORDER BY、Merge Join 前置
Hash 构建哈希表 Hash Join 内侧
Aggregate 聚合 GROUP BY、COUNT 等
Limit 限制行数 LIMIT 子句

五、执行计划选择问题排查

5.1 统计信息过时

现象rows=1 但实际返回数千行,或反之。

原因:统计信息未及时更新。

解决

sql 复制代码
-- 手动更新统计信息
ANALYZE table_name;

-- 查看统计信息最后更新时间
SELECT relname, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'table_name';

5.2 相关谓词导致估算偏差

现象:多列组合过滤时行数估算严重偏低。

原因:规划器默认谓词独立,相关列会导致乘积低估。

解决:创建多列统计信息(PG 10+):

sql 复制代码
CREATE STATISTICS stat_name (dependencies) ON col1, col2 FROM table_name;
ANALYZE table_name;

5.3 选错扫描方式

现象:应走索引却走全表扫描,或反之。

排查

sql 复制代码
-- 临时禁用某种扫描方式验证
SET enable_seqscan = off;
SET enable_indexscan = off;
SET enable_bitmapscan = off;
EXPLAIN SELECT ...;

根本原因:通常是统计信息不准或成本参数不匹配实际硬件。

5.4 连接顺序不优

现象:大表驱动小表,或连接顺序导致中间结果集过大。

解决方案

sql 复制代码
-- 方案1:调整 join_collapse_limit 保留显式 JOIN 顺序
SET join_collapse_limit = 1;

-- 方案2:用 CTE 强制分段优化(PG 12 前默认 fence,PG 12+ 需加 MATERIALIZED)
WITH sub AS MATERIALIZED (
  SELECT ... FROM large_table WHERE ...
)
SELECT ... FROM sub JOIN small_table ON ...;

5.5 遗传算法导致计划不稳定

现象:多表连接查询计划随机变化,性能不稳定。

解决

sql 复制代码
-- 降低 geqo_threshold 或关闭 geqo
SET geqo = off;
-- 或减少参与连接的表数量(拆分查询、使用 CTE)

六、执行阶段内存管理

参数 说明 默认值
work_mem 单个排序/哈希操作可用内存 4MB
temp_file_limit 临时文件总大小上限 -1(无限)
  • 一个查询可能有多个需要内存的节点(Sort、Hash 等),每个节点独立分配最多 work_mem
  • 超出 work_mem 时数据溢出到磁盘临时文件,性能大幅下降。
  • EXPLAIN (ANALYZE, BUFFERS) 中出现 Batches: N(N>1)说明 Hash Join 发生了溢出。
sql 复制代码
-- 针对当前会话调大(大查询临时使用)
SET work_mem = '256MB';

七、关键参数速查

参数 默认值 说明
enable_seqscan on 是否允许全表扫描
enable_indexscan on 是否允许索引扫描
enable_bitmapscan on 是否允许位图扫描
enable_hashjoin on 是否允许哈希连接
enable_mergejoin on 是否允许归并连接
enable_nestloop on 是否允许嵌套循环
join_collapse_limit 8 JOIN 展平阈值
from_collapse_limit 8 子查询展平阈值
geqo on 是否启用遗传算法
geqo_threshold 12 启用遗传算法的表数阈值
cursor_tuple_fraction 0.1 游标预期获取比例
plan_cache_mode auto 预备语句计划缓存策略
work_mem 4MB 排序/哈希操作内存
default_statistics_target 100 统计信息采样精度

八、优化建议总结

  1. 保持统计信息新鲜 :确保 autovacuum 正常运行;对频繁变更的表手动 ANALYZE;对高选择性列提高 statistics target

    sql 复制代码
    ALTER TABLE t ALTER COLUMN c SET STATISTICS 500;
  2. 善用 EXPLAIN ANALYZE :对比 rows=估算rows=实际,偏差超过 10 倍时需关注统计信息或查询结构。

  3. 避免阻止规划器使用索引

    • 不要对索引列做函数运算(WHERE date(created_at) = ... → 改为范围条件)
    • 不要隐式类型转换(WHERE int_col = '123'
  4. 合理使用预备语句 :参数选择性差异大(如有时走索引有时走全表)时,设置 plan_cache_mode = force_custom_plan

  5. 大表连接控制顺序 :超过 8 张表的复杂查询,考虑用 CTE 分段或手动设置 join_collapse_limit = 1 固定连接顺序。

  6. work_mem 按需调整:排序/哈希密集型查询在会话级临时调大,避免全局调大导致内存压力。

  7. 分区表注意分区裁剪:确保查询条件包含分区键,否则规划器无法裁剪分区,退化为全分区扫描。

相关推荐
解救女汉子2 小时前
Bootstrap Gutters间距用法 Bootstrap 5中g-,gx-,gy--如何使用
jvm·数据库·python
2401_887724502 小时前
JavaScript中Object-hasOwn作为现代安全检测方案
jvm·数据库·python
qq_334563552 小时前
如何利用RETURNING获取ROWID_更新单行后快速定位物理地址
jvm·数据库·python
zhangchaoxies2 小时前
HTML怎么显示同步最后成功时间_HTML “上次同步:X分钟前”【教程】
jvm·数据库·python
m0_514520572 小时前
mysql服务器如何优化网络传输设置_调整tcp相关内核参数
jvm·数据库·python
m0_640309302 小时前
如何快速重置SQL表中的自增ID_使用ALTER TABLE重置计数
jvm·数据库·python
2301_764150562 小时前
CSS如何制作响应式导航栏_利用Flexbox实现自适应水平排列
jvm·数据库·python
qq_334563552 小时前
HTML怎么创建表格_HTML表格结构与基本语法【教程】
jvm·数据库·python
yejqvow122 小时前
C#怎么实现缓存功能 C#如何用MemoryCache和Redis实现数据缓存提升访问速度【架构】
jvm·数据库·python