【PostgreSQL内核学习:query_planner 函数详解】

PostgreSQL内核学习:query_planner 函数详解

声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。

本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

函数源码

以下 PostgreSQLquery_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;

该函数对这个示例的作用:

  1. 检测到查询的 jointree->fromlist 仅有 1 个元素,且为 RTE_RESULT 类型(无实际表关联)。
  2. 直接调用 build_simple_rel 构建空的 RelOptInfo,无需处理表连接。
  3. 调用 create_group_result_path 生成简单的 Result 执行路径(无需扫描任何表,直接计算表达式结果)。
  4. 调用 qp_callback 处理潜在的排序需求(该示例无排序,回调函数仅完成基础初始化)。
  5. 返回 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;

该函数对这个示例的作用:

  1. 初始化规划器各类列表,调用 setup_simple_rel_arrays 构建基础关系数组。
  2. 检测到不是 RTE_RESULT 场景,调用 add_base_rels_to_querysys_usersys_order 两个基础表构建 RelOptInfo
  3. 调用 build_base_rel_tlists 分析查询目标列(idusername 等),添加到对应表的目标列表。
  4. 调用 deconstruct_jointree 解构连接树,提取连接条件(u.id = o.user_id)和限制条件(u.age > 18、o.order_amount > 1000)。
  5. 构建等价类(u.ido.user_id 等价),调用 qp_callback 计算路径键。
  6. 优化连接逻辑(无无用外连接 / 自连接,无需移除),展开分区表(该示例无分区表,无额外操作)。
  7. 调用核心函数 make_one_rel,生成两表连接的最优执行路径(如嵌套循环连接、哈希连接),并选择最优路径(cheapest_total_path)。
  8. 返回顶层连接关系的 RelOptInfogrouping_planner,上层函数后续会处理排序、分页等额外逻辑(该示例无额外逻辑,直接生成最终执行计划)。

实际案例分析

场景构建

  1. 创建表(只用两张表)
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);
  1. 插入极少量数据(总共才 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);
  1. 推荐用于单步调试的极简查询
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,把所有非 JOINRTE(也就是真正的基础数据来源)都创建出对应的 RelOptInfobaserel)。
  • 递归遍历 :通过对 FromExprJoinExpr 的递归调用,最终会访问到 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
  • 这个 FromExprfromlist 里只有一个元素:一个 JoinExpr(代表整个 emp JOIN dept
  • WHERE 条件被挂在这个 FromExprquals 字段上

下一步会发生什么?

函数会进入递归的下一层:

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
}

于是会继续递归两次:

  1. 处理 j->larg → RangeTblRef (rtindex = 1) → emp
    • 调用 build_simple_rel(root, 1, NULL)
    • 创建 empRelOptInfo → 填入 simple_rel_array[1]
  2. 处理 j->rarg → RangeTblRef (rtindex = 2) → dept
    • 调用 build_simple_rel(root, 2, NULL)
    • 创建 deptRelOptInfo → 填入 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 中的所有 WHEREON 条件拆解、分类,并分发到对应的两个基础表(empdept)的 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_recursedeconstruct_jointree 的递归核心函数,它负责深度遍历 jointreeFROM + JOIN + ON 的树形结构),为每一个节点创建 JoinTreeItem,收集所有基础关系的 relids,计算 qualscope / inner_join_rels / nonnullable_rels 等关键范围信息,同时根据 join_collapse_limitFULL 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 加入 1
      • jtitem->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_relPostgreSQL 查询优化中"生成所有可能执行路径 "的核心函数。它先为每个表生成单表访问路径,再根据 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;
}
相关推荐
小高不会迪斯科8 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
e***8908 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
testpassportcn9 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
失忆爆表症10 小时前
03_数据库配置指南:PostgreSQL 17 + pgvector 向量存储
数据库·postgresql
AI_567810 小时前
Excel数据透视表提速:Power Query预处理百万数据
数据库·excel
SQL必知必会11 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
Gauss松鼠会11 小时前
【GaussDB】GaussDB数据库开发设计之JDBC高可用性
数据库·数据库开发·gaussdb
+VX:Fegn089511 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计