# PostgreSQL 18.3 查询优化器流程:`standard_planner()` 源码导读

PostgreSQL 18.3 查询优化器流程:standard_planner() 源码导读

1. 文档目标

本文按照 PostgreSQL 18.3 中 standard_planner() 的执行顺序,讲解默认查询优化器从 QueryPlannedStmt 的完整流程:

版本说明: 本文以 PostgreSQL 官方源码标签
REL_18_3 为准。
该版本的 planner()standard_planner() 都是四参数形式。开发分支中的
函数签名、Hook 或辅助字段可能已经变化,阅读时不要把不同版本源码混在一起。

1. 初始化 PlannerGlobal
        ↓
2. 判断是否允许并行规划
        ↓
3. 计算 tuple_fraction
        ↓
4. 调用 subquery_planner()
        ↓
5. 获得最终 RelOptInfo
        ↓
6. 选择最优 Path
        ↓
7. Path 转换为 Plan
        ↓
8. 处理游标和调试 Gather
        ↓
9. 完成 Param 和子计划处理
        ↓
10. 修正计划中的引用
        ↓
11. 构造 PlannedStmt
        ↓
12. 设置 JIT 标志
        ↓
13. 清理并返回

需要先明确:standard_planner() 是默认优化器的总调度函数,但它并不亲自实现所有算法。关系代数层面的逻辑变换、基表访问路径生成、连接顺序枚举、成本计算和上层操作规划,会继续委托给其他函数。

本文重点回答以下问题:

  • standard_planner() 的输入为什么已经是 Query
  • PlannerGlobalPlannerInfo 有什么区别?
  • 关系代数等价变换发生在哪里?
  • subquery_planner() 做什么,返回什么?
  • subquery_planner() 返回时是否已经产生最优 Path?
  • RelOptInfo 是什么?
  • 为什么 UPPERREL_FINAL 是完整查询候选实现的最终容器?
  • 最优 Path 由哪个函数选择?
  • Path 转换成 Plan 到底转换了什么?
  • Partial PathGatherGather Merge 在流程中的位置是什么?
  • Param、SubPlan、extParamallParam 为什么需要最后处理?
  • set_plan_references() 为什么在 create_plan() 之后?
  • PlannedStmt 为什么不能只用一个 Plan * 代替?
  • JIT 为什么在计划选定后才决定?

2. standard_planner() 位于整个 SQL 流程的什么位置

standard_planner() 并不是 SQL 进入 PostgreSQL 后调用的第一个函数。在进入优化器之前,SQL 已经完成词法分析、语法分析、语义分析和查询重写。

简单查询协议的主调用链可以概括为:

exec_simple_query()
    ↓
pg_parse_query()
    ↓
raw_parser()
    ↓
RawStmt / SelectStmt
    ↓
pg_analyze_and_rewrite_fixedparams()
    ├── parse_analyze_fixedparams()
    └── pg_rewrite_query()
    ↓
一个或多个 Query
    ↓
pg_plan_queries()
    ↓
pg_plan_query()
    ↓
planner()
    ↓
standard_planner()

主要源码位置:

src/backend/tcop/postgres.c
    exec_simple_query()
    pg_parse_query()
    pg_analyze_and_rewrite_fixedparams()
    pg_plan_queries()

src/backend/optimizer/plan/planner.c
    planner()
    standard_planner()
    subquery_planner()
    grouping_planner()

因此,standard_planner() 的参数:

Query *parse

不是原始语法树,而是经过语义分析和规则重写的查询树。

可以把主要数据转换记成:

SQL 字符串
    ↓
RawStmt / SelectStmt       原始语法结构
    ↓
Query                      已解析表、列、函数和类型的语义查询树
    ↓
PlannerInfo / RelOptInfo   优化状态与逻辑关系
    ↓
Path                       候选物理实现
    ↓
Plan                       最终静态执行计划
    ↓
PlannedStmt                完整的语句级规划结果
    ↓
PlanState                  执行器运行时状态

3. 默认优化器入口

优化器对外入口是:

PlannedStmt *
planner(Query *parse,
        const char *query_string,
        int cursorOptions,
        ParamListInfo boundParams)

planner() 首先检查扩展是否注册了 planner_hook

if (planner_hook)
    result = planner_hook(parse,
                          query_string,
                          cursorOptions,
                          boundParams);
else
    result = standard_planner(parse,
                              query_string,
                              cursorOptions,
                              boundParams);

所以需要区分:

planner()
    优化器公开入口和扩展钩子入口

standard_planner()
    PostgreSQL 内置默认优化器的总入口

本文讨论的是 standard_planner()


4. 贯穿全文的示例 SQL

使用下面的查询贯穿整个优化过程:

SELECT c.region,
       SUM(o.amount) AS total
FROM customer AS c
JOIN orders AS o
  ON o.customer_id = c.id
WHERE c.active = true
  AND o.order_date >= DATE '2026-01-01'
GROUP BY c.region
HAVING SUM(o.amount) > 10000
ORDER BY total DESC
LIMIT 10;

假设存在:

customer
    主键索引 customer_pkey(id)
    索引 customer_active_idx(active)

orders
    索引 orders_customer_id_idx(customer_id)
    索引 orders_order_date_idx(order_date)

该查询包含:

两个基本表
两个过滤条件
一个等值连接
聚合
HAVING
排序
LIMIT

因此可以观察优化器的主要阶段。

它进入 standard_planner() 时,Query 大致表示:

Query
├── commandType = CMD_SELECT
├── hasAggs = true
├── rtable
│   ├── customer
│   ├── orders
│   └── customer JOIN orders
├── jointree
│   ├── JoinExpr
│   │   └── o.customer_id = c.id
│   └── quals
│       ├── c.active = true
│       └── o.order_date >= DATE '2026-01-01'
├── targetList
│   ├── c.region
│   └── SUM(o.amount)
├── groupClause
│   └── c.region
├── havingQual
│   └── SUM(o.amount) > 10000
├── sortClause
│   └── total DESC
└── limitCount
    └── 10

5. 阅读前必须掌握的六个数据结构

5.1 Query

Query 描述查询的语义:

访问哪些关系
输出哪些表达式
FROM 和 JOIN 是什么
WHERE、HAVING 条件是什么
是否有聚合、窗口函数或子查询
是否有排序、DISTINCT 和 LIMIT

它回答的是:

查询想得到什么结果?

它不回答:

应该使用 Seq Scan 还是 Index Scan?应该使用 Hash Join 还是 Nested Loop?


5.2 PlannerGlobal

PlannerGlobal 表示一次完整 planner() 调用共享的全局状态。

一条 SQL 可能包含多个 Query 层级:

顶层 Query
├── CTE Query
├── FROM 子查询
└── SubLink Query

每个 Query 层级有自己的 PlannerInfo,但它们共享同一个 PlannerGlobal


5.3 PlannerInfo

PlannerInfo 表示一个 Query 层级的规划上下文:

当前 Query
父 Query 的 PlannerInfo
当前查询层级
基本表 RelOptInfo 数组
JOIN RelOptInfo
EquivalenceClass
RestrictInfo
Upper RelOptInfo
候选 Path

关系是:

PlannerGlobal
├── PlannerInfo:顶层 Query,query_level = 1
├── PlannerInfo:一级子查询,query_level = 2
└── PlannerInfo:二级子查询,query_level = 3

5.4 RelOptInfo

RelOptInfo 表示一个逻辑关系或逻辑结果。

它可能代表:

一张基本表
多张表连接后的结果
聚合后的结果
窗口计算后的结果
排序后的结果
完整查询的最终结果

