PostgreSQL内核学习:query_planner 函数详解
- 函数源码
- [SQL 示例解释函数作用](#SQL 示例解释函数作用)
-
- [1. 选择简单示例(覆盖 RTE_RESULT 场景)](#1. 选择简单示例(覆盖 RTE_RESULT 场景))
- [2. 选择复杂示例(覆盖表连接场景)](#2. 选择复杂示例(覆盖表连接场景))
- 实际案例分析
声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书
函数源码
以下 PostgreSQL 中 query_planner() 函数添加的逐行中文注释。
c
/*\
* query_planner
* 为一个基本的查询生成路径(即简化的执行计划),
* 该查询可能包含连接(join),但不包含更复杂的功能(如分组、排序等)。
*\
* 因为 query_planner 不负责顶层处理(分组、排序等),它无法独自选出"最佳路径"。
* 相反,它会返回顶层连接关系的 RelOptInfo,调用者(通常是 grouping_planner)
* 可以从该 rel 的所有存活路径中选择最优的。
*
* root 描述整个查询的 PlannerInfo 结构
* qp_callback 一个回调函数,用于在安全时机计算 query_pathkeys(期望的输出排序)
* qp_extra 可选的额外数据,传递给 qp_callback
*
* 注意:PlannerInfo 里也有一个 query_pathkeys 字段,它告诉 query_planner 最终输出需要的排序顺序。
* 但在调用本函数时这个字段还不可用,必须等到等价类(EC)合并完成后才能由 qp_callback 计算。
*/
RelOptInfo *
query_planner(PlannerInfo *root,
query_pathkeys_callback qp_callback, void *qp_extra)
{
Query *parse = root->parse; // 当前查询的原始解析树(Query 节点)
List *joinlist; // 最终用于 make_one_rel 的连接关系列表
RelOptInfo *final_rel; // 最终的顶层连接 RelOptInfo(本函数主要返回值)
/*\
* 初始化 planner 的各种工作列表为空。
* 注意:append_rel_list 已经在 subquery_planner 中设置好,此处不碰它。
*/
root->join_rel_list = NIL; // 清空已构建的连接关系列表
root->join_rel_hash = NULL; // 清空连接关系的哈希表
root->join_rel_level = NULL; // 清空按层组织的连接关系
root->join_cur_level = 0; // 当前连接层级归零
root->canon_pathkeys = NIL; // 清空规范化的路径键
root->left_join_clauses = NIL; // 清空 LEFT JOIN 条件
root->right_join_clauses = NIL; // 清空 RIGHT JOIN 条件
root->full_join_clauses = NIL; // 清空 FULL JOIN 条件
root->join_info_list = NIL; // 清空特殊连接信息列表
root->placeholder_list = NIL; // 清空占位符信息列表
root->placeholder_array = NULL; // 清空占位符数组
root->placeholder_array_size = 0; // 占位符数组大小归零
root->fkey_list = NIL; // 清空外键匹配信息
root->initial_rels = NIL; // 清空初始基础关系列表
/*\
* 设置用于快速访问基础关系和 AppendRelInfo 的数组
*/
setup_simple_rel_arrays(root); // 初始化 simple_rte_array 和 simple_rel_array
/*\
* 特殊优化:如果 FROM 子句只有一个 RTE_RESULT(无表查询,例如 SELECT 1+2;)
* 则直接构造一个简单的 RelOptInfo 并返回,跳过后续复杂逻辑
* 这对 SELECT 常量、INSERT ... VALUES() 等很常见的情况有明显加速
*/
Assert(parse->jointree->fromlist != NIL);
if (list_length(parse->jointree->fromlist) == 1)
{
Node *jtnode = (Node *) linitial(parse->jointree->fromlist);
if (IsA(jtnode, RangeTblRef))
{
int varno = ((RangeTblRef *) jtnode)->rtindex;
RangeTblEntry *rte = root->simple_rte_array[varno];
Assert(rte != NULL);
if (rte->rtekind == RTE_RESULT) // 无真实表,只有表达式
{
final_rel = build_simple_rel(root, varno, NULL); // 直接构建 RelOptInfo
// 判断是否可以并行执行(子查询中更重要,顶层 Result 通常不并行)
if (root->glob->parallelModeOK &&
(root->query_level > 1 ||
debug_parallel_query != DEBUG_PARALLEL_OFF))
final_rel->consider_parallel =
is_parallel_safe(root, parse->jointree->quals);
// 为这种无表查询构造一个特殊的 GroupResultPath(带 WHERE 条件)
add_path(final_rel, (Path *)
create_group_result_path(root, final_rel,
final_rel->reltarget,
(List *) parse->jointree->quals));
set_cheapest(final_rel); // 选出当前最便宜路径(这里只有一条)
// 假装等价类合并已经完成(虽然这里没生成任何 EC)
root->ec_merging_done = true;
// 仍然要调用回调,可能需要处理 ORDER BY 常量的情况
(*qp_callback) (root, qp_extra);
return final_rel;
}
}
}
/*\
* 为查询中真正用到的所有基础表(base relation)创建 RelOptInfo 节点
* 注意:我们从 jointree 里找,而不是 rangetable,因为 rangetable 可能包含视图等无关项
*/
add_base_rels_to_query(root, (Node *) parse->jointree);
// 删除无意义的 GROUP BY 列(例如重复列或函数依赖的列)
remove_useless_groupby_columns(root);
/*\
* 扫描 targetlist 和 jointree,把所有被引用的 Var 加入 base rel 的 targetlist,
* 把 PlaceHolderVar 注册到 placeholder_list,同时把 restrict/join quals 挂到对应 rel 上
* 同时开始构建 EquivalenceClasses(等价类)
*/
build_base_rel_tlists(root, root->processed_tlist);
find_placeholders_in_jointree(root);
find_lateral_references(root);
// 把 jointree 解构成一个 joinlist(连接顺序无关的连接关系列表)
joinlist = deconstruct_jointree(root);
// 重新审视外连接条件,看是否能因为新生成的 EC 而做更多推导
reconsider_outer_join_clauses(root);
// 根据等价类生成额外的基表 restrict 条件(例如 a=b AND b=5 → a=5)
generate_base_implied_equalities(root);
/*\
* 等价类合并已经完成,现在可以生成规范形式的 pathkeys 了
* 调用回调函数填充 query_pathkeys 等排序相关字段
*/
(*qp_callback) (root, qp_extra);
// 修复占位符的输入所需层级(尤其在外连接被移除后可能需要调整)
fix_placeholder_input_needed_levels(root);
// 删除无意义的 OUTER JOIN(现在已经有足够信息判断)
joinlist = remove_useless_joins(root, joinlist);
// 把满足唯一性约束的 semi-join 降级为普通 inner join
reduce_unique_semijoins(root);
// 删除自身连接中基于唯一键的无意义自连接
joinlist = remove_useless_self_joins(root, joinlist);
// 把占位符按需分发到基础表(join 移除后可能改变可计算位置)
add_placeholders_to_base_rels(root);
// 根据最终的占位符计算层级,生成 lateral 引用信息
create_lateral_join_info(root);
// 匹配外键约束与等价类和 join 条件(用于后续路径生成优化)
match_foreign_keys_to_quals(root);
// 从 JOIN 中的 OR 条件中提取单表 restrict OR 条件
extract_restriction_or_clauses(root);
/*\
* 最后才展开 appendrel(分区表/继承表),因为此时 restrict 条件最多,
* 可以尽可能剪枝掉不满足条件的子分区
*/
add_other_rels_to_query(root);
// 把 UPDATE/DELETE/MERGE 所需的行标识列分发到目标表(必须在 appendrel 展开后)
distribute_row_identity_vars(root);
/*\
* 核心步骤:根据前面准备好的所有信息,真正开始生成连接路径
* 这是 PostgreSQL 中经典的动态规划 / GEQO 选择连接顺序和路径的地方
*/
final_rel = make_one_rel(root, joinlist);
// 安全检查:必须至少有一条可用的非参数化路径
if (!final_rel || !final_rel->cheapest_total_path ||
final_rel->cheapest_total_path->param_info != NULL)
elog(ERROR, "failed to construct the join relation");
return final_rel;
}
SQL 示例解释函数作用
1. 选择简单示例(覆盖 RTE_RESULT 场景)
sql
-- 无 FROM 子句的 SELECT 查询(对应 RTE_RESULT 场景)
SELECT 100 + 200 AS sum_result, current_timestamp AS query_time;
该函数对这个示例的作用:
- 检测到查询的
jointree->fromlist仅有1个元素,且为RTE_RESULT类型(无实际表关联)。 - 直接调用
build_simple_rel构建空的RelOptInfo,无需处理表连接。 - 调用
create_group_result_path生成简单的Result执行路径(无需扫描任何表,直接计算表达式结果)。 - 调用
qp_callback处理潜在的排序需求(该示例无排序,回调函数仅完成基础初始化)。 - 返回
RelOptInfo给上层grouping_planner,最终生成的执行计划仅包含一个Result节点,高效返回计算结果。
2. 选择复杂示例(覆盖表连接场景)
sql
-- 包含两表内连接的查询(无分组、排序,符合 query_planner 处理范围)
SELECT u.id, u.username, o.order_no, o.order_amount
FROM sys_user u
JOIN sys_order o ON u.id = o.user_id
WHERE u.age > 18 AND o.order_amount > 1000;
该函数对这个示例的作用:
- 初始化规划器各类列表,调用
setup_simple_rel_arrays构建基础关系数组。 - 检测到不是
RTE_RESULT场景,调用add_base_rels_to_query为sys_user、sys_order两个基础表构建RelOptInfo。 - 调用
build_base_rel_tlists分析查询目标列(id、username等),添加到对应表的目标列表。 - 调用
deconstruct_jointree解构连接树,提取连接条件(u.id = o.user_id)和限制条件(u.age > 18、o.order_amount > 1000)。 - 构建等价类(
u.id和o.user_id等价),调用qp_callback计算路径键。 - 优化连接逻辑(无无用外连接 / 自连接,无需移除),展开分区表(该示例无分区表,无额外操作)。
- 调用核心函数
make_one_rel,生成两表连接的最优执行路径(如嵌套循环连接、哈希连接),并选择最优路径(cheapest_total_path)。 - 返回顶层连接关系的
RelOptInfo给grouping_planner,上层函数后续会处理排序、分页等额外逻辑(该示例无额外逻辑,直接生成最终执行计划)。
实际案例分析
场景构建
- 创建表(只用两张表)
sql
CREATE TABLE dept (
deptno INT PRIMARY KEY,
dname VARCHAR(30)
);
CREATE TABLE emp (
empno INT PRIMARY KEY,
ename VARCHAR(30),
deptno INT REFERENCES dept(deptno),
sal NUMERIC(10,2)
);
-- 建一点点索引(方便看到索引路径)
CREATE INDEX idx_emp_deptno ON emp(deptno);
- 插入极少量数据(总共才 5 行)
sql
INSERT INTO dept (deptno, dname) VALUES
(10, 'RESEARCH'),
(20, 'SALES');
INSERT INTO emp (empno, ename, deptno, sal) VALUES
(7369, 'SMITH', 20, 800.00),
(7499, 'ALLEN', 20, 1600.00),
(7782, 'CLARK', 10, 2450.00),
(7839, 'KING', 10, 5000.00);
- 推荐用于单步调试的极简查询
sql
SELECT e.ename, e.sal, d.dname
FROM emp e
JOIN dept d ON e.deptno = d.deptno
WHERE e.sal > 1500
AND d.dname = 'RESEARCH'
ORDER BY e.sal DESC;
跟踪调试
bash
(gdb) p *parse
$1 = {type = T_Query, commandType = CMD_SELECT, querySource = QSRC_ORIGINAL, queryId = 0, canSetTag = true,
utilityStmt = 0x0, resultRelation = 0, hasAggs = false, hasWindowFuncs = false, hasTargetSRFs = false, hasSu
bLinks = false, hasDistinctOn = false, hasRecursive = false, hasModifyingCTE = false, hasForUpdate = false,
hasRowSecurity = false, hasGroupRTE = false, isReturn = false, cteList = 0x0, rtable = 0x19bfb90, rteperminf
os = 0x19bfb40, jointree = 0x19ebae8, mergeActionList = 0x0, mergeTargetRelation = 0, mergeJoinCondition = 0
x0, targetList = 0x19ec1a8, override = OVERRIDING_NOT_SET, onConflict = 0x0, returningOldAlias = 0x0, return
ingNewAlias = 0x0, returningList = 0x0, groupClause = 0x0, groupDistinct = false, groupingSets = 0x0, having
Qual = 0x0, windowClause = 0x0, distinctClause = 0x0, sortClause = 0x19eb3d8, limitOffset = 0x0, limitCount
= 0x0, limitOption = LIMIT_OPTION_COUNT, rowMarks = 0x0, setOperations = 0x0, constraintDeps = 0x0, withChec
kOptions = 0x0, stmt_location = 0, stmt_len = 142}
整理后清晰格式(带字段注释):
cpp
$1 = {
type = T_Query, // 节点类型:查询节点
commandType = CMD_SELECT, // 命令类型:SELECT 查询(还有 CMD_INSERT/UPDATE/DELETE 等)
querySource = QSRC_ORIGINAL, // 查询来源:原始查询(非子查询上拉、CTE 等衍生)
queryId = 0, // 查询ID:未分配(仅用于跟踪嵌套查询)
canSetTag = true, // 是否可以设置查询结果标签(如 psql 中的结果行数提示)
utilityStmt = 0x0, // 工具语句:无(仅用于 CREATE/DROP 等非 DML 语句)
resultRelation = 0, // 结果关系表索引:0(SELECT 无结果表,INSERT/UPDATE 对应 rtindex)
hasAggs = false, // 是否包含聚合函数(SUM/COUNT/AVG 等)
hasWindowFuncs = false, // 是否包含窗口函数(ROW_NUMBER()/RANK() 等)
hasTargetSRFs = false, // 是否包含目标列表中的集合返回函数(generate_series() 等)
hasSubLinks = false, // 是否包含子链接(如 EXISTS/IN 子查询)
hasDistinctOn = false, // 是否包含 DISTINCT ON 子句
hasRecursive = false, // 是否包含递归 CTE(WITH RECURSIVE)
hasModifyingCTE = false, // 是否包含修改数据的 CTE(CTE 中包含 INSERT/UPDATE/DELETE)
hasForUpdate = false, // 是否包含 FOR UPDATE/SHARE 行锁子句
hasRowSecurity = false, // 是否启用行级安全策略(RLS)
hasGroupRTE = false, // 是否包含分组关系表(用于 GROUP BY 相关优化)
isReturn = false, // 是否为 RETURNING 子句的查询结果
cteList = 0x0, // CTE 列表:无(对应 WITH 子句定义的公共表达式)
rtable = 0x19bfb90, // 范围表(RTE 列表):存储查询中涉及的表/视图/CTE 等元信息
rteperminfos = 0x19bfb40, // 范围表权限信息:存储各表的访问权限相关数据
jointree = 0x19ebae8, // 连接树:存储查询的 FROM 子句和 WHERE 子句(表连接关系、查询条件)
mergeActionList = 0x0, // 合并操作列表:无(仅用于 MERGE 语句)
mergeTargetRelation = 0, // 合并目标关系:0(仅用于 MERGE 语句)
mergeJoinCondition = 0x0, // 合并连接条件:无(仅用于 MERGE 语句)
targetList = 0x19ec1a8, // 目标列表:存储查询的 SELECT 列(表达式、字段等)
override = OVERRIDING_NOT_SET, // 覆盖选项:未设置(仅用于 INSERT 语句的 OVERRIDING USER VALUE)
onConflict = 0x0, // 冲突处理:无(仅用于 INSERT ... ON CONFLICT)
returningOldAlias = 0x0, // 返回旧数据别名:无(仅用于 UPDATE/DELETE 的 RETURNING)
returningNewAlias = 0x0, // 返回新数据别名:无(仅用于 INSERT/UPDATE 的 RETURNING)
returningList = 0x0, // 返回列表:无(仅用于 DML 语句的 RETURNING 子句)
groupClause = 0x0, // 分组子句:无(对应 GROUP BY 子句)
groupDistinct = false, // 分组是否去重:否(对应 GROUP BY ... DISTINCT)
groupingSets = 0x0, // 分组集合:无(对应 GROUPING SETS/ROLLUP/CUBE)
havingQual = 0x0, // HAVING 条件:无(对应 HAVING 子句,过滤聚合结果)
windowClause = 0x0, // 窗口子句:无(对应 WINDOW 子句,定义窗口函数的窗口)
distinctClause = 0x0, // 去重子句:无(对应 DISTINCT 子句,过滤查询结果)
sortClause = 0x19eb3d8, // 排序子句:有(对应 ORDER BY 子句,定义结果排序规则)
limitOffset = 0x0, // 偏移量:无(对应 OFFSET 子句)
limitCount = 0x0, // 限制行数:无(对应 LIMIT 子句)
limitOption = LIMIT_OPTION_COUNT, // 限制选项:按行数限制(默认,还有 LIMIT_OPTION_ALL 等)
rowMarks = 0x0, // 行标记:无(对应 FOR UPDATE 等行锁的标记信息)
setOperations = 0x0, // 集合操作:无(对应 UNION/INTERSECT/EXCEPT 等)
constraintDeps = 0x0, // 约束依赖:无(存储查询依赖的约束信息)
withCheckOptions = 0x0, // 检查选项:无(仅用于视图的 WITH CHECK OPTION)
stmt_location = 0, // 语句在原始SQL中的起始位置:0
stmt_len = 142 // 语句在原始SQL中的长度:142 字节
}
setup_simple_rel_arrays 函数
该函数的核心价值是「链表转数组 」,因为 PostgreSQL 中范围表(rtable)、继承表列表(append_rel_list)默认以链表(List)存储,链表遍历的时间复杂度为 O (n),而数组访问的时间复杂度为 O (1),后续规划器频繁访问表信息时,数组能大幅提升效率。
函数的核心目的 :把 rtable 链表转换成数组形式(simple_rte_array),同时预分配 simple_rel_array(用于存放后续创建的 RelOptInfo),并处理已存在的继承/分区关系(append_rel_array),为后续规划提供 O(1) 访问能力。
常见后续使用场景(帮助理解为什么需要这些数组)
build_simple_rel(root, varno, ...)→ 通过root->simple_rte_array[varno]快速拿到RTE- 查找某个
relid对应的RelOptInfo→ 直接root->simple_rel_array[relid] - 判断某个
rel是否是某个父表的子分区 →root->append_rel_array[relid]是否非空
以下是 setup_simple_rel_arrays 函数的逐行详细中文注释
c
/*
* setup_simple_rel_arrays
* 准备用于快速访问基础关系(base relations)和 AppendRelInfos 的数组。
*
* 这个函数的主要目的是根据 rangetable(rtable)的大小,初始化几个关键数组,
* 方便后续规划过程中通过 relid(范围表索引)快速找到对应的 RelOptInfo 或 RangeTblEntry。
*/
void
setup_simple_rel_arrays(PlannerInfo *root)
{
int size; // 用于保存数组所需的大小
Index rti; // 范围表索引(从 1 开始编号)
ListCell *lc; // 用于遍历 rtable 链表的游标
/* 数组使用 RT 索引(从 1 到 N)进行访问,因此数组大小需要比 rtable 多一个位置 */
size = list_length(root->parse->rtable) + 1;
/* 记录数组的总大小,后续代码会反复使用这个值来做边界检查 */
root->simple_rel_array_size = size;
/*
* simple_rel_array 是一个指向 RelOptInfo* 的数组
* 初始化时全部置为 NULL,因为此时还没有为任何关系创建 RelOptInfo 节点
* 后续会在 build_simple_rel() 等函数中逐个填充
*/
root->simple_rel_array = (RelOptInfo **)
palloc0(size * sizeof(RelOptInfo *)); // 使用 palloc0 分配并清零
/*
* simple_rte_array 是一个指向 RangeTblEntry* 的数组
* 作用是把链表形式的 rtable 转换成数组形式,方便通过 relid 快速访问
* (链表遍历较慢,数组随机访问效率高)
*/
root->simple_rte_array = (RangeTblEntry **)
palloc0(size * sizeof(RangeTblEntry *));
/* 从 1 开始编号(PostgreSQL 的 relid 从 1 开始,0 不用) */
rti = 1;
/* 把 rtable 链表中的每一项依次放入 simple_rte_array 对应位置 */
foreach(lc, root->parse->rtable)
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
/* 按顺序填入数组,rti 递增 */
root->simple_rte_array[rti++] = rte;
}
/*
* 如果当前没有任何 AppendRelInfo(继承关系或 UNION ALL 展平产生的子关系),
* 则不需要创建 append_rel_array,直接返回
*/
if (root->append_rel_list == NIL)
{
root->append_rel_array = NULL;
return;
}
/*
* 如果存在 AppendRelInfo,则需要创建 append_rel_array 数组
* 用于通过 child_relid 快速找到对应的 AppendRelInfo 结构
*/
root->append_rel_array = (AppendRelInfo **)
palloc0(size * sizeof(AppendRelInfo *));
/*
* 把已有的 append_rel_list 中的每一项填入 append_rel_array
* 注意:此时 append_rel_list 中的内容主要来自 UNION ALL 的展平处理
* 后续在处理表继承/分区时,可能会继续向这个数组添加新的条目
*/
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
int child_relid = appinfo->child_relid;
/* 安全检查:child_relid 必须在合理范围内 */
Assert(child_relid < size);
/*
* 同一个 child_relid 不应该出现两次,否则说明有 bug
* (同一个子表不应被多次附加到不同的父表)
*/
if (root->append_rel_array[child_relid])
elog(ERROR, "child relation already exists");
/* 把 AppendRelInfo 指针放入对应位置 */
root->append_rel_array[child_relid] = appinfo;
}
}
跟踪调试
c
void
setup_simple_rel_arrays(PlannerInfo *root)
{
int size;
Index rti;
ListCell *lc;
/* size = list_length(root->parse->rtable) + 1; */
// root->parse->rtable 里面有 3 个 RTE(emp,dept,unnamed_join)
// 所以 list_length = 3
// size = 3 + 1 = 4
size = 4;
---------------------------------------------------------------------------
(gdb) p size
$1 = 4
(gdb) p root->parse->rtable
$2 = (List *) 0x19bfb90
(gdb) p *root->parse->rtable
$3 = {type = T_List, length = 3, max_length = 5, elements = 0x19bfba8, initial_elements = 0x19bfba8}
---------------------------------------------------------------------------
// root->simple_rel_array_size 被赋值为 4
root->simple_rel_array_size = 4;
/*
* 分配 simple_rel_array,大小为 3 个指针
* 全部初始化为 NULL
* 索引 0 不用,1 和 2 后面会填 RelOptInfo *
*/
root->simple_rel_array = palloc0(4 * sizeof(RelOptInfo *));
// 此时内容相当于:
// root->simple_rel_array[0] = NULL
// root->simple_rel_array[1] = NULL ← 对应 emp
// root->simple_rel_array[2] = NULL ← 对应 dept
// root->simple_rel_array[3] = RTE_JOIN unnamed_join
/*
* 分配 simple_rte_array,大小也是 4 个指针
* 全部初始化为 NULL
*/
root->simple_rte_array = palloc0(4 * sizeof(RangeTblEntry *));
// 开始填充 simple_rte_array
rti = 1; // 从 1 开始编号
// 遍历 root->parse->rtable(链表,有三个元素)
foreach(lc, root->parse->rtable)
{
RangeTblEntry *rte = lfirst(lc);
// 第一次循环:lc 指向第一个 RTE(emp)
// rte = RTE for emp (relkind = RTE_RELATION, relid = pg_class 中的 oid, alias = "e")
root->simple_rte_array[rti++] = rte; // rti=1 → simple_rte_array[1] = emp 的 RTE
---------------------------------------------------------------------------
(gdb) p *rte
$1 = {type = T_RangeTblEntry, alias = 0x1998be8, eref = 0x19bf9e8, rtekind = RTE_RELATION, relid = 16392, in
h = false, relkind = 114 'r', rellockmode = 1, perminfoindex = 1, tablesample = 0x0, subquery = 0x0, securit
y_barrier = false, jointype = JOIN_INNER, joinmergedcols = 0, joinaliasvars = 0x0, joinleftcols = 0x0, joinr
ightcols = 0x0, join_using_alias = 0x0, functions = 0x0, funcordinality = false, tablefunc = 0x0, values_lis
ts = 0x0, ctename = 0x0, ctelevelsup = 0, self_reference = false, coltypes = 0x0, coltypmods = 0x0, colcolla
tions = 0x0, enrname = 0x0, enrtuples = 0, groupexprs = 0x0, lateral = false, inFromCl = true, securityQuals
= 0x0}
(gdb) p *rte.eref
$2 = {type = T_Alias, aliasname = 0x19bfa18 "e", colnames = 0x1999b08}
---------------------------------------------------------------------------
// 第二次循环:lc 指向第二个 RTE(dept)
// rte = RTE for dept (alias = "d")
root->simple_rte_array[rti++] = rte; // rti=2 → simple_rte_array[2] = dept 的 RTE
---------------------------------------------------------------------------
(gdb) p *rte
$3 = {type = T_RangeTblEntry, alias = 0x1998c98, eref = 0x19bfe88, rtekind = RTE_RELATION, relid = 16386, in
h = false, relkind = 114 'r', rellockmode = 1, perminfoindex = 2, tablesample = 0x0, subquery = 0x0, securit
y_barrier = false, jointype = JOIN_INNER, joinmergedcols = 0, joinaliasvars = 0x0, joinleftcols = 0x0, joinr
ightcols = 0x0, join_using_alias = 0x0, functions = 0x0, funcordinality = false, tablefunc = 0x0, values_lis
ts = 0x0, ctename = 0x0, ctelevelsup = 0, self_reference = false, coltypes = 0x0, coltypmods = 0x0, colcolla
tions = 0x0, enrname = 0x0, enrtuples = 0, groupexprs = 0x0, lateral = false, inFromCl = true, securityQuals
= 0x0}
(gdb) p *rte.eref
$4 = {type = T_Alias, aliasname = 0x19bfeb8 "d", colnames = 0x19bff08}
---------------------------------------------------------------------------
// 第三次循环:lc 指向第三个 RTE(RTE_JOIN)
// rte = RTE ("emp ⋈ dept(unnamed_join)")
root->simple_rte_array[rti++] = rte; // rti=3 → simple_rte_array[3] = unnamed_join 的 RTE
---------------------------------------------------------------------------
(gdb) p *rte
$5 = {type = T_RangeTblEntry, alias = 0x0, eref = 0x19c08c8, rtekind = RTE_JOIN, relid = 0, inh = false, rel
kind = 0 '\000', rellockmode = 0, perminfoindex = 0, tablesample = 0x0, subquery = 0x0, security_barrier = f
alse, jointype = JOIN_INNER, joinmergedcols = 0, joinaliasvars = 0x0, joinleftcols = 0x19c0378, joinrightcol
s = 0x19c05a8, join_using_alias = 0x0, functions = 0x0, funcordinality = false, tablefunc = 0x0, values_list
s = 0x0, ctename = 0x0, ctelevelsup = 0, self_reference = false, coltypes = 0x0, coltypmods = 0x0, colcollat
ions = 0x0, enrname = 0x0, enrtuples = 0, groupexprs = 0x0, lateral = false, inFromCl = true, securityQuals
= 0x0}
(gdb) p *rte.eref
$6 = {type = T_Alias, aliasname = 0x19c08f8 "unnamed_join", colnames = 0x19c09a8}
---------------------------------------------------------------------------
}
// 循环结束后:
// root->simple_rte_array[0] = NULL
// root->simple_rte_array[1] = RangeTblEntry * (emp 表)
// root->simple_rte_array[2] = RangeTblEntry * (dept 表)
// root->simple_rte_array[3] = unnamed_join(emp JOIN dept)
/*
* 检查是否有 append_rel_list
* 在这个简单查询中,没有继承、没有分区表、没有 UNION ALL
* 所以 root->append_rel_list == NIL
*/
if (root->append_rel_list == NIL)
{
root->append_rel_array = NULL; // 保持为 NULL
return; // 函数到此结束
}
// 下面部分在本例中不会执行
// ...
}
add_base_rels_to_query 函数
- 函数作用 :遍历整个
jointree,把所有非JOIN的RTE(也就是真正的基础数据来源)都创建出对应的RelOptInfo(baserel)。 - 递归遍历 :通过对
FromExpr和JoinExpr的递归调用,最终会访问到jointree中所有的叶子节点(RangeTblRef)。 - 只处理
baserel:它不会为连接本身(JoinExpr)创建RelOptInfo,也不会处理继承/分区表的子成员(otherrel)。 - 关键调用 :
build_simple_rel(root, varno, NULL)是真正创建RelOptInfo的地方。 - 执行时机 :在
query_planner()中很靠前调用,在setup_simple_rel_arrays之后,确保simple_rte_array已经准备好。
c
/*
* add_base_rels_to_query
*
* 扫描查询的 jointree(连接树),为其中出现的所有基础关系(base relations)
* 创建对应的 RelOptInfo 结构(baserel)。
*
* 所谓基础关系,主要包括:
* - 普通表(RTE_RELATION)
* - 子查询(RTE_SUBQUERY)
* - 函数返回集合(RTE_FUNCTION)
* - VALUES 子句(RTE_VALUES)
* - 其他非 JOIN 的 RTE 类型
*
* 调用约定:
* 第一次调用时,必须传入 root->parse->jointree 作为 jtnode 参数。
* 函数内部会递归遍历整个 jointree 树。
*
* 执行结束时,对于查询中真正被使用的每个非 JOIN 类型的 RTE,
* 都应该已经创建了一个对应的 baserel RelOptInfo。
*
* 注意:这里只处理"基础"关系(baserel),对于继承表/分区表的"成员关系"(otherrel),
* 会在后续阶段(add_other_rels_to_query)再添加。
*/
void
add_base_rels_to_query(PlannerInfo *root, Node *jtnode)
{
// 如果传入的节点为空,直接返回(递归的终止条件之一)
if (jtnode == NULL)
return;
// 当前节点是一个 RangeTblRef,指向 rangetable 中的某一项
if (IsA(jtnode, RangeTblRef))
{
// 获取这个 RangeTblRef 指向的 RTE 的索引(rtindex,从 1 开始编号)
int varno = ((RangeTblRef *) jtnode)->rtindex;
// 为这个 RTE 创建(或获取已存在的)RelOptInfo 结构
// 第二个参数为 NULL,表示这不是 appendrel 的子成员(parent不是在这里处理的)
// 函数 build_simple_rel 内部会检查是否已经存在,如果存在则直接返回
(void) build_simple_rel(root, varno, NULL);
// 注意:这里使用 (void) 忽略返回值,因为我们只关心创建 RelOptInfo 的副作用
// 并不需要使用返回的 RelOptInfo 指针
}
// 当前节点是一个 FromExpr(普通的 FROM 子句,可能包含多个表/连接)
else if (IsA(jtnode, FromExpr))
{
FromExpr *f = (FromExpr *) jtnode;
ListCell *l;
// 遍历 FromExpr 中的 fromlist(可能是多个 RangeTblRef 或 JoinExpr 的列表)
foreach(l, f->fromlist)
{
// 递归处理 fromlist 中的每一个子节点
add_base_rels_to_query(root, lfirst(l));
}
}
// 当前节点是一个 JoinExpr(显式的 JOIN,比如 INNER JOIN、LEFT JOIN 等)
else if (IsA(jtnode, JoinExpr))
{
JoinExpr *j = (JoinExpr *) jtnode;
// 递归处理 JOIN 的左子树(可能是 RangeTblRef、FromExpr 或另一个 JoinExpr)
add_base_rels_to_query(root, j->larg);
// 递归处理 JOIN 的右子树(同上)
add_base_rels_to_query(root, j->rarg);
// 注意:这里并没有为 JoinExpr 本身创建 RelOptInfo
// 因为 JoinExpr 代表的是连接操作,而不是一个基础关系
// 连接关系的 RelOptInfo 会在后续的 make_one_rel 中才生成
}
// 如果遇到不认识的节点类型,报错(防御性编程)
else
elog(ERROR, "unrecognized node type: %d",
(int) nodeTag(jtnode));
}
跟踪调试
在 PostgreSQL 的解析器中,这个查询的 jointree 结构通常会是这样的(简化表示):
c
jointree
└── FromExpr
├── fromlist (length = 1)
│ └── JoinExpr (INNER JOIN)
│ ├── larg → RangeTblRef (rtindex=1) // emp e
│ ├── rarg → RangeTblRef (rtindex=2) // dept d
│ └── quals → (e.deptno = d.deptno) // ON 条件
└── quals → (e.sal > 1500 AND d.dname = 'RESEARCH') // WHERE 条件
也就是说:
- 最外层是一个
FromExpr- 这个
FromExpr的fromlist里只有一个元素:一个JoinExpr(代表整个emp JOIN dept)WHERE条件被挂在这个FromExpr的quals字段上
下一步会发生什么?
函数会进入递归的下一层:
c
add_base_rels_to_query(root, JoinExpr 节点)
然后代码会匹配到:
c
else if (IsA(jtnode, JoinExpr))
{
JoinExpr *j = (JoinExpr *) jtnode;
add_base_rels_to_query(root, j->larg); // emp 的 RangeTblRef
add_base_rels_to_query(root, j->rarg); // dept 的 RangeTblRef
}
于是会继续递归两次:
- 处理
j->larg → RangeTblRef (rtindex = 1) → emp
- 调用
build_simple_rel(root, 1, NULL)- 创建
emp的RelOptInfo →填入simple_rel_array[1]- 处理
j->rarg → RangeTblRef (rtindex = 2) → dept
- 调用
build_simple_rel(root, 2, NULL)- 创建
dept的RelOptInfo →填入simple_rel_array[2]
c
(gdb) bt
#0 add_base_rels_to_query (root=0x19c13a8, jtnode=0x19bfd10) at initsplan.c:164
#1 0x00000000008cb02f in add_base_rels_to_query (root=0x19c13a8, jtnode=0x1998f68) at initsplan.c:180
#2 0x00000000008cafb0 in add_base_rels_to_query (root=0x19c13a8, jtnode=0x19c12d8) at initsplan.c:174
#3 0x00000000008d1ec3 in query_planner (root=0x19c13a8, qp_callback=0x8d8561 <standard_qp_callback>, qp_ext
ra=0x7ffff2bfab10) at planmain.c:170
#4 0x00000000008d4ca7 in grouping_planner (root=0x19c13a8, tuple_fraction=0, setops=0x0) at planner.c:1654
#5 0x00000000008d43e4 in subquery_planner (glob=0x19997e8, parse=0x19998f8, parent_root=0x0, hasRecursion=f
alse, tuple_fraction=0, setops=0x0) at planner.c:1221
#6 0x00000000008d26c4 in standard_planner (parse=0x19998f8, query_string=0x1997c70 "SELECT e.ename, e.sal,
d.dname\nFROM emp e\nJOIN dept d ON e.deptno = d.deptno\nWHERE e.sal > 1500\nAND d.dname = 'RESEARCH'\nORDER
BY e.sal DESC;", cursorOptions=2048, boundParams=0x0) at planner.c:435
#7 0x00000000008d2401 in planner (parse=0x19998f8, query_string=0x1997c70 "SELECT e.ename, e.sal, d.dname\n
FROM emp e\nJOIN dept d ON e.deptno = d.deptno\nWHERE e.sal > 1500\nAND d.dname = 'RESEARCH'\nORDER BY e.sal
DESC;", cursorOptions=2048, boundParams=0x0) at planner.c:295
#8 0x0000000000a28b9a in pg_plan_query (querytree=0x19998f8, query_string=0x1997c70 "SELECT e.ename, e.sal,
d.dname\nFROM emp e\nJOIN dept d ON e.deptno = d.deptno\nWHERE e.sal > 1500\nAND d.dname = 'RESEARCH'\nORDE
调试可能会用到的指令:
bash
# 看看 fromlist 里的唯一元素是什么
(gdb) p *(Node *)linitial(f->fromlist)
(gdb) p nodeTag(linitial(f->fromlist)) # 应该输出 JOIN_EXPR
# 如果确认是 JoinExpr,可以直接看它的结构
(gdb) set $je = (JoinExpr *)linitial(f->fromlist)
(gdb) p *$je
(gdb) p $je->larg
(gdb) p $je->rarg
# 继续单步执行,看是否进入 JoinExpr 分支
(gdb) next
# 或
(gdb) step
deconstruct_jointree 函数
deconstruct_jointree 的核心作用是:把 jointree 中的所有 WHERE 和 ON 条件拆解、分类,并分发到对应的两个基础表(emp 和 dept)的 RelOptInfo 上,同时生成一个简单的 joinlist(包含两个 baserel),为后续的连接路径搜索做好准备。
c
/*
* deconstruct_jointree
* 递归扫描查询的 join tree(jointree),处理 WHERE 和 JOIN/ON 中的限定条件(quals),
* 将它们分类添加到对应的基础关系(base RelOptInfo)的 restrictinfo 或 joininfo 列表中。
*
* 同时,为查询中出现的任何外连接(LEFT/RIGHT/FULL OUTER JOIN)创建 SpecialJoinInfo 节点,
* 并加入到 root->join_info_list 中。
*
* 最终返回一个 "joinlist" 数据结构,它描述了 make_one_rel() 需要处理的连接顺序决策。
* joinlist 中的元素要么是 RangeTblRef(单个基础关系),要么是子 joinlist(子连接问题)。
*
* 说明:
* - 同一层级的 joinlist 中的所有项需要在 make_one_rel 中决定连接顺序
* - 子 joinlist 表示一个需要单独规划的子问题(目前主要由 FULL OUTER JOIN 或
* join_collapse_limit/from_collapse_limit 限制产生)
*/
List *
deconstruct_jointree(PlannerInfo *root)
{
List *result; // 最终返回的 joinlist
JoinDomain *top_jdomain; // 顶层 JoinDomain(管理整个查询的 relids)
List *item_list = NIL; // 临时收集所有 JoinTreeItem 的列表,用于后续分发 quals
ListCell *lc;
/*
* 在这个函数之后,就不允许再创建新的 PlaceHolderInfo 了。
* 因为 make_outerjoininfo 需要在遍历 jointree 时看到所有已存在的占位符。
*/
root->placeholdersFrozen = true;
/* 获取已经创建好的顶层 JoinDomain(一般在之前阶段已初始化) */
top_jdomain = linitial_node(JoinDomain, root->join_domains);
/* 清空顶层 JoinDomain 的 relids,稍后会在递归中填充 */
top_jdomain->jd_relids = NULL;
/* 断言:jointree 不为空且是 FromExpr(最顶层通常是 FromExpr) */
Assert(root->parse->jointree != NULL &&
IsA(root->parse->jointree, FromExpr));
/* 初始化一些全局收集的 relids 集合(后面会填充) */
root->all_baserels = NULL; // 所有基础关系的 relids
root->outer_join_rels = NULL; // 所有外连接涉及的关系 relids
/* 开始递归扫描 jointree,把结构分解并收集信息 */
result = deconstruct_recurse(root, (Node *) root->parse->jointree,
top_jdomain, NULL,
&item_list);
/*
* 现在可以计算 all_query_rels = all_baserels ∪ outer_join_rels
* (代表查询中涉及的所有关系的并集)
*/
root->all_query_rels = bms_union(root->all_baserels, root->outer_join_rels);
/* 断言:顶层 JoinDomain 的 relids 应该与 all_query_rels 一致 */
Assert(bms_equal(root->all_query_rels, top_jdomain->jd_relids));
/*
* 第二遍扫描:把收集到的 quals(限制条件和连接条件)分发到对应的 RelOptInfo 上
* (限制条件挂到 baserel 的 baserestrictinfo,连接条件挂到 joininfo)
*/
foreach(lc, item_list)
{
JoinTreeItem *jtitem = (JoinTreeItem *) lfirst(lc);
deconstruct_distribute(root, jtitem);
}
/*
* 如果查询中存在外连接(SpecialJoinInfo 非空),可能有一些 LEFT JOIN 的条件
* 被推迟处理(postponed),现在再来分发它们
*/
if (root->join_info_list)
{
foreach(lc, item_list)
{
JoinTreeItem *jtitem = (JoinTreeItem *) lfirst(lc);
if (jtitem->oj_joinclauses != NIL)
deconstruct_distribute_oj_quals(root, item_list, jtitem);
}
}
/* 清理临时结构,不再需要 JoinTreeItem 了 */
list_free_deep(item_list);
/* 返回最终的 joinlist,供 make_one_rel 使用 */
return result;
}
跟踪调试
jointree 的形态
bash
FromExpr
fromlist = ( JoinExpr )
quals = (e.sal > 1500 AND d.dname = 'RESEARCH')
deconstruct_recurse 是 deconstruct_jointree 的递归核心函数,它负责深度遍历 jointree(FROM + JOIN + ON 的树形结构),为每一个节点创建 JoinTreeItem,收集所有基础关系的 relids,计算 qualscope / inner_join_rels / nonnullable_rels 等关键范围信息,同时根据 join_collapse_limit 和 FULL JOIN 的规则,构建出适合后续 make_one_rel 使用的 joinlist(连接顺序决策列表)。
用 SQL 示例来具体说明
案例查询是一个最简单的两表内连接 ,没有外连接、没有子查询、没有 FULL JOIN、没有很多表。在这种情况下,deconstruct_recurse 的执行路径和结果会非常简单、清晰。
jointree 是一个 FromExpr,里面只有一个元素:一个 JoinExpr(emp JOIN dept)
bash
(gdb) p *jtnode
$45 = {type = T_FromExpr}
(gdb) p *(FromExpr *)jtnode
$46 = {type = T_FromExpr, fromlist = 0x19c0ad8, quals = 0x1a81ca0}
- 进入
FromExpr分支:- 遍历
fromlist(只有一个JoinExpr) - 递归调用
deconstruct_recurse处理这个JoinExpr
- 遍历
- 进入
JoinExpr分支,且jointype == JOIN_INNER:- 创建
jtitem(JoinTreeItem)给这个JoinExpr - 递归处理左边(
larg):RangeTblRef(rtindex=1) → emp - 递归处理右边(
rarg):RangeTblRef(rtindex=2) → dept - 合并左右的
qualscope → jtitem->qualscope = {1,2} - 因为是
INNER JOIN:jtitem->inner_join_rels = qualscope = {1,2} nonnullable_rels = NULL(内连接不会产生置空关系)
- 创建
c
(gdb) bt
#0 deconstruct_recurse (root=0x19c13a8, jtnode=0x1998f68, parent_domain=0x19c17b8, parent_jtitem=0x1a83820,
item_list=0x7ffff2bfaa20) at initsplan.c:1252
#1 0x00000000008cccca in deconstruct_recurse (root=0x19c13a8, jtnode=0x19c12d8, parent_domain=0x19c17b8, pa
rent_jtitem=0x0, item_list=0x7ffff2bfaa20) at initsplan.c:1223
#2 0x00000000008cc95d in deconstruct_jointree (root=0x19c13a8) at initsplan.c:1111
#3 0x00000000008d1f0d in query_planner (root=0x19c13a8, qp_callback=0x8d8561 <standard_qp_callback>, qp_ext
ra=0x7ffff2bfab10) at planmain.c:191
#4 0x00000000008d4ca7 in grouping_planner (root=0x19c13a8, tuple_fraction=0, setops=0x0) at planner.c:1654
- 两个叶子节点(
RangeTblRef)处理:emp (rtindex=1)root->all_baserels加入1jtitem->qualscope = {1}jtitem->inner_join_rels = NULL(单个表不是内连接)joinlist = list_make1(RangeTblRef(1))
dept (rtindex=2)同理qualscope = {2}joinlist = list_make1(RangeTblRef(2))
c
#1 0x00000000008cceac in deconstruct_recurse (root=0x19c13a8, jtnode=0x1998f68, parent_domain=0x19c17b8, pa
rent_jtitem=0x1a83820, item_list=0x7ffff2bfaa20) at initsplan.c:1266
#2 0x00000000008cccca in deconstruct_recurse (root=0x19c13a8, jtnode=0x19c12d8, parent_domain=0x19c17b8, pa
rent_jtitem=0x0, item_list=0x7ffff2bfaa20) at initsplan.c:1223
#3 0x00000000008cc95d in deconstruct_jointree (root=0x19c13a8) at initsplan.c:1111
#4 0x00000000008d1f0d in query_planner (root=0x19c13a8, qp_callback=0x8d8561 <standard_qp_callback>, qp_ext
ra=0x7ffff2bfab10) at planmain.c:191
#5 0x00000000008d4ca7 in grouping_planner (root=0x19c13a8, tuple_fraction=0, setops=0x0) at planner.c:1654
#6 0x00000000008d43e4 in subquery_planner (glob=0x19997e8, parse=0x19998f8, parent_root=0x0, hasRecursion=f
alse, tuple_fraction=0, setops=0x0) at planner.c:1221
- 回到
JoinExpr层:- 计算
qualscope = {1,2} left_rels = {1}, right_rels = {2}- 因为是
INNER JOIN,且左右子joinlist都很小(各1个元素)list_length(left) + list_length(right) = 1 + 1 = 2 ≤ join_collapse_limit(默认8)- 所以直接合并:
joinlist = list_concat(leftjoinlist, rightjoinlist) - 结果:
joinlist = { RangeTblRef(1), RangeTblRef(2) }
- 计算
- 回到最外层
FromExpr:- 只有一个子
joinlist(来自JoinExpr) - 因为只有一个元素,不需要再做合并
- 最终返回的
joinlist就是:{ RangeTblRef(1), RangeTblRef(2) }
- 只有一个子
make_one_rel 函数
make_one_rel 是 PostgreSQL 查询优化中"生成所有可能执行路径 "的核心函数。它先为每个表生成单表访问路径,再根据 joinlist 使用动态规划(或遗传算法)生成所有合理的连接路径组合,最终返回一个包含多种可能执行方式的 RelOptInfo,供上层选择最优路径。
make_one_rel 在整个流程中的位置
c
query_planner()
├── setup_simple_rel_arrays()
├── add_base_rels_to_query() → 创建 baserel RelOptInfo
├── build_base_rel_tlists() 等 → 收集列、占位符
├── deconstruct_jointree() → 分类 quals,生成 joinlist
└── make_one_rel(root, joinlist) ← 这里!生成所有路径
└── 返回最终 RelOptInfo(包含所有可能路径)
c
/*
* make_one_rel
* 为整个查询找到所有可能的访问路径(access paths),最终返回一个代表
* 查询中所有基础关系(base rels)连接后的单一 RelOptInfo。
*
* 这个函数是查询规划中非常核心的一步,它负责:
* 1. 为每个基础表生成可能的访问路径(顺序扫描、索引扫描、位图扫描等)
* 2. 根据 joinlist 进行连接规划,生成所有可能的连接方式和顺序
* 3. 返回最终的"总 RelOptInfo",包含所有可能的路径(后续由上层选择最优路径)
*/
RelOptInfo *
make_one_rel(PlannerInfo *root, List *joinlist)
{
RelOptInfo *rel; // 最终返回的顶层 RelOptInfo(代表整个查询的连接结果)
Index rti; // 用于遍历 simple_rel_array 的索引
double total_pages; // 查询涉及的所有表的总页面数(用于成本估算)
/*
* 第一步:为每个基础关系设置是否需要考虑"快速启动"(fast-start)计划的标志。
* 例如:某些路径(如 IndexScan)可能启动成本低,但总成本高;
* 而 SeqScan 启动成本高,但总成本可能更低。
* 这个标志会影响后续路径选择(尤其在有 LIMIT 时更重要)。
*/
set_base_rel_consider_startup(root);
/*
* 第二步:为每个基础关系计算行数估计(rows)、页面数(pages)、
* 并行安全标志(consider_parallel)等关键统计信息。
* 这是在前期统计信息(pg_class、pg_statistic)基础上完成的。
*/
set_base_rel_sizes(root);
/*
* 第三步:计算整个查询涉及的所有表的总页面数(total_table_pages)。
* 这个值主要用于后续的成本估算(例如,判断是否值得使用并行扫描、hash join 等)。
*
* 注意事项:
* - 只统计实际用到的表(非 dummy、非删除的)
* - 继承表/分区表的父表 pages 通常为 0,不会重复计数
* - 自连接(self-join)的表会被多次计数(当前实现没有特殊处理)
*/
total_pages = 0;
for (rti = 1; rti < root->simple_rel_array_size; rti++)
{
RelOptInfo *brel = root->simple_rel_array[rti];
/* 跳过空槽位(非基础关系的 RTE,例如 JOIN RTE) */
if (brel == NULL)
continue;
/* 安全检查:确保 relid 与数组索引一致 */
Assert(brel->relid == rti);
/* 跳过 dummy rel(例如被 join removal 删除的关系) */
if (IS_DUMMY_REL(brel))
continue;
/* 只统计简单关系(非连接关系、非 appendrel 父表) */
if (IS_SIMPLE_REL(brel))
total_pages += (double) brel->pages;
}
/* 保存总页面数到 root 中,供后续成本计算使用 */
root->total_table_pages = total_pages;
/*
* 第四步:为每个基础关系生成所有可能的访问路径(Path)。
* 包括:
* - SeqScan
* - IndexScan / IndexOnlyScan
* - BitmapHeapScan + BitmapIndexScan
* - TidScan(如果有 CTID 条件)
* - ForeignScan(如果是外表)
* - 等等
*
* 这些路径会被添加到对应 RelOptInfo 的 pathlist 中。
*/
set_base_rel_pathlists(root);
/*
* 第五步(最核心步骤):根据前面生成的 joinlist,进行连接规划。
* - 遍历 joinlist 中的所有项(可能是 RangeTblRef 或子 joinlist)
* - 使用动态规划(或 GEQO 遗传算法,当表很多时)生成所有可能的连接顺序和连接方式
* - 尝试 NestLoop、HashJoin、MergeJoin 等不同 join 类型
* - 为每种组合生成路径,计算成本
* - 最终构建出代表整个查询连接结果的 RelOptInfo(即 rel)
*
* 这个函数内部会调用 make_rel_from_joinlist,它是连接顺序搜索的入口。
*/
rel = make_rel_from_joinlist(root, joinlist);
/*
* 最终检查:返回的 rel 应该正好覆盖查询中所有基础关系 + 外连接关系,
* 没有多也没有少。
*/
Assert(bms_equal(rel->relids, root->all_query_rels));
/* 返回这个代表整个查询的 RelOptInfo */
return rel;
}