同一个 RelOptInfo 中保存能够产生相同逻辑结果的不同 Path。


5.5 Path

Path 表示一种候选物理实现:

SeqScan Path
IndexPath
BitmapHeapPath
NestPath
HashPath
MergePath
AggPath
SortPath
GatherPath
LimitPath

这里需要区分“图中的候选路径名称”和“源码中的独立 C 结构体”。例如,
顺序扫描候选通常是一个 pathtype = T_SeqScan 的通用 Path,并不存在
名为 SeqScanPath 的独立结构体;聚合通常使用 AggPath,再通过
aggstrategy 区分 AGG_HASHEDAGG_SORTED 等策略。本文的结构图会尽量
使用源码中的真实名称,并在必要时用空格分开的概念标签辅助阅读。

Path 主要服务于优化器,保存:

startup_cost
total_cost
rows
pathkeys
参数化信息
并行属性
子 Path

5.6 PlanPlannedStmt

Plan 是执行器能理解的静态计划节点:

SeqScan
IndexScan
NestLoop
HashJoin
MergeJoin
Agg
Sort
Gather
Limit

PlannedStmt 是语句级容器:

PlannedStmt
├── 主 Plan Tree
├── SubPlan
├── 最终 rtable
├── 权限信息
├── 行锁信息
├── 分区裁剪信息
├── 参数类型
├── 计划缓存依赖
├── 并行模式信息
└── JIT 标志

6. standard_planner() 的 13 步流程

第 1 步:初始化 PlannerGlobal

核心代码:

glob = makeNode(PlannerGlobal);

随后初始化大量全局字段:

glob->boundParams = boundParams;
glob->subplans = NIL;
glob->subpaths = NIL;
glob->subroots = NIL;
glob->rewindPlanIDs = NULL;
glob->finalrtable = NIL;
glob->partPruneInfos = NIL;
glob->relationOids = NIL;
glob->invalItems = NIL;
glob->paramExecTypes = NIL;
glob->parallelModeNeeded = false;
glob->partition_directory = NULL;

6.1.1 为什么要有全局对象

如果查询包含子查询:

SELECT *
FROM orders
WHERE amount > (
    SELECT AVG(amount)
    FROM order_history
);

可能形成:

PlannerGlobal
├── 顶层 PlannerInfo:orders
└── 子查询 PlannerInfo:order_history

两个 Query 层级需要共同维护:

subplans
subroots
PARAM_EXEC 类型
最终范围表
关系依赖
分区裁剪信息

如果全部放进某个局部 PlannerInfo,跨 Query 层级的子计划和参数就难以统一管理。

6.1.2 重要字段分组

字段 作用
boundParams 已知的外部参数值
subplans 最终产生的子计划
subpaths 子计划对应的 Path
subroots 子计划对应的 PlannerInfo
finalrtable 所有 Query 层级合并后的最终范围表
paramExecTypes 内部 PARAM_EXEC 参数类型
partPruneInfos 执行期分区裁剪信息
relationOids 计划依赖的关系 OID
invalItems 计划缓存失效依赖
rewindPlanIDs 需要支持 rewind 的子计划
partition_directory 规划期间的分区描述缓存

6.1.3 本步骤的输入和输出

输入:
    boundParams

输出:
    一个初始化完成的 PlannerGlobal

本步骤没有生成 Path,也没有进行关系代数重写。


第 2 步:判断是否允许并行规划

首先执行低成本检查:

if ((cursorOptions & CURSOR_OPT_PARALLEL_OK) != 0 &&
    IsUnderPostmaster &&
    parse->commandType == CMD_SELECT &&
    !parse->hasModifyingCTE &&
    max_parallel_workers_per_gather > 0 &&
    !IsParallelWorker())

条件含义:

条件 含义
CURSOR_OPT_PARALLEL_OK 调用方允许考虑并行计划
IsUnderPostmaster 当前是正常服务器后端进程
CMD_SELECT 当前是可并行考虑的查询命令
!hasModifyingCTE 没有修改数据的 CTE
max_parallel_workers_per_gather > 0 配置允许使用 worker
!IsParallelWorker() 当前进程本身不是 worker

低成本检查通过后,再遍历 Query 树:

glob->maxParallelHazard =
    max_parallel_hazard(parse);

glob->parallelModeOK =
    glob->maxParallelHazard != PROPARALLEL_UNSAFE;

max_parallel_hazard() 会检查:

targetList
WHERE 和 JOIN 条件
HAVING
函数调用
子查询
CTE

函数和表达式可能是:

PROPARALLEL_SAFE
PROPARALLEL_RESTRICTED
PROPARALLEL_UNSAFE

6.2.1 parallelModeOK 不等于最终一定并行

glob->parallelModeOK = true;

只表示:

后续允许生成和比较 Parallel Path。

是否最终选择并行计划,还要取决于:

Parallel Path 是否能够生成
计划是否 parallel_safe
worker 数量
parallel_setup_cost
parallel_tuple_cost
并行方案总成本

6.2.2 本步骤的输入和输出

输入:
    Query *parse
    cursorOptions
    并行相关 GUC

输出:
    glob->maxParallelHazard
    glob->parallelModeOK
    glob->parallelModeNeeded 的初始值

本步骤只判断并行资格,不生成 Gather,也不选择并行计划。


第 3 步:计算 tuple_fraction

核心代码:

if (cursorOptions & CURSOR_OPT_FAST_PLAN)
{
    tuple_fraction = cursor_tuple_fraction;

    if (tuple_fraction >= 1.0)
        tuple_fraction = 0.0;
    else if (tuple_fraction <= 0.0)
        tuple_fraction = 1e-10;
}
else
{
    tuple_fraction = 0.0;
}

6.3.1 tuple_fraction 的含义

tuple_fraction 表示:

调用方预计会消费最终结果中的多少元组。

它不是:

WHERE 条件选择率
表过滤后剩余比例
JOIN 选择率

一般含义:

tuple_fraction = 0
    预计读取全部结果

0 < tuple_fraction < 1
    预计读取结果的一部分,数值表示比例

tuple_fraction >= 1
    在部分内部场景中表示预计读取的绝对行数

普通查询通常从:

tuple_fraction = 0.0;

开始,也就是优先考虑完整执行的 total_cost

LIMIT 会在后续规划过程中进一步影响结果消费目标。游标也可以通过 CURSOR_OPT_FAST_PLAN 使用 cursor_tuple_fraction,但游标不是本文重点。

6.3.2 为什么它会改变最优 Path

Path 同时具有:

startup_cost;
total_cost;

只读取部分结果时,近似成本为:

fractional_cost
    = startup_cost
    + fraction × (total_cost - startup_cost)

例如:

Path A:启动快,但完整执行慢
startup_cost = 5
total_cost   = 1000

Path B:启动慢,但完整执行快
startup_cost = 100
total_cost   = 500

读取全部结果时选择 B;只读取很少结果时可能选择 A。

6.3.3 贯穿示例

示例查询包含:

LIMIT 10

虽然 standard_planner() 初始可能设置:

tuple_fraction = 0.0;

grouping_planner() 处理 LIMIT 时,会利用预计总行数和 limitCount 调整规划目标,更重视能够快速得到前 10 行的有序 Path。

6.3.4 本步骤的输入和输出

输入:
    cursorOptions
    cursor_tuple_fraction

输出:
    tuple_fraction

本步骤不生成 Path,只确定后续成本选择的目标。


第 4 步:调用 subquery_planner()

核心代码:

root = subquery_planner(glob,
                        parse,
                        NULL,
                        false,
                        tuple_fraction,
                        NULL);

这是 standard_planner() 中最重要的一步。

6.4.1 subquery_planner() 的作用

它负责:

对一个完整 Query 层级进行逻辑预处理和物理优化,返回该层级的 PlannerInfo

返回值是:

PlannerInfo *root;

它不是:

Plan
PlannedStmt
最终 best_path

第一次调用时:

parent_root == NULL

所以处理的是顶层 Query,而不是子查询。

遇到真正的子 Query 时,规划器会再次调用 subquery_planner(),并设置:

parent_root != NULL
query_level = parent_root->query_level + 1

6.4.2 第 4 步内部主调用链

可以将它概括为:

subquery_planner()
    │
    ├── 创建 PlannerInfo
    ├── 处理 CTE 和 SubLink
    ├── 子查询提升
    ├── 表达式预处理
    ├── 外连接化简
    ├── 继承和分区展开
    │
    ▼
grouping_planner()
    │
    ├── 普通关系规划
    ├── GROUP BY / Aggregate
    ├── Window
    ├── DISTINCT
    ├── ORDER BY
    └── LIMIT
    │
    ▼
query_planner()
    │
    ├── 建立基本表 RelOptInfo
    ├── 分解 WHERE 和 JOIN 条件
    ├── 建立 RestrictInfo
    ├── 建立 EquivalenceClass
    ├── 生成基本表 Path
    └── 枚举连接关系和 Join Path

6.4.3 关系代数等价变换在哪里

在 13 步流程中,关系代数相关逻辑主要集中在第 4 步。

但还需要区分进入 standard_planner() 前的 Query Rewriter:

进入 standard_planner() 之前:
    pg_rewrite_query()
    负责视图、Rule System 和 RLS 展开

第 4 步 subquery_planner():
    子查询提升
    SubLink 转 JOIN
    表达式简化
    简单 UNION ALL 展开
    外连接化简

第 4 步内部 query_planner():
    谓词分解
    等价类构造
    隐含条件推导

第 4 步内部 Join Search:
    利用连接交换律和结合律枚举合法连接顺序

典型逻辑变换包括:

EXISTS SubLink
    → Semi Join

可以安全提升的 FROM 子查询
    → 合并到父 Query

LEFT JOIN + 拒绝 NULL 的 WHERE 条件
    → Inner Join

a.id = b.id,b.id = 10
    → 推导 a.id = 10

PostgreSQL 并不会为每个连接顺序复制一棵 Query 树。不同连接顺序会在后续表示为同一个 JOIN RelOptInfo 中的不同 Path。

6.4.4 基本表 Path 生成

query_planner() 最终会进入:

make_one_rel()
    ↓
set_base_rel_sizes()
    ↓
set_base_rel_pathlists()

customer 可能生成:

RelOptInfo(customer)
├── SeqScan Path
├── IndexPath(customer_active_idx)
└── BitmapHeapPath

orders 可能生成:

RelOptInfo(orders)
├── SeqScan Path
├── IndexPath(orders_order_date_idx)
├── IndexPath(orders_customer_id_idx)
└── BitmapHeapPath

每个 Path 的成本由对应的 cost_xxx() 计算。

6.4.5 连接顺序和连接方法枚举

两个表时可能生成:

RelOptInfo({customer, orders})
├── HashPath
├── NestPath
└── MergePath

更多表时,标准动态规划搜索按关系集合大小逐层进行:

Level 1
{A} {B} {C}

Level 2
{A,B} {A,C} {B,C}

Level 3
{A,B,C}

主要调用链:

make_one_rel()
    ↓
make_rel_from_joinlist()
    ↓
standard_join_search()
    ↓
join_search_one_level()
    ↓
add_paths_to_joinrel()

外连接、半连接、反连接和 LATERAL 依赖会通过 SpecialJoinInfo 等结构限制连接顺序,不能随意应用交换律和结合律。

6.4.6 Partial Path 的位置

普通完整 Path 保存在:

rel->pathlist;

并行 Partial Path 保存在:

rel->partial_pathlist;

Partial Path 执行一次只产生关系结果的一部分,多个 worker 的结果通过:

Gather
Gather Merge

组合成完整结果。

典型结构:

GatherPath
└── Partial SeqScan Path

需要区分三个属性:

parallel_safe
    允许在 worker 中执行

parallel_aware
    节点知道并行环境并协调工作划分

partial_pathlist
    每个执行者只产生部分结果的候选 Path

parallel_safe 的 Path 不一定是 Partial Path。

6.4.7 聚合、排序和 LIMIT 的 Upper Path

示例查询还会继续产生:

UPPERREL_GROUP_AGG
├── AggPath (AGG_HASHED)
└── AggPath (AGG_SORTED)

UPPERREL_ORDERED
├── SortPath
├── IncrementalSortPath
└── 已经满足排序要求的 Path

UPPERREL_FINAL
└── LimitPath 或最终完成 Path

6.4.8 subquery_planner() 是否已经生成最优 Path

准确答案是:

它已经生成并剪枝候选 Path,也通常已经为各个 RelOptInfo 记录最便宜的启动 Path 和总成本 Path,但顶层最终使用的 best_path 在返回后才明确选出。

返回时已经完成:

Path 生成
成本计算
add_path() 剪枝
set_cheapest() 记录 cheapest Path
Upper RelOptInfo 构造

standard_planner() 仍要执行:

get_cheapest_fractional_path()

根据最终 tuple_fraction 选出顶层 Path。

6.4.9 本步骤的输入和输出

输入:
    PlannerGlobal
    Query
    tuple_fraction
    父查询 PlannerInfo

输出:
    PlannerInfo *root

root 内部已经包含:
    基本表 RelOptInfo
    JOIN RelOptInfo
    Upper RelOptInfo
    RestrictInfo
    EquivalenceClass
    剪枝后的候选 Path

第 5 步:获得最终 RelOptInfo

核心代码:

final_rel =
    fetch_upper_rel(root,
                    UPPERREL_FINAL,
                    NULL);

6.5.1 RelOptInfo 是什么

RelOptInfo 表示:

一个逻辑结果,以及产生这个结果的候选物理实现和相关优化信息。

核心字段可以简化为:

RelOptKind  reloptkind;
Relids      relids;
double      rows;
PathTarget *reltarget;

List       *pathlist;
List       *partial_pathlist;

Path       *cheapest_startup_path;
Path       *cheapest_total_path;
List       *cheapest_parameterized_paths;

同一个 RelOptInfo 中的 Path 应当产生相同逻辑结果,但可以具有不同:

成本
排序属性
参数化属性
并行属性
物理算法

6.5.2 三类 RelOptInfo

基本表 RelOptInfo
    RelOptInfo(customer)

JOIN RelOptInfo
    RelOptInfo({customer, orders})

Upper RelOptInfo
    UPPERREL_GROUP_AGG
    UPPERREL_ORDERED
    UPPERREL_FINAL

6.5.3 什么是 UPPERREL_FINAL

对于贯穿示例,逻辑阶段大致是:

customer 和 orders 基本关系
        ↓
customer JOIN orders
        ↓
WHERE 过滤完成
        ↓
GROUP BY / Aggregate
        ↓
HAVING
        ↓
ORDER BY
        ↓
LIMIT
        ↓
UPPERREL_FINAL

UPPERREL_FINAL 表示:

已经满足整条 SQL 最终语义要求的逻辑结果。

它不是物理表,也不是最终 Plan。

6.5.4 “完整查询候选执行方式的最终容器”怎么理解

假设 final_rel->pathlist 中保留两个顶层 Path:

候选 A
LimitPath
└── SortPath
    └── AggPath (AGG_HASHED)
        └── HashPath
            ├── SeqScan Path (customer)
            └── SeqScan Path (orders)
候选 B
LimitPath
└── IncrementalSortPath
    └── AggPath (AGG_SORTED)
        └── NestPath
            ├── IndexPath(customer)
            └── IndexPath(orders)

每个顶层 Path 都通过子 Path 指针形成一棵完整 Path Tree。沿着其中任何一个顶层 Path 向下遍历,都能得到从最终输出到底层扫描的端到端实现。

因此:

final_rel
    是完整查询逻辑结果的 RelOptInfo

final_rel->pathlist
    保存经过剪枝后仍然有价值的完整候选 Path 根节点

这里的“所有候选”不是历史上生成过的每一个 Path。add_path() 已经删除被完全支配的方案。

6.5.5 fetch_upper_rel() 做了什么

概念上:

在 root 的 Upper Relation 集合中,
查找 kind = UPPERREL_FINAL 的 RelOptInfo。

fetch_upper_rel() 具有获取或创建 Upper RelOptInfo 的能力,但在这里,grouping_planner() 已经创建并填充了最终关系,所以主要是在取回它。

第三个参数为:

NULL

表示这个最终 Upper Relation 不再按某个具体基本表集合区分;当前 Query 层级通常只有一个最终输出关系。

6.5.6 本步骤的输入和输出

输入:
    PlannerInfo *root

输出:
    RelOptInfo *final_rel

final_rel 中包含:
    最终输出 PathTarget
    最终结果行数估计
    完整查询候选 Path 根节点

本步骤只是取得最终关系,不重新执行 Path 搜索。


第 6 步:选择最优 Path

核心代码:

best_path =
    get_cheapest_fractional_path(final_rel,
                                 tuple_fraction);

最终顶层选择函数是:

get_cheapest_fractional_path()

但最优 Path 的形成经历四个阶段:

cost_xxx()
    计算候选成本
        ↓
add_path()
    删除被支配候选
        ↓
set_cheapest()
    记录每个 RelOptInfo 的 cheapest Path
        ↓
get_cheapest_fractional_path()
    选择完整查询最终 Path

6.6.1 成本如何产生

典型成本函数:

cost_seqscan()
cost_index()
cost_bitmap_heap_scan()

initial_cost_nestloop()
final_cost_nestloop()

initial_cost_hashjoin()
final_cost_hashjoin()

initial_cost_mergejoin()
final_cost_mergejoin()

cost_sort()
cost_incremental_sort()
cost_agg()
cost_gather()
cost_gather_merge()

每个 Path 保存:

path->startup_cost;
path->total_cost;
path->rows;

这些是估算成本,不是实际毫秒数。

6.6.2 add_path() 如何剪枝

新 Path 通过:

add_path(rel, new_path);

尝试加入:

rel->pathlist;

核心源码位置:

src/backend/optimizer/util/pathnode.c
    compare_path_costs_fuzzily()
    add_path_precheck()
    add_path()
    add_partial_path_precheck()
    add_partial_path()
    set_cheapest()

这里的剪枝不是“只留下 total_cost 最低的 Path”,而是维护一组:

在成本、排序、参数化、行数和并行安全性等维度上互不支配的 Path。

这组候选可以理解为 Path 的 Pareto Frontier(帕累托前沿)。

每次加入新 Path 时,add_path() 会把它和 rel->pathlist 中已经存在的
Path 逐个比较:

新 Path 被旧 Path 支配
    ↓
丢弃 new_path

新 Path 支配旧 Path
    ↓
删除 old_path,保留 new_path

双方各有优势
    ↓
两个 Path 都保留

函数内部使用两个关键标志:

bool accept_new = true;
bool remove_old = false;

其中:

accept_new = false
    已经找到一个能够支配 new_path 的旧 Path

remove_old = true
    new_path 能够支配当前 old_path

一个新 Path 可能同时支配并删除多个旧 Path。

6.6.3 剪枝比较哪些维度

add_path() 综合比较:

维度 Path 中的信息 哪一方更有优势
被禁用节点数 disabled_nodes 越少越好
启动成本 startup_cost 越小越好
总成本 total_cost 越小越好
排序能力 pathkeys 能满足更多有用顺序更好
外部参数依赖 PATH_REQ_OUTER(path) 依赖集合越小,适用范围越广
输出行数 rows 在其他条件不差时越少越好
并行安全性 parallel_safe truefalse 更有价值

disabled_nodes 是高优先级比较维度。例如用户设置:

SET enable_seqscan = off;

并不意味着优化器完全不能生成顺序扫描,而是使用被禁用节点的 Path 会带有
更高的 disabled_nodes。只要存在不使用被禁用节点的可行 Path,它就会优先。

6.6.4 成本采用模糊比较

成本比较主要通过:

compare_path_costs_fuzzily(new_path,
                           old_path,
                           STD_FUZZ_FACTOR);

其中:

#define STD_FUZZ_FACTOR 1.01

这表示约 1% 以内的成本差异可以被视为“模糊相等”。这样做能够:

避免浮点误差导致计划不稳定
避免为没有实际意义的微小成本差异保留大量 Path
降低优化器的时间和内存开销

返回值有四种:

COSTS_EQUAL
    启动成本和总成本都近似相同

COSTS_BETTER1
    第一个 Path 在成本上支配第二个

COSTS_BETTER2
    第二个 Path 在成本上支配第一个

COSTS_DIFFERENT
    一个启动成本更好,另一个总成本更好

例如:

Path startup_cost total_cost
Index Path 2 130
SeqScan Path 20 100

Index Path 更快产生第一批元组,SeqScan Path 读取全部结果的成本更低。
双方不能互相支配,所以都会保留:

Index Path
    适合 LIMIT、游标或只消费少量结果

SeqScan Path
    适合读取完整结果

这也是为什么剪枝阶段不能只保留 cheapest_total_path

6.6.5 排序能力为什么会阻止剪枝

成本比较之后,add_path() 通过:

compare_pathkeys(new_path_pathkeys,
                 old_path_pathkeys);

比较两个 Path 的排序能力,可能得到:

PATHKEYS_EQUAL
PATHKEYS_BETTER1
PATHKEYS_BETTER2
PATHKEYS_DIFFERENT

例如:

Path A
    total_cost = 100
    无序

Path B
    total_cost = 105
    已按 total DESC 排序

虽然 Path B 略贵,但它可能直接满足示例查询:

ORDER BY total DESC
LIMIT 10;

如果删除 Path B,后续可能必须在 Path A 上增加:

SortPath
└── Path A

因此,只要 Path B 的排序能力有后续价值,它就可能继续保留。

如果一个 Path 按 (a) 排序,另一个按 (b) 排序,两种顺序互不包含,
compare_pathkeys() 会返回 PATHKEYS_DIFFERENT,通常两个 Path 都会保留。

6.6.6 参数化 Path 怎么比较

参数化 Path 通过:

PATH_REQ_OUTER(path)

记录执行它之前必须由哪些外部关系提供参数。

例如:

普通 SeqScan Path
    required_outer = {}

参数化 orders IndexPath
    required_outer = {customer}

参数化 orders IndexPath 只有获得当前 customer.id 后才能执行:

NestPath
├── customer Path
└── parameterized orders IndexPath
    Index Cond: orders.customer_id = customer.id

参数依赖集合越小,通常代表适用范围越广。但参数化更强的 Path 可能已经应用
更多连接条件,因此会产生更少的行。add_path() 需要同时比较:

required_outer 的包含关系
rows
成本
并行安全性

参数集合的比较使用:

bms_subset_compare(PATH_REQ_OUTER(new_path),
                   PATH_REQ_OUTER(old_path));

可能得到:

BMS_EQUAL
BMS_SUBSET1
BMS_SUBSET2
BMS_DIFFERENT

为了限制参数化 Path 数量,PostgreSQL 在 add_path() 中把参数化 Path
pathkeys 当成 NIL

new_path_pathkeys =
    new_path->param_info ? NIL : new_path->pathkeys;

也就是说,参数化 Path 不能只依靠排序优势战胜其他参数化 Path。

6.6.7 一个 Path 何时能够支配另一个

忽略源码中的特殊分支后,可以近似理解为:

if (new_cost <= old_cost &&
    new_pathkeys >= old_pathkeys &&
    new_required_outer ⊆ old_required_outer &&
    new_rows <= old_rows &&
    new_parallel_safe >= old_parallel_safe)
{
    remove(old_path);
}

这里的 <=>= 是“在对应维度上不差”,不是简单的数值比较。

假设已有:

Old Path
    startup_cost   = 10
    total_cost     = 100
    pathkeys       = (a)
    required_outer = {}
    rows           = 1000
    parallel_safe  = true

新生成:

New Path
    startup_cost   = 8
    total_cost     = 90
    pathkeys       = (a)
    required_outer = {}
    rows           = 900
    parallel_safe  = true

新 Path:

启动成本更低
总成本更低
排序相同
外部依赖相同
输出行数更少
并行安全性相同

所以 New Path 支配 Old Path,旧 Path 会被删除。

如果新 Path 改为按 (b) 排序,那么两个排序可能互不包含。即使新 Path
成本更低,也不能保证它对所有后续操作都更好,两个 Path 可能都要保留。

6.6.8 add_path_precheck():构造 Path 前的预剪枝

某些候选 Path 的完整创建和成本计算本身就比较昂贵。PostgreSQL 可以先调用:

add_path_precheck(parent_rel,
                  disabled_nodes,
                  startup_cost,
                  total_cost,
                  pathkeys,
                  required_outer);

此时甚至还没有完整的 Path 对象,只使用:

成本下界
排序信息
参数化信息
disabled_nodes

进行快速判断:

已有 Path 显然在成本、排序和参数化上支配该候选
        ↓
add_path_precheck() 返回 false
        ↓
不再构造完整 Path
        ↓
节省成本计算、内存和比较时间

预检查不能证明候选一定胜出,只能尽早排除明显不可能胜出的候选。

rel->pathlist 会按:

disabled_nodes
    ↓
total_cost

排序,低成本 Path 靠前。这样 add_path_precheck()add_path() 更容易
尽早找到支配者并退出比较。

6.6.9 Partial Path 如何剪枝

并行 Partial Path 保存在:

rel->partial_pathlist;

使用:

add_partial_path(rel, new_path);

进行剪枝。

与普通 add_path() 相比,它主要比较:

disabled_nodes
total_cost
pathkeys

通常不需要比较:

startup_cost
参数化
rows

原因是 PostgreSQL 18.3 不生成参数化 Partial Path;并行候选通常预计执行
到完成,而且同一关系的 Partial Path 应产生相同的完整结果行数。

Partial Path 必须满足:

new_path->parallel_safe == true;
parent_rel->consider_parallel == true;

相应的快速预检查函数是:

add_partial_path_precheck();

6.6.10 被淘汰的 Path 怎么处理

如果新 Path 被旧 Path 支配:

accept_new = false
    ↓
不加入 rel->pathlist
    ↓
释放 new_path

如果新 Path 支配旧 Path:

remove_old = true
    ↓
从 rel->pathlist 删除 old_path
    ↓
释放 old_path

源码通常只释放 Path 节点本身,不递归释放共享的子结构,因为表达式、
PathTarget、子 Path 或 Query 树节点可能被其他候选共同引用。

IndexPath 还有特殊处理:它可能被 BitmapHeapPath 引用,因此被淘汰时
不能像普通 Path 一样立即 pfree()

6.6.11 set_cheapest() 做什么

完成候选生成后:

set_cheapest(rel);

设置:

rel->cheapest_startup_path;
rel->cheapest_total_path;
rel->cheapest_parameterized_paths;

含义:

cheapest_startup_path
    最快返回第一批元组

cheapest_total_path
    返回全部元组的总成本最低

cheapest_parameterized_paths
    不同外部参数依赖下值得保留的 Path

需要特别区分:

add_path()
    剪掉被支配的候选,维护互不支配的 pathlist

set_cheapest()
    不负责主要剪枝,从幸存 Path 中记录几个代表性最优 Path

6.6.12 最终分数成本选择

如果:

tuple_fraction <= 0.0

通常直接使用:

final_rel->cheapest_total_path;

如果只预计读取部分结果,则近似比较:

fractional_cost
    = startup_cost
    + tuple_fraction × (total_cost - startup_cost)

绝对行数形式会先根据预计总行数换算成比例。

6.6.13 贯穿示例

示例中有:

ORDER BY total DESC
LIMIT 10

可能存在:

Path A
    HashAggregate 后全局 Sort
    startup_cost 较高
    total_cost 较低

Path B
    利用已有顺序或 Incremental Sort
    startup_cost 较低
    total_cost 略高

如果只需要前 10 行,B 可能胜出;如果需要全部结果,A 可能胜出。

把整个候选生成和剪枝过程串起来:

cost_xxx()
    估算候选成本
        ↓
add_path_precheck()
    构造前排除明显失败者
        ↓
create_xxx_path()
    创建完整候选 Path
        ↓
add_path()
    维护互不支配的 rel->pathlist
        ↓
set_cheapest()
    记录最低启动成本、最低总成本和参数化代表 Path
        ↓
get_cheapest_fractional_path()
    根据 tuple_fraction 选择顶层 best_path

最重要的结论是:

PostgreSQL 剪掉的是“无论后续如何使用都不可能更优”的 Path,而不是
简单剪掉当前 total_cost 不是最低的 Path。

6.6.14 本步骤的输入和输出

输入:
    final_rel->pathlist
    final_rel->cheapest_total_path
    tuple_fraction

输出:
    Path *best_path

此时仍然是 Path,不是执行器 Plan。


第 7 步:将 Path 转换为 Plan

核心代码:

top_plan = create_plan(root, best_path);

6.7.1 为什么 Path 不能直接执行

Path 主要包含优化器信息:

RelOptInfo
成本
PathKey
参数化关系集合
候选子 Path

执行器需要更具体的信息:

输出 targetlist
过滤 qual
扫描哪个关系
使用哪个索引
JOIN 条件
Hash 条件
排序列编号和操作符
左右子 Plan

因此转换不是强制类型转换,而是重新递归构造 Plan Tree。

6.7.2 常见映射

Path Plan
SeqScan Path(Pathpathtype = T_SeqScan SeqScan
IndexPath IndexScanIndexOnlyScan
BitmapHeapPath BitmapHeapScan
NestPath NestLoop
HashPath HashJoin
MergePath MergeJoin
AggPath Agg
SortPath Sort
IncrementalSortPath IncrementalSort
MemoizePath Memoize
GatherPath Gather
GatherMergePath GatherMerge
AppendPath Append
MaterialPath Material
LimitPath Limit

6.7.3 贯穿示例的转换

假设胜出的 Path Tree 是:

LimitPath
└── SortPath
    └── AggPath (AGG_HASHED)
        └── HashPath
            ├── IndexPath(customer_active_idx)
            └── SeqScan Path (orders)

create_plan() 可能转换为:

Limit
└── Sort
    └── HashAggregate
        └── HashJoin
            ├── IndexScan(customer_active_idx)
            └── Hash
                └── SeqScan(orders)

Hash Join 的内侧会增加执行器需要的 Hash 节点。

6.7.4 转换过程中补充的信息

create_plan() 会:

把 PathTarget 转成 Plan.targetlist
把 RestrictInfo 中的表达式取出并分配到 qual
区分 joinqual、hashclauses 和 mergeclauses
建立 lefttree 和 righttree
确定 scanrelid 和 indexid
建立 Index Cond
建立 NestLoopParam
将 PathKey 转成 Sort 的列编号、操作符和 NULL 顺序
复制成本、行数、宽度和并行属性

6.7.5 只转换胜出的 Path

如果:

final_rel->pathlist
├── Path A
├── Path B
└── Path C

最终:

best_path = Path B;

通常只将 Path B 及其子 Path 转换为 Plan,其他候选不会转换。

6.7.6 Path、Plan 和 PlanState

Path
    优化器候选物理实现

Plan
    最终静态执行计划

PlanState
    执行器运行时状态

完整转换:

Path
    ↓ create_plan()
Plan
    ↓ ExecutorStart() / ExecInitNode()
PlanState
    ↓ ExecutorRun()
实际执行

6.7.7 本步骤的输入和输出

输入:
    PlannerInfo *root
    Path *best_path

输出:
    Plan *top_plan

第 8 步:处理游标和调试 Gather

这一部分发生在最优 Path 已经转换为 Plan 之后,主要是额外执行包装,不是主优化流程。

6.8.1 可滚动游标

if (cursorOptions & CURSOR_OPT_SCROLL)
{
    if (!ExecSupportsBackwardScan(top_plan))
        top_plan =
            materialize_finished_plan(top_plan);
}

如果 SCROLL 游标要求反向读取,而 Plan Tree 不支持 backward scan,则增加:

Material
└── 原 top_plan

Material 将结果保存到 tuplestore,使游标可以向前、向后和重新读取。

6.8.2 调试 Gather

debug_parallel_query 开启时,如果最终 Plan:

top_plan->parallel_safe

PostgreSQL 可以强制包装:

Gather
└── 原 top_plan

这个 Gather:

不参与正常 Path 成本竞争
不以性能优化为目标
主要用于验证 parallel_safe 标记是否正确

为什么在选出最优计划后才添加:

避免改变正常 Path 搜索
避免调试 Gather 因成本高而被淘汰
只测试正常情况下真正胜出的完整 Plan
此时才能访问 targetlist、initPlan 等具体 Plan 信息

需要区分:

正常 Gather
    来源于 GatherPath
    在 Path 阶段参与成本比较

调试 Gather
    在 Plan 生成后强制包装
    仅用于测试

6.8.3 Gather 和 Gather Merge

Gather
    收集 worker 输出,不保证顺序

Gather Merge
    对多个按同一 PathKey 排序的 worker 输出做多路归并,
    保持全局顺序

这部分只需要知道其存在即可,主查询优化仍然发生在前面的 Path 阶段。


第 9 步:完成 Param 和子计划处理

核心代码:

if (glob->paramExecTypes != NIL)
{
    forboth(lp, glob->subplans,
            lr, glob->subroots)
    {
        Plan *subplan = (Plan *) lfirst(lp);
        PlannerInfo *subroot =
            lfirst_node(PlannerInfo, lr);

        SS_finalize_plan(subroot, subplan);
    }

    SS_finalize_plan(root, top_plan);
}

核心函数:

SS_finalize_plan()

6.9.1 两类主要 Param

PARAM_EXTERN
    客户端提供的参数,例如 SQL 中的 $1

PARAM_EXEC
    计划内部在节点之间传递的执行参数

glob->paramExecTypes 记录内部参数类型:

PARAM_EXEC 0:integer
PARAM_EXEC 1:numeric
PARAM_EXEC 2:date

执行器使用:

ParamExecData[]

保存这些值。

6.9.2 InitPlan 示例

SELECT *
FROM orders
WHERE amount > (
    SELECT AVG(amount)
    FROM order_history
);

不相关子查询可能成为 InitPlan:

InitPlan
└── Aggregate
    └── SeqScan(order_history)

它先计算:

PARAM_EXEC 0 = AVG(amount)

主计划使用:

SeqScan(orders)
└── Filter: amount > PARAM_EXEC 0

6.9.3 参数化 Nested Loop 示例

Nested Loop
├── SeqScan(customer)
└── IndexScan(orders_customer_id_idx)

每个外表元组将:

customer.id
    ↓
NestLoopParam
    ↓
PARAM_EXEC 0
    ↓
orders.customer_id = PARAM_EXEC 0

参数变化后,内侧 IndexScan 需要重新扫描。

6.9.4 extParamallParam

每个 Plan 节点包含:

Bitmapset *extParam;
Bitmapset *allParam;

概念上:

extParam
    当前 Plan 子树依赖、但由子树外部提供的 PARAM_EXEC

allParam
    影响当前节点或其子树的全部执行参数集合

执行器根据这些集合判断:

参数变化后哪些节点需要 ExecReScan()

6.9.5 为什么先处理子计划

执行顺序是:

先 SS_finalize_plan(subplan)
再 SS_finalize_plan(main plan)

因为主计划可能引用子计划产生或依赖的参数。只有先完成子计划的参数集合,才能正确计算主计划的 extParamallParam

6.9.6 本步骤的输入和输出

输入:
    主 Plan Tree
    glob->subplans
    glob->subroots
    glob->paramExecTypes

输出:
    每个 Plan 节点正确的 extParam 和 allParam
    完成的 InitPlan/SubPlan 参数依赖

本步骤不改变连接顺序,也不重新选择 Path。


第 10 步:修正计划中的引用

核心代码:

top_plan =
    set_plan_references(root, top_plan);

子计划也需要调用:

lfirst(lp) =
    set_plan_references(subroot, subplan);

核心源码:

src/backend/optimizer/plan/setrefs.c

6.10.1 为什么需要修正 Var

优化器阶段的 Var 可能表示:

Var(varno = 1, varattno = 2)
    customer 的第 2 列

Var(varno = 2, varattno = 4)
    orders 的第 4 列

但执行 JOIN 时,父节点面对的是:

左子计划的 tuple slot
右子计划的 tuple slot

所以需要转换为:

OUTER_VAR
    从左侧子计划输出槽读取

INNER_VAR
    从右侧子计划输出槽读取

例如:

customer.region
    ↓
Var(OUTER_VAR, 左子计划输出列位置)

orders.amount
    ↓
Var(INNER_VAR, 右子计划输出列位置)

执行器不需要重新根据表名或关系集合查找列,只需要按 tuple slot 位置读取。

6.10.2 合并多个 Query 层级的 rtable

每个 Query 都有自己的 rtable,且下标从 1 开始:

顶层 Query.rtable
    1 = customer
    2 = orders

子 Query.rtable
    1 = order_history

最终 PlannedStmt 只有一个统一 rtable

finalrtable
    1 = customer
    2 = orders
    3 = order_history

子计划原来的:

scanrelid = 1

需要调整为:

scanrelid = 3

set_plan_references() 会完成这类编号偏移。

6.10.3 收集最终语句级信息

遍历 Plan Tree 时,会填充:

glob->finalrtable;
glob->finalrteperminfos;
glob->finalrowmarks;
glob->resultRelations;
glob->appendRelations;

这些信息随后进入 PlannedStmt

6.10.4 为什么必须在 create_plan() 后进行

在 Path 阶段还没有具体:

Plan targetlist
lefttree
righttree
scanrelid
JOIN 子节点输出列位置

只有生成 Plan Tree 后,才能确定每个表达式应该从哪个运行时 tuple slot 读取。

6.10.5 本步骤的输入和输出

输入:
    仍带有规划器引用形式的 Plan Tree
    当前 Query 层级的 PlannerInfo

输出:
    执行器可直接使用列引用的 Plan Tree
    合并后的 finalrtable
    最终权限、行锁和目标关系信息

SS_finalize_plan()set_plan_references() 的区别:

SS_finalize_plan()
    解决参数由谁产生、谁使用、变化后谁需要重扫

set_plan_references()
    解决列值从哪个 tuple slot、哪个输出位置读取

第 11 步:构造 PlannedStmt

核心代码:

result = makeNode(PlannedStmt);

随后将规划结果封装到语句级容器:

result->commandType = parse->commandType;
result->queryId = parse->queryId;
result->planTree = top_plan;
result->subplans = glob->subplans;
result->rtable = glob->finalrtable;
result->partPruneInfos = glob->partPruneInfos;
result->paramExecTypes = glob->paramExecTypes;

6.11.1 为什么不能只返回 Plan *

Plan *top_plan 只表示主 Plan Tree。

执行器还需要:

语句类型
SubPlan
最终范围表
权限信息
目标关系
行锁
分区裁剪
内部参数类型
计划缓存依赖
并行模式
JIT 标志

所以需要 PlannedStmt

6.11.2 主要字段

字段 作用
commandType SELECT、INSERT、UPDATE、DELETE 或 MERGE
queryId 查询标识
planTree 主 Plan Tree
subplans SubPlan 和 InitPlan 对应的计划
rtable 合并后的最终范围表
permInfos 权限检查信息
resultRelations 数据修改目标关系
appendRelations 继承和分区列映射
partPruneInfos 运行时分区裁剪步骤
rowMarks FOR UPDATE 等行锁信息
paramExecTypes PARAM_EXEC 类型
rewindPlanIDs 需要 rewind 的子计划编号
relationOids 计划依赖的关系 OID
invalItems 计划缓存失效依赖
parallelModeNeeded 执行时是否进入并行模式
jitFlags JIT 执行选项

6.11.3 计划缓存依赖

如果计划使用:

orders_customer_id_idx

随后索引被删除或相关目录对象发生变化,缓存计划必须失效并重新规划。

relationOids;
invalItems;

帮助计划缓存判断何时需要重新规划。

6.11.4 本步骤的输入和输出

输入:
    top_plan
    PlannerGlobal 中收集的全局结果
    Query 中的命令信息

输出:
    PlannedStmt *result

本步骤主要是收集、整理和封装,不继续枚举 Path。


第 12 步:设置 JIT 标志

首先:

result->jitFlags = PGJIT_NONE;

满足成本条件后:

if (jit_enabled &&
    jit_above_cost >= 0 &&
    top_plan->total_cost > jit_above_cost)
{
    result->jitFlags |= PGJIT_PERFORM;
    ...
}

6.12.1 本步骤没有立即编译机器码

standard_planner() 只设置:

PlannedStmt.jitFlags

真正的 LLVM IR 生成、优化和机器码生成发生在执行阶段。

6.12.2 主要标志

标志 含义
PGJIT_PERFORM 启用 JIT 总开关
PGJIT_EXPR JIT 编译表达式求值
PGJIT_DEFORM JIT 编译 tuple deforming
PGJIT_INLINE 执行函数内联
PGJIT_OPT3 执行更昂贵的 LLVM 优化

6.12.3 为什么根据成本决定

JIT 有额外启动开销:

生成 LLVM IR
执行优化 Pass
生成机器码
装载机器码

短 OLTP 查询可能因为编译开销变慢;扫描大量行、执行复杂表达式和聚合的分析查询更可能受益。

6.12.4 为什么在计划选定后设置

判断依据是:

top_plan->total_cost;

只有完成 Path 搜索、选出 best_path 并创建 Plan 后,才有最终完整计划成本。

因此:

先选择最优 Path
再决定如何执行这个 Plan

JIT 标志通常不会触发重新进行连接顺序和访问路径搜索。

6.12.5 本步骤的输入和输出

输入:
    top_plan->total_cost
    JIT 相关 GUC

输出:
    result->jitFlags

第 13 步:清理并返回

最后:

if (glob->partition_directory != NULL)
    DestroyPartitionDirectory(
        glob->partition_directory);

return result;

6.13.1 清理分区目录

partition_directory 是本次规划期间使用的分区描述缓存:

分区边界
分区层次
分区描述
规划期间重复使用的分区元数据

最终执行器需要的信息已经转换为:

result->partPruneInfos;
result->appendRelations;
result->rtable;
result->planTree;

所以规划器临时使用的 PartitionDirectory 可以销毁。

这不会:

删除分区表
删除 PlannedStmt 的运行时裁剪信息
破坏最终 Plan Tree

6.13.2 为什么不逐个释放 Path 和 RelOptInfo

PostgreSQL 使用 MemoryContext 批量管理内存。

规划期间产生的大量:

PlannerInfo
RelOptInfo
Path
RestrictInfo
EquivalenceClass

通常不在这里逐一 pfree(),而是在对应内存上下文生命周期结束时整体释放。

必须保留:

result
result->planTree
result->subplans
result->rtable
result->partPruneInfos

因为它们是最终规划结果的一部分。

6.13.3 返回后去哪里

standard_planner()
    ↓
planner()
    ↓
pg_plan_query()
    ↓
pg_plan_queries()
    ↓
Portal 或 Plan Cache
    ↓
QueryDesc
    ↓
ExecutorStart()
    ↓
ExecInitNode()
    ↓
PlanState
    ↓
ExecutorRun()

7. 贯穿示例的完整数据流

再次观察示例:

SELECT c.region,
       SUM(o.amount) AS total
FROM customer AS c
JOIN orders AS o
  ON o.customer_id = c.id
WHERE c.active = true
  AND o.order_date >= DATE '2026-01-01'
GROUP BY c.region
HAVING SUM(o.amount) > 10000
ORDER BY total DESC
LIMIT 10;

7.1 进入优化器

Query
├── rtable:customer、orders、JOIN
├── jointree:JOIN + WHERE
├── targetList:region、SUM(amount)
├── groupClause:region
├── havingQual:SUM(amount) > 10000
├── sortClause:total DESC
└── limitCount:10

7.2 基本表优化

RelOptInfo(customer)
├── SeqScan Path
├── IndexPath(customer_active_idx)
└── BitmapHeapPath
RelOptInfo(orders)
├── SeqScan Path
├── IndexPath(orders_order_date_idx)
├── IndexPath(orders_customer_id_idx)
└── BitmapHeapPath

7.3 JOIN 优化

RelOptInfo({customer, orders})
├── HashPath
│   ├── customer Path
│   └── orders Path
├── NestPath
│   ├── customer outer Path
│   └── parameterized orders IndexPath
└── MergePath
    ├── ordered customer Path
    └── ordered orders Path

add_path() 删除被完全支配的方案,set_cheapest() 记录最低启动和最低总成本 Path。

7.4 聚合和上层操作

UPPERREL_GROUP_AGG
├── AggPath (AGG_HASHED)
└── AggPath (AGG_SORTED)
UPPERREL_ORDERED
├── SortPath
├── IncrementalSortPath
└── 已有序 Path
UPPERREL_FINAL
└── 完成 LIMIT 和最终投影的 Path

7.5 最终选择

final_rel =
    fetch_upper_rel(root,
                    UPPERREL_FINAL,
                    NULL);

best_path =
    get_cheapest_fractional_path(
        final_rel,
        tuple_fraction);

7.6 转换为 Plan

假设选择:

LimitPath
└── SortPath
    └── AggPath (AGG_HASHED)
        └── HashPath

转换为:

Limit
└── Sort
    └── HashAggregate
        └── HashJoin
            ├── IndexScan(customer_active_idx)
            └── Hash
                └── SeqScan(orders)

7.7 最终封装

PlannedStmt
├── planTree
│   └── Limit → Sort → HashAggregate → HashJoin
├── rtable
│   ├── customer
│   └── orders
├── permInfos
├── paramExecTypes
├── relationOids
├── invalItems
├── parallelModeNeeded
└── jitFlags

8. 最容易混淆的问题

8.1 Query 是原始语法树吗

不是。

SelectStmt
    原始语法树

Query
    经过语义分析后的查询树

进入 standard_planner()Query 通常还已经经过 pg_rewrite_query()

8.2 subquery_planner() 只处理子查询吗

不是。

第一次调用处理顶层 Query,遇到子 Query 后再递归处理子查询层级。

8.3 subquery_planner() 返回最优 Path 吗

它返回:

PlannerInfo *

它已经生成并筛选候选 Path,但最终顶层:

Path *best_path

get_cheapest_fractional_path() 在它返回后选出。

8.4 RelOptInfo 是 Path 吗

不是。

RelOptInfo
    表示一个逻辑结果,包含多个候选 Path

Path
    表示产生该逻辑结果的一种物理实现

8.5 UPPERREL_FINAL 是最终 Plan 吗

不是。

它是最终逻辑结果对应的 RelOptInfo,其中仍然保存候选 Path。

8.6 cheapest_total_path 一定是最终 best_path

不一定。

如果 tuple_fraction > 0,启动更快的 Path 可能在部分结果成本上更优。

8.7 Path Tree 和 Plan Tree 有什么区别

Path Tree
    用于搜索和成本比较

Plan Tree
    用于执行器初始化

只有胜出的 Path Tree 会被转换。

8.8 Partial Path 是否只用于并行

在 PostgreSQL 优化器术语中,partial_pathlist 是并行规划专用概念。

一个 Partial Path 只产生部分结果,需要 Gather 或 Gather Merge 组合。

但:

parallel_safe Path

不一定是 Partial Path。

8.9 正常 Gather 和调试 Gather 是否相同

不同。

正常 Gather
    来自 GatherPath,参与成本比较

调试 Gather
    在 Plan 生成后强制包装,只用于 parallel safety 测试

8.10 set_plan_references() 是否还在优化

不是。

它将规划器形式的引用转换成执行器形式:

表列 Var
    → OUTER_VAR / INNER_VAR / tuple slot 位置

每个 Query 独立 rtable
    → PlannedStmt 统一 finalrtable

8.11 设置 JIT 标志是否已经执行 JIT

没有。

优化器只设置 jitFlags,真正的 LLVM 编译在执行阶段按需发生。


9. 适合源码跟踪的断点顺序

第一轮只观察主流程:

break planner
break standard_planner
break subquery_planner
break grouping_planner
break query_planner
break make_one_rel
break create_plan

第二轮观察 Path 生成和选择:

break set_base_rel_sizes
break set_base_rel_pathlists
break add_path
break set_cheapest
break standard_join_search
break join_search_one_level
break get_cheapest_fractional_path
break compare_fractional_path_costs

第三轮观察 Plan 最终化:

break SS_finalize_plan
break set_plan_references
break DestroyPartitionDirectory

get_cheapest_fractional_path() 中重点观察:

print tuple_fraction
print rel->rows
print rel->pathlist
print rel->cheapest_startup_path
print rel->cheapest_total_path

standard_planner() 中重点观察:

print parse->commandType
print parse->hasAggs
print parse->hasSubLinks
print glob->parallelModeOK
print root->query_level
print final_rel->rows
print best_path->startup_cost
print best_path->total_cost
print top_plan->type
print result->jitFlags

add_path() 调用次数可能很多,第一次跟踪时可以先禁用,理解基本流程后再单独研究。


10. 推荐源码阅读路线

第一遍:控制流程

src/backend/tcop/postgres.c
    pg_plan_queries()
    pg_plan_query()

src/backend/optimizer/plan/planner.c
    planner()
    standard_planner()
    subquery_planner()
    grouping_planner()

目标:

知道 Query 如何进入优化器
知道每个 Query 层级如何建立 PlannerInfo
知道最终如何得到 PlannedStmt

第二遍:关系与 Path 搜索

src/backend/optimizer/plan/planmain.c
    query_planner()

src/backend/optimizer/path/allpaths.c
    make_one_rel()
    set_base_rel_sizes()
    set_base_rel_pathlists()
    standard_join_search()

src/backend/optimizer/path/joinrels.c
    join_search_one_level()

src/backend/optimizer/path/joinpath.c
    add_paths_to_joinrel()

src/backend/optimizer/util/pathnode.c
    add_path()
    set_cheapest()
    create_xxx_path()

src/backend/optimizer/path/costsize.c
    cost_xxx()

目标:

理解 RelOptInfo
理解候选 Path 如何生成
理解连接顺序如何枚举
理解 Path 如何剪枝和比较

第三遍:Plan 和执行器交接

src/backend/optimizer/plan/createplan.c
    create_plan()

src/backend/optimizer/plan/subselect.c
    SS_finalize_plan()

src/backend/optimizer/plan/setrefs.c
    set_plan_references()

src/backend/executor/execMain.c
    ExecutorStart()
    ExecutorRun()

目标:

理解 Path 如何变成 Plan
理解 Param 和 SubPlan
理解 Var 如何变成 tuple slot 引用
理解 Plan 如何初始化为 PlanState

11. 最终总结

standard_planner() 本身可以压缩成四次关键数据转换:

Query
    ↓ subquery_planner()
PlannerInfo + RelOptInfo + Path
    ↓ get_cheapest_fractional_path()
best Path
    ↓ create_plan()
Plan Tree
    ↓ finalize + setrefs + package
PlannedStmt

13 步的职责可以进一步归纳为:

第 1~3 步
    建立优化环境和优化目标

第 4 步
    完成逻辑预处理、关系构造、Path 生成和成本搜索

第 5~6 步
    取得最终逻辑关系并选出顶层最优 Path

第 7 步
    把胜出的 Path Tree 转换成 Plan Tree

第 8~10 步
    补充执行要求、参数依赖和运行时列引用

第 11~13 步
    封装 PlannedStmt、设置 JIT,并清理规划临时资源

最重要的概念关系是:

Query
    描述查询要什么

RelOptInfo
    描述正在优化哪个逻辑结果

Path
    描述有哪些物理方法可以得到该结果

best_path
    表示成本模型最终选中的方法

Plan
    表示执行器可以初始化的静态计划

PlannedStmt
    表示整条语句完整、可执行、可缓存的规划结果
posted @ 2026-07-28 16:05  aixueforever  阅读(0)  评论(0)    收藏  举报