【PostgreSQL内核学习:通过 ExprState 提升哈希聚合与子计划执行效率(二)】

发布于:2025-08-29 ⋅ 阅读:(12) ⋅ 点赞:(0)

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

引言

  为了更清晰地理解 ExecBuildHash32FromAttrs 函数的运行流程,我将通过一个详细的 SQL 查询示例,结合每行代码的执行,逐步说明该函数如何构建哈希计算的 ExprState
  ExecBuildHash32FromAttrsPostgreSQL 中用于为指定列构建哈希计算执行计划的核心函数,常用于 GROUP BYNOT IN 子查询的哈希操作,特别是在优化补丁(“Use ExprStates for hashing in GROUP BY and SubPlans”)中。本例将使用一个具体的 SQL 查询,覆盖每行代码的解释,并以清晰的方式展示函数的逻辑流程。
  其他详细描述可见:PostgreSQL内核学习:通过 ExprState 提升哈希聚合与子计划执行效率(一)


ExecBuildHash32FromAttrs

示例 SQL 查询

假设有以下 SQL 查询:

SELECT region, department, SUM(amount)
FROM sales
GROUP BY region, department;
  • 表结构

    • sales 包含列:region (VARCHAR), department (VARCHAR), amount (INTEGER)。
    • 示例数据:
      region  | department | amount
      --------+------------+-------
      North   | Sales      | 100
      South   | Marketing  | 200
      North   | Marketing  | 150
      
  • 查询目标:按 regiondepartment 分组,计算 amount 的总和。

  • 哈希计算:需要为 regiondepartment 列生成组合哈希值,用于哈希表分组。

  在执行计划中,PostgreSQL 会调用 ExecBuildHash32FromAttrsGROUP BY 的列(regiondepartment)构建哈希计算的 ExprState,以高效计算哈希值并支持 JIT 编译。


函数运行流程与代码解释

  以下是 ExecBuildHash32FromAttrs 的代码,结合示例查询,逐行解释其执行过程,展示如何为 regiondepartment 构建哈希计算的执行计划。

/* 
 * 构建一个 ExprState,用于对指定的列(attnums,由 keyColIdx 提供)调用哈希函数。
 * 当 numCols > 1 时,将每个哈希函数返回的哈希值组合成一个单一的哈希值。
 *
 * desc: 要哈希的列的元组描述符
 * ops: 用于元组描述符的 TupleTableSlotOps 操作
 * hashfunctions: 每个列的哈希函数(FmgrInfo),数量与 numCols 对应,需保持分配状态
 * collations: 调用哈希函数时使用的排序规则
 * numCols: hashfunctions、collations 和 keyColIdx 的数组长度
 * parent: 评估 ExprState 的 PlanState 节点
 * init_value: 初始哈希值,通常为 0,非零值会略微降低性能,仅在必要时使用
 */
ExprState *
ExecBuildHash32FromAttrs(TupleDesc desc, const TupleTableSlotOps *ops,
						 FmgrInfo *hashfunctions, Oid *collations,
						 int numCols, AttrNumber *keyColIdx,
						 PlanState *parent, uint32 init_value)
{
    // 创建一个新的 ExprState 节点,用于存储哈希计算的执行计划
    ExprState  *state = makeNode(ExprState);
    // 示例:分配一个新的 ExprState 节点,初始化为空,准备存储步骤序列
    // 结果:state->steps 为空,state->parent = NULL

    // 初始化一个临时的 ExprEvalStep 结构,用于构建执行步骤
    ExprEvalStep scratch = {0};
    // 示例:创建一个空的 ExprEvalStep,字段如 opcode、resvalue 等初始化为 0 或 NULL
    // 结果:scratch 用于临时配置每个步骤,随后推入 state->steps

    // 初始化中间结果存储,用于存储多列哈希计算的中间值
    NullableDatum *iresult = NULL;
    // 示例:iresult 初始化为 NULL,后续可能分配内存存储中间哈希值
    // 结果:iresult = NULL

    // 定义操作码,用于指定当前步骤的类型(如提取列值或调用哈希函数)
    intptr_t	opcode;
    // 示例:opcode 用于存储当前步骤的操作类型(如 EEOP_INNER_FETCHSOME)
    // 结果:opcode 未初始化

    // 记录最大列编号,用于确定需要解构的元组范围
    AttrNumber	last_attnum = 0;
    // 示例:last_attnum 初始化为 0,用于跟踪最大列编号
    // 结果:last_attnum = 0

    // 断言列数非负,确保输入参数有效
    Assert(numCols >= 0);
    // 示例:numCols = 2(region 和 department),断言通过
    // 结果:确保 numCols = 2 有效

    // 设置 ExprState 的父节点为传入的 PlanState,用于上下文关联
    state->parent = parent;
    // 示例:parent 是 HashAggregate 节点的 PlanState,设置 state->parent
    // 结果:state->parent = HashAggregate 节点

    /*
     * 如果有多于一个列需要哈希,或者有一个列且有非零初始值,
     * 分配内存用于存储中间哈希值,以便在多列计算时进行组合
     */
    if ((int64) numCols + (init_value != 0) > 1)
        iresult = palloc(sizeof(NullableDatum));
    // 示例:numCols = 2,init_value = 0,条件 (2 + 0 > 1) 满足,分配 iresult
    // 结果:iresult 指向新分配的 NullableDatum,value 和 isnull 初始化为 0

    /* 遍历所有列,找到最大的列编号,以便解构元组到该位置 */
    for (int i = 0; i < numCols; i++)
        last_attnum = Max(last_attnum, keyColIdx[i]);
    // 示例:keyColIdx = [1, 2](region = 1, department = 2)
    // 循环:i = 0, last_attnum = Max(0, 1) = 1
    //       i = 1, last_attnum = Max(1, 2) = 2
    // 结果:last_attnum = 2

    // 设置操作码为提取部分列值(EEOP_INNER_FETCHSOME),准备从元组中提取数据
    scratch.opcode = EEOP_INNER_FETCHSOME;
    // 示例:设置 scratch 的操作码为 EEOP_INNER_FETCHSOME,表示提取元组列
    // 结果:scratch.opcode = EEOP_INNER_FETCHSOME

    // 指定需要提取的最大列编号
    scratch.d.fetch.last_var = last_attnum;
    // 示例:last_attnum = 2,设置提取到第 2 列(department)
    // 结果:scratch.d.fetch.last_var = 2

    // 设置非固定格式,允许动态解构元组
    scratch.d.fetch.fixed = false;
    // 示例:元组格式可能动态变化(如虚拟元组),设为非固定
    // 结果:scratch.d.fetch.fixed = false

    // 指定元组操作类型(如 TTSOpsMinimalTuple)
    scratch.d.fetch.kind = ops;
    // 示例:ops = TTSOpsMinimalTuple(最小元组操作)
    // 结果:scratch.d.fetch.kind = TTSOpsMinimalTuple

    // 设置元组描述符,用于定义列的结构
    scratch.d.fetch.known_desc = desc;
    // 示例:desc 是 sales 表的元组描述符(region: VARCHAR, department: VARCHAR, amount: INTEGER)
    // 结果:scratch.d.fetch.known_desc = sales 表的 TupleDesc

    // 计算元组槽信息并检查是否需要添加提取步骤
    if (ExecComputeSlotInfo(state, &scratch))
        // 示例:调用 ExecComputeSlotInfo 检查是否需要解构元组(因 last_var = 2,需解构)
        // 结果:返回 true,需添加提取步骤

        // 将提取步骤添加到 ExprState 的执行计划中
        ExprEvalPushStep(state, &scratch);
        // 示例:将 scratch(EEOP_INNER_FETCHSOME)推入 state->steps
        // 结果:state->steps = [EEOP_INNER_FETCHSOME {last_var=2, fixed=false, kind=TTSOpsMinimalTuple, known_desc=desc}]

    // 如果初始哈希值为 0
    if (init_value == 0)
    {
        /*
         * 没有初始值,直接使用第一个列的哈希函数结果,无需与初始值组合
         * 设置操作码为 EEOP_HASHDATUM_FIRST,表示首次哈希计算
         */
        opcode = EEOP_HASHDATUM_FIRST;
        // 示例:init_value = 0,设置 opcode 为首次哈希
        // 结果:opcode = EEOP_HASHDATUM_FIRST
    }
    else
    {
        /*
         * 设置初始哈希值的操作,存储到中间结果或 ExprState 的结果字段
         * 如果有列要哈希,存储到中间结果;否则直接存储到 ExprState
         */
        scratch.opcode = EEOP_HASHDATUM_SET_INITVAL;
        // 示例:此分支不执行(init_value = 0)
        // 结果:跳过

        // 将初始值转换为 Datum 类型
        scratch.d.hashdatum_initvalue.init_value = UInt32GetDatum(init_value);
        // 示例:不执行
        // 结果:无

        // 根据是否有列,选择存储位置(中间结果或最终结果)
        scratch.resvalue = numCols > 0 ? &iresult->value : &state->resvalue;
        scratch.resnull = numCols > 0 ? &iresult->isnull : &state->resnull;
        // 示例:不执行
        // 结果:无

        // 将初始值设置步骤添加到执行计划
        ExprEvalPushStep(state, &scratch);
        // 示例:不执行
        // 结果:无

        /*
         * 使用初始值时,后续哈希计算使用 EEOP_HASHDATUM_NEXT32,
         * 以避免覆盖初始值(EEOP_HASHDATUM_FIRST 会覆盖)
         */
        opcode = EEOP_HASHDATUM_NEXT32;
        // 示例:不执行
        // 结果:无
    }

    // 遍历每一列,构建哈希计算的执行步骤
    for (int i = 0; i < numCols; i++)
    {
        // 获取当前列的哈希函数信息
        FmgrInfo   *finfo;
        // 示例:i = 0 时,finfo 将指向 region 的哈希函数;i = 1 时,指向 department 的哈希函数
        // 结果:finfo 未赋值

        // 初始化函数调用信息结构
        FunctionCallInfo fcinfo;
        // 示例:fcinfo 将指向新分配的函数调用信息
        // 结果:fcinfo 未初始化

        // 获取当前列的排序规则
        Oid			inputcollid = collations[i];
        // 示例:i = 0,inputcollid = collations[0](如 C 排序规则)
        // 结果:inputcollid = C

        // 列编号从 1 开始,转换为 0 基索引
        AttrNumber	attnum = keyColIdx[i] - 1;
        // 示例:i = 0,keyColIdx[0] = 1(region),attnum = 1 - 1 = 0
        //       i = 1,keyColIdx[1] = 2(department),attnum = 2 - 1 = 1
        // 结果:attnum = 0(第一次循环),1(第二次循环)

        // 获取当前列的哈希函数
        finfo = &hashfunctions[i];
        // 示例:i = 0,finfo = hashfunctions[0](如 hash_any for VARCHAR)
        // 结果:finfo = hash_any

        // 分配并初始化函数调用信息结构,参数数量为 1
        fcinfo = palloc0(SizeForFunctionCallInfo(1));
        // 示例:分配 FunctionCallInfo 结构,大小为 1 个参数,初始化为 0
        // 结果:fcinfo 指向新分配的内存,args 数组清零

        // 初始化函数调用信息,设置函数、参数数量和排序规则
        InitFunctionCallInfoData(*fcinfo, finfo, 1, inputcollid, NULL, NULL);
        // 示例:设置 fcinfo->flinfo = finfo,nargs = 1,fncollation = C
        // 结果:fcinfo 配置为调用 hash_any,参数数 1,排序规则 C

        /*
         * 设置提取列值的步骤(EEOP_INNER_VAR),将指定列的值存储到哈希函数的第一个参数
         */
        scratch.opcode = EEOP_INNER_VAR;
        // 示例:设置操作码为 EEOP_INNER_VAR,提取列值
        // 结果:scratch.opcode = EEOP_INNER_VAR

        // 设置存储目标为哈希函数的第一个参数
        scratch.resvalue = &fcinfo->args[0].value;
        scratch.resnull = &fcinfo->args[0].isnull;
        // 示例:将列值存储到 fcinfo->args[0].value,NULL 标志存储到 fcinfo->args[0].isnull
        // 结果:scratch.resvalue = &fcinfo->args[0].value, scratch.resnull = &fcinfo->args[0].isnull

        // 设置要提取的列编号
        scratch.d.var.attnum = attnum;
        // 示例:i = 0,attnum = 0(region);i = 1,attnum = 1(department)
        // 结果:scratch.d.var.attnum = 0(第一次),1(第二次)

        // 设置列的数据类型
        scratch.d.var.vartype = TupleDescAttr(desc, attnum)->atttypid;
        // 示例:i = 0,desc[0].atttypid = VARCHAR;i = 1,desc[1].atttypid = VARCHAR
        // 结果:scratch.d.var.vartype = VARCHAR

        // 将提取列值的步骤添加到执行计划
        ExprEvalPushStep(state, &scratch);
        // 示例:i = 0,添加 EEOP_INNER_VAR {attnum=0, vartype=VARCHAR}
        //       i = 1,添加 EEOP_INNER_VAR {attnum=1, vartype=VARCHAR}
        // 结果:state->steps = [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_INNER_VAR {department}]

        // 设置调用哈希函数的步骤,使用之前确定的操作码
        scratch.opcode = opcode;
        // 示例:i = 0,opcode = EEOP_HASHDATUM_FIRST;i = 1,opcode = EEOP_HASHDATUM_NEXT32
        // 结果:scratch.opcode = EEOP_HASHDATUM_FIRST(第一次),EEOP_HASHDATUM_NEXT32(第二次)

        // 如果是最后一列
        if (i == numCols - 1)
        {
            /*
             * 最后一列的哈希结果直接存储到 ExprState 的结果字段
             */
            scratch.resvalue = &state->resvalue;
            scratch.resnull = &state->resnull;
            // 示例:i = 1(department,最后一列),结果存储到 state->resvalue 和 state->resnull
            // 结果:scratch.resvalue = &state->resvalue, scratch.resnull = &state->resnull
        }
        else
        {
            // 确保中间结果已分配
            Assert(iresult != NULL);
            // 示例:i = 0(region),iresult 已分配
            // 结果:断言通过

            // 中间列的哈希结果存储到中间结果中
            scratch.resvalue = &iresult->value;
            scratch.resnull = &iresult->isnull;
            // 示例:i = 0,存储到 iresult->value 和 iresult->isnull
            // 结果:scratch.resvalue = &iresult->value, scratch.resnull = &iresult->isnull
        }

        /*
         * 为 NEXT32 操作码设置中间结果,FIRST 操作码不会使用
         * 为安全起见,始终设置中间结果指针
         */
        scratch.d.hashdatum.iresult = iresult;
        // 示例:设置中间结果指针
        // 结果:scratch.d.hashdatum.iresult = iresult

        // 设置哈希函数信息
        scratch.d.hashdatum.finfo = finfo;
        // 示例:finfo = hash_any
        // 结果:scratch.d.hashdatum.finfo = hash_any

        // 设置函数调用信息
        scratch.d.hashdatum.fcinfo_data = fcinfo;
        // 示例:fcinfo 包含 hash_any 的调用信息
        // 结果:scratch.d.hashdatum.fcinfo_data = fcinfo

        // 设置函数地址
        scratch.d.hashdatum.fn_addr = finfo->fn_addr;
        // 示例:fn_addr = hash_any 的函数地址
        // 结果:scratch.d.hashdatum.fn_addr = hash_any

        // 设置跳转标志,初始为 -1
        scratch.d.hashdatum.jumpdone = -1;
        // 示例:无跳转需求,设为 -1
        // 结果:scratch.d.hashdatum.jumpdone = -1

        // 将哈希函数调用步骤添加到执行计划
        ExprEvalPushStep(state, &scratch);
        // 示例:i = 0,添加 EEOP_HASHDATUM_FIRST {finfo=hash_any, resvalue=iresult}
        //       i = 1,添加 EEOP_HASHDATUM_NEXT32 {finfo=hash_any, resvalue=state->resvalue}
        // 结果:state->steps = [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_HASHDATUM_FIRST, EEOP_INNER_VAR {department}, EEOP_HASHDATUM_NEXT32]

        // 后续列使用 EEOP_HASHDATUM_NEXT32,以组合前面的哈希值
        opcode = EEOP_HASHDATUM_NEXT32;
        // 示例:i = 0 后,opcode 更新为 EEOP_HASHDATUM_NEXT32
        // 结果:opcode = EEOP_HASHDATUM_NEXT32
    }

    // 设置终止步骤,清除结果指针
    scratch.resvalue = NULL;
    scratch.resnull = NULL;
    // 示例:清除结果指针,准备终止步骤
    // 结果:scratch.resvalue = NULL, scratch.resnull = NULL

    // 设置操作码为 EEOP_DONE,表示执行计划结束
    scratch.opcode = EEOP_DONE;
    // 示例:设置终止操作码
    // 结果:scratch.opcode = EEOP_DONE

    // 将终止步骤添加到执行计划
    ExprEvalPushStep(state, &scratch);
    // 示例:添加 EEOP_DONE
    // 结果:state->steps = [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_HASHDATUM_FIRST, EEOP_INNER_VAR {department}, EEOP_HASHDATUM_NEXT32, EEOP_DONE]

    // 准备 ExprState,使其可执行
    ExecReadyExpr(state);
    // 示例:设置 state->evalfunc(如 ExecJustHashVarImpl 或 JIT 编译函数),准备执行
    // 结果:state->evalfunc 设置,ExprState 可执行

    // 返回构建完成的 ExprState
    return state;
    // 示例:返回包含完整步骤序列的 ExprState
    // 结果:返回 state
}

最终 ExprState 结构

  对于示例查询,ExecBuildHash32FromAttrs 生成的 ExprState 包含以下步骤序列:

  1. EEOP_INNER_FETCHSOME

    • last_var = 2(提取到 department 列)。
    • fixed = false, kind = TTSOpsMinimalTuple, known_desc = sales 表的 TupleDesc
    • 作用:解构输入元组,准备提取 regiondepartment
  2. EEOP_INNER_VAR (region)

    • attnum = 0, vartype = VARCHAR
    • resvalue = fcinfo[0]->args[0].value, resnull = fcinfo[0]->args[0].isnull
    • 作用:提取 region 列值,存储到第一个哈希函数的参数。
  3. EEOP_HASHDATUM_FIRST

    • finfo = hash_any, fcinfo_data = hash_any 的调用信息, fn_addr = hash_any
    • resvalue = iresult->value, resnull = iresult->isnull
    • 作用:调用 hash_any 计算 region 的哈希值,存储到中间结果。
  4. EEOP_INNER_VAR (department)

    • attnum = 1, vartype = VARCHAR
    • resvalue = fcinfo[1]->args[0].value, resnull = fcinfo[1]->args[0].isnull
    • 作用:提取 department 列值,存储到第二个哈希函数的参数。
  5. EEOP_HASHDATUM_NEXT32

    • finfo = hash_any, fcinfo_data = hash_any 的调用信息, fn_addr = hash_any
    • resvalue = state->resvalue, resnull = state->resnull
    • iresult:指向中间结果,包含 region 的哈希值。
    • 作用:调用 hash_any 计算 department 的哈希值,与中间结果组合,存储最终哈希值。
  6. EEOP_DONE

    • 作用:终止执行计划。

执行示例

  假设处理元组 (region = "North", department = "Sales", amount = 100)

  1. 调用

    • ExecBuildHash32FromAttrs(desc, TTSOpsMinimalTuple, [hash_any, hash_any], [C, C], 2, [1, 2], HashAggregate, 0)
    • 输入:descsales 表的描述符,keyColIdx = [1, 2]regiondepartment),init_value = 0
  2. 执行步骤

    • EEOP_INNER_FETCHSOME:解构元组,提取 regiondepartment
    • EEOP_INNER_VAR (region):提取 "North",存入 fcinfo[0]->args[0].value
    • EEOP_HASHDATUM_FIRST:调用 hash_any("North"),得到哈希值(如 0x12345678),存入 iresult->value
    • EEOP_INNER_VAR (department):提取 "Sales",存入 fcinfo[1]->args[0].value
    • EEOP_HASHDATUM_NEXT32:调用 hash_any("Sales"),得到哈希值(如 0xabcdef12),与 iresult->value 组合(如通过 hash_combine32),存入 state->resvalue
    • EEOP_DONE:结束,state->resvalue 包含最终哈希值(如 0x7890abcd)。
  3. 结果

    • 返回的 ExprStateHashAggregate 节点使用,哈希值 0x7890abcd 用于哈希表插入或查找。

总结

  ExecBuildHash32FromAttrs 通过构建 ExprState 和一系列 ExprEvalStep,为 GROUP BY 的列(regiondepartment)生成高效的哈希计算计划。每个步骤(提取元组、获取列值、调用哈希函数)被精确配置,减少函数调用开销,支持 JIT 编译。示例查询展示了如何从输入参数到最终 ExprState 的构建,覆盖了每行代码的作用,最终生成一个包含提取、哈希和终止步骤的执行计划,用于高效哈希表操作。

ExecComputeSlotInfo

  ExecComputeSlotInfoPostgreSQL 执行器中用于确定元组槽(TupleTableSlot)是否为固定格式的辅助函数,决定是否需要添加解构步骤(EEOP_*_FETCHSOME),常用于哈希计算等场景。以下内容分为两部分:ExecComputeSlotInfo 的代码注释与解释,以及 ExecBuildHash32FromAttrs 的流程图代码。


示例 SQL 查询

与之前一致,使用以下查询作为上下文:

SELECT region, department, SUM(amount)
FROM sales
GROUP BY region, department;
  • 表结构

    • sales 包含列:region (VARCHAR), department (VARCHAR), amount (INTEGER)。
    • 示例数据:
      region  | department | amount
      --------+------------+-------
      North   | Sales      | 100
      South   | Marketing  | 200
      North   | Marketing  | 150
      
  • 查询目标:按 regiondepartment 分组,计算 amount 的总和。

  • 上下文ExecBuildHash32FromAttrs 调用 ExecComputeSlotInfo 来确定是否需要添加 EEOP_INNER_FETCHSOME 步骤,用于提取 regiondepartment 的值以进行哈希计算。


函数注释与解释

  以下是 PostgreSQLExecComputeSlotInfo 函数的每行代码,添加中文注释并结合 SQL 查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department 解释其作用。
  该函数用于确定元组槽(TupleTableSlot)是否为固定格式(fixed),以决定是否需要添加解构步骤(EEOP_*_FETCHSOME,在哈希计算(如 ExecBuildHash32FromAttrs)中确保正确提取列值。

/*
 * 计算 EEOP_*_FETCHSOME 操作的附加信息。
 *
 * 目标是确定元组槽是否为“固定”格式,即每次表达式求值时槽类型和描述符是否保持一致。
 *
 * 返回 true 表示需要解构步骤,返回 false 表示不需要。
 */
static bool
ExecComputeSlotInfo(ExprState *state, ExprEvalStep *op)
{
    // 获取 ExprState 的父节点(PlanState),用于访问计划信息
	PlanState  *parent = state->parent;
    // 初始化元组描述符,存储槽的列结构
	TupleDesc	desc = NULL;
    // 初始化元组槽操作类型(如 TTSOpsMinimalTuple)
	const TupleTableSlotOps *tts_ops = NULL;
    // 标志位,指示槽是否为固定格式
	bool		isfixed = false;
    // 获取当前操作码(EEOP_INNER_FETCHSOME、EEOP_OUTER_FETCHSOME 或 EEOP_SCAN_FETCHSOME)
	ExprEvalOp	opcode = op->opcode;

    // 断言操作码为提取操作,确保输入有效
	Assert(opcode == EEOP_INNER_FETCHSOME ||
		   opcode == EEOP_OUTER_FETCHSOME ||
		   opcode == EEOP_SCAN_FETCHSOME);
    // 示例:opcode = EEOP_INNER_FETCHSOME(由 ExecBuildHash32FromAttrs 设置)
    // 结果:断言通过

    // 如果已知元组描述符(known_desc)不为空
	if (op->d.fetch.known_desc != NULL)
	{
        // 使用已知的描述符
		desc = op->d.fetch.known_desc;
        // 使用已知的槽操作类型
		tts_ops = op->d.fetch.kind;
        // 如果 kind 不为空,槽格式固定
		isfixed = op->d.fetch.kind != NULL;
        // 示例:op->d.fetch.known_desc = sales 表描述符,kind = TTSOpsMinimalTuple
        // 结果:desc = sales 表 TupleDesc,tts_ops = TTSOpsMinimalTuple,isfixed = true
	}
    // 如果没有已知描述符且无父节点
	else if (!parent)
	{
        // 槽格式非固定
		isfixed = false;
        // 示例:parent = HashAggregate 节点,此分支不执行
        // 结果:无
	}
    // 如果操作码为 EEOP_INNER_FETCHSOME
	else if (opcode == EEOP_INNER_FETCHSOME)
	{
        // 获取内层计划状态
		PlanState  *is = innerPlanState(parent);
        // 示例:is = 扫描 sales 表的 Scan 节点
        // 结果:is = SeqScan 节点

        // 如果内层操作已设置但非固定格式
		if (parent->inneropsset && !parent->inneropsfixed)
		{
            // 槽格式非固定
			isfixed = false;
            // 示例:inneropsfixed = true(通常固定),此分支不执行
            // 结果:无
		}
        // 如果内层操作已设置且有操作类型
		else if (parent->inneropsset && parent->innerops)
		{
            // 槽格式固定
			isfixed = true;
            // 使用内层操作类型
			tts_ops = parent->innerops;
            // 获取内层计划的结果描述符
			desc = ExecGetResultType(is);
            // 示例:innerops = TTSOpsMinimalTuple,desc = sales 表描述符
            // 结果:isfixed = true,tts_ops = TTSOpsMinimalTuple,desc = sales 表 TupleDesc
		}
        // 如果存在内层计划
		else if (is)
		{
            // 获取内层计划的槽操作类型和固定标志
			tts_ops = ExecGetResultSlotOps(is, &isfixed);
            // 获取内层计划的结果描述符
			desc = ExecGetResultType(is);
            // 示例:is = SeqScan 节点,tts_ops = TTSOpsMinimalTuple,isfixed = true
            // 结果:tts_ops = TTSOpsMinimalTuple,desc = sales 表 TupleDesc,isfixed = true
		}
	}
    // 如果操作码为 EEOP_OUTER_FETCHSOME
	else if (opcode == EEOP_OUTER_FETCHSOME)
	{
        // 获取外层计划状态
		PlanState  *os = outerPlanState(parent);
        // 示例:无外层计划(单表查询),此分支不执行
        // 结果:无

        // 如果外层操作已设置但非固定格式
		if (parent->outeropsset && !parent->outeropsfixed)
		{
			isfixed = false;
		}
        // 如果外层操作已设置且有操作类型
		else if (parent->outeropsset && parent->outerops)
		{
			isfixed = true;
			tts_ops = parent->outerops;
			desc = ExecGetResultType(os);
		}
        // 如果存在外层计划
		else if (os)
		{
			tts_ops = ExecGetResultSlotOps(os, &isfixed);
			desc = ExecGetResultType(os);
		}
	}
    // 如果操作码为 EEOP_SCAN_FETCHSOME
	else if (opcode == EEOP_SCAN_FETCHSOME)
	{
        // 使用父节点的扫描描述符
		desc = parent->scandesc;
        // 示例:scandesc = sales 表描述符,此分支不执行(opcode = EEOP_INNER_FETCHSOME)
        // 结果:无

        // 如果扫描操作已设置
		if (parent->scanops)
			tts_ops = parent->scanops;
        // 如果扫描操作已设置固定标志
		if (parent->scanopsset)
			isfixed = parent->scanopsfixed;
	}

    // 如果槽格式固定且描述符和操作类型不为空
	if (isfixed && desc != NULL && tts_ops != NULL)
	{
        // 设置槽为固定格式
		op->d.fetch.fixed = true;
        // 设置槽操作类型
		op->d.fetch.kind = tts_ops;
        // 设置已知描述符
		op->d.fetch.known_desc = desc;
        // 示例:isfixed = true,desc = sales 表 TupleDesc,tts_ops = TTSOpsMinimalTuple
        // 结果:op->d.fetch.fixed = true,kind = TTSOpsMinimalTuple,known_desc = sales 表 TupleDesc
	}
	else
	{
        // 设置槽为非固定格式
		op->d.fetch.fixed = false;
        // 清空槽操作类型
		op->d.fetch.kind = NULL;
        // 清空描述符
		op->d.fetch.known_desc = NULL;
        // 示例:此分支不执行(isfixed = true)
        // 结果:无
	}

    // 如果槽固定且为虚拟槽(TTSOpsVirtual),无需解构
	if (op->d.fetch.fixed && op->d.fetch.kind == &TTSOpsVirtual)
		return false;
    // 示例:kind = TTSOpsMinimalTuple,非虚拟槽
    // 结果:不返回 false,继续检查

    // 返回 true,表示需要解构步骤
	return true;
    // 示例:kind != TTSOpsVirtual,返回 true
    // 结果:返回 true,需添加 EEOP_INNER_FETCHSOME 步骤
}

作用

  ExecComputeSlotInfoPostgreSQL 执行器中的辅助函数,用于为 EEOP_*_FETCHSOME 操作(提取元组列值的步骤)计算元组槽的附加信息,确定槽是否为“固定”格式(即每次求值时槽类型和描述符一致)。它在 ExecBuildHash32FromAttrs 中被调用,决定是否需要添加 EEOP_INNER_FETCHSOME 步骤来解构元组(如提取 regiondepartment 的值)。其核心目标是:

  • 确定槽类型:检查槽是否为固定格式(fixed),并获取槽操作类型(tts_ops)和描述符(desc)。
  • 优化性能:如果槽为虚拟槽(TTSOpsVirtual),无需解构,返回 false,减少步骤;否则返回 true,添加解构步骤。

参数

  • stateExprState *,表达式执行计划,包含父节点(parent)信息。
  • opExprEvalStep *,当前操作步骤,包含 opcode(如 EEOP_INNER_FETCHSOME)和 fetch 子结构(known_desc, kind, fixed)。

返回值

  • booltrue 表示需要解构步骤(添加 EEOP_*_FETCHSOME),false 表示无需解构。

执行流程(结合示例)

  1. 初始化

    • 获取 parentHashAggregate 节点),opcodeEEOP_INNER_FETCHSOME),初始化 desctts_opsisfixed
    • 示例:parentHashAggregateopcode = EEOP_INNER_FETCHSOME
  2. 检查已知描述符

    • 如果 op->d.fetch.known_desc 不为空,直接使用已知的 desctts_ops,设置 isfixed
    • 示例:known_desc = sales 表 TupleDesckind = TTSOpsMinimalTupleisfixed = true
  3. 处理内层计划(EEOP_INNER_FETCHSOME

    • 获取内层计划(SeqScan 节点),检查 parent->inneropssetinneropsfixed
    • 示例:innerops = TTSOpsMinimalTupleinneropsfixed = truedesc = sales 表 TupleDesc
  4. 设置槽信息

    • 如果 isfixed = truedesctts_ops 不为空,设置 op->d.fetch.fixed = truekind = TTSOpsMinimalTupleknown_desc = sales 表 TupleDesc
    • 示例:设置 op->d.fetch 为固定格式,kind = TTSOpsMinimalTuple
  5. 检查虚拟槽

    • 如果槽固定且为 TTSOpsVirtual,返回 false(无需解构)。
    • 示例:kind = TTSOpsMinimalTuple,非虚拟槽,返回 true
  6. 返回结果

    • 示例:返回 true,表示需要添加 EEOP_INNER_FETCHSOME 步骤。

ExecBuildHash32FromAttrs 的关系

  在 ExecBuildHash32FromAttrsExecComputeSlotInfo 被调用以确定是否需要 EEOP_INNER_FETCHSOME 步骤:

  • 输入state 是新建的 ExprStateopscratchopcode = EEOP_INNER_FETCHSOME, last_var = 2, kind = TTSOpsMinimalTuple, known_desc = sales 表 TupleDesc)。
  • 输出:返回 true,触发 ExprEvalPushStep(state, &scratch),添加解构步骤。
  • 作用:确保 regiondepartment 的值被正确提取,供后续哈希计算使用。

功能结构流程图

  以下是 ExecBuildHash32FromAttrs 的 Mermaid 流程图代码,结合 ExecComputeSlotInfo 的调用,展示其完整功能结构。流程图采用纵向布局(TD),使用简洁中文描述,节点为矩形(操作)或菱形(条件),颜色区分不同阶段,布局合理,清晰展示构建 ExprState 的过程。

开始: ExecBuildHash32FromAttrs
初始化 ExprState 和 scratch
设置 parent 和 last_attnum
numCols + init_value > 1?
分配中间结果 iresult
添加 EEOP_INNER_FETCHSOME
调用 ExecComputeSlotInfo
init_value == 0?
设置 opcode = EEOP_HASHDATUM_FIRST
添加 EEOP_HASHDATUM_SET_INITVAL
opcode = EEOP_HASHDATUM_NEXT32
遍历每列 keyColIdx
设置 EEOP_INNER_VAR
提取列值到 fcinfo->args
最后一列?
设置结果到 state->resvalue
设置结果到 iresult
添加哈希步骤
EEOP_HASHDATUM_FIRST/NEXT32
更新 opcode = EEOP_HASHDATUM_NEXT32
更多列?
添加 EEOP_DONE
调用 ExecReadyExpr
返回 ExprState
结束

流程图说明

  • 布局:纵向(TD),从上到下展示 ExecBuildHash32FromAttrs 的逻辑流程,清晰简洁。
  • 节点
    • 矩形表示操作(如初始化、提取列值),菱形表示条件(如 numCols + init_value > 1)。
    • 节点分组:初始化(init,绿色)、条件(condition,黄色)、循环(loop,浅红)、提取/设置(extract/setup,绿色)、存储/哈希(store/hash,蓝色)、结束(finish,灰色)。
  • 逻辑
    • 初始化 ExprStatescratch,设置 parentlast_attnum
    • 检查是否需要中间结果(iresult)。
    • 调用 ExecComputeSlotInfo 添加 EEOP_INNER_FETCHSOME
    • 根据 init_value 设置初始哈希操作(EEOP_HASHDATUM_FIRSTSET_INITVAL)。
    • 循环每列,添加 EEOP_INNER_VAREEOP_HASHDATUM_FIRST/NEXT32,存储结果到 iresultstate->resvalue
    • 添加 EEOP_DONE,调用 ExecReadyExpr,返回 ExprState
  • 与 ExecComputeSlotInfo 的关联
    • 在节点 F(EEOP_INNER_FETCHSOME),调用 ExecComputeSlotInfo 确定槽格式,决定是否添加解构步骤。
    • 示例中,ExecComputeSlotInfo 返回 true,触发添加 EEOP_INNER_FETCHSOME {last_var=2, kind=TTSOpsMinimalTuple}

示例执行总结

对于查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department

  1. ExecBuildHash32FromAttrs 调用

    • 输入:desc = sales 表 TupleDesc, ops = TTSOpsMinimalTuple, hashfunctions = [hash_any, hash_any], collations = [C, C], numCols = 2, keyColIdx = [1, 2], parent = HashAggregate, init_value = 0
    • 过程:
      • 初始化 ExprStatelast_attnum = 2
      • 调用 ExecComputeSlotInfo,确认槽为固定格式(TTSOpsMinimalTuple),返回 true,添加 EEOP_INNER_FETCHSOME
      • region 添加 EEOP_INNER_VAREEOP_HASHDATUM_FIRST(结果存 iresult)。
      • department 添加 EEOP_INNER_VAREEOP_HASHDATUM_NEXT32(结果存 state->resvalue)。
      • 添加 EEOP_DONE,调用 ExecReadyExpr
    • 输出:ExprState 包含步骤 [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_HASHDATUM_FIRST, EEOP_INNER_VAR {department}, EEOP_HASHDATUM_NEXT32, EEOP_DONE]
  2. ExecComputeSlotInfo 作用

    • 确认槽为 TTSOpsMinimalTuple,固定格式,返回 true,确保 EEOP_INNER_FETCHSOME 正确解构元组,提取 regiondepartment
  3. 最终效果

    • 生成的 ExprState 用于 HashAggregate,为每行元组(如 ("North", "Sales", 100))计算组合哈希值,支持高效分组。

总结

  ExecComputeSlotInfoExecBuildHash32FromAttrs 的关键辅助函数,通过确定元组槽的固定性和类型,确保正确添加解构步骤(EEOP_INNER_FETCHSOME),为哈希计算准备列值。结合示例查询,ExecComputeSlotInfo 返回 true,触发解构步骤的添加,而 ExecBuildHash32FromAttrs 构建完整的 ExprState,包含提取、哈希和终止步骤,支持高效哈希表操作和 JIT 编译。流程图直观展示了 ExecBuildHash32FromAttrs 的逻辑,突出 ExecComputeSlotInfo 的作用。

为了帮助你理解 ExprEvalPushStep 函数的运行流程,我将结合 ExecBuildHash32FromAttrs 的上下文,通过之前的 SQL 查询示例,逐行解释 ExprEvalPushStep 的代码,展示其在哈希计算中如何向 ExprState 添加执行步骤。同时,我会为 ExecBuildHash32FromAttrs 提供一个补充的 Mermaid 流程图代码,进一步澄清其逻辑流程,确保与 ExprEvalPushStep 的调用关系清晰。ExprEvalPushStep 是 PostgreSQL 执行器中用于向 ExprState 的步骤数组添加新 ExprEvalStep 的辅助函数,广泛用于构建表达式执行计划。


ExprEvalPushStep

示例 SQL 查询(上下文)

  继续使用之前的查询:

SELECT region, department, SUM(amount)
FROM sales
GROUP BY region, department;
  • 表结构

    • sales 包含列:region (VARCHAR), department (VARCHAR), amount (INTEGER).
    • 示例数据:
      region  | department | amount
      --------+------------+-------
      North   | Sales      | 100
      South   | Marketing  | 200
      North   | Marketing  | 150
      
  • 查询目标:按 regiondepartment 分组,计算 amount 的总和。

  • 上下文ExecBuildHash32FromAttrs 调用 ExprEvalPushStep 将多个 ExprEvalStep(如 EEOP_INNER_FETCHSOME, EEOP_INNER_VAR, EEOP_HASHDATUM_FIRST, EEOP_HASHDATUM_NEXT32, EEOP_DONE)添加到 ExprState->steps,构建哈希计算的执行计划。


函数注释与解释

  以下是 PostgreSQLExprEvalPushStep 函数的每行代码,添加中文注释并结合 SQL 查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department 解释其作用。该函数用于向 ExprState 的步骤数组(steps)添加新的 ExprEvalStep,支持动态扩展数组,确保构建表达式执行计划(如哈希计算)的正确性。

/*
 * 向 ExprState->steps 添加一个新的表达式求值步骤。
 *
 * 注意:此操作可能重新分配 es->steps 数组,因此在构建表达式期间,
 * 不得使用指向该数组的指针。
 */
void
ExprEvalPushStep(ExprState *es, const ExprEvalStep *s)
{
    // 如果步骤数组未分配(初始为空)
	if (es->steps_alloc == 0)
	{
        // 初始化分配大小为 16
		es->steps_alloc = 16;
        // 分配内存,存储 16 个 ExprEvalStep
		es->steps = palloc(sizeof(ExprEvalStep) * es->steps_alloc);
        // 示例:在 ExecBuildHash32FromAttrs 中,首次调用时 es->steps_alloc = 0
        // 结果:es->steps_alloc = 16,es->steps 指向新分配的内存,可存 16 个步骤
	}
    // 如果步骤数组已满(当前长度等于分配大小)
	else if (es->steps_alloc == es->steps_len)
	{
        // 将分配大小加倍
		es->steps_alloc *= 2;
        // 重新分配内存,扩展为新的大小
		es->steps = repalloc(es->steps,
							 sizeof(ExprEvalStep) * es->steps_alloc);
        // 示例:当 steps_len = 16 时,扩展到 32
        // 结果:es->steps_alloc = 32,es->steps 指向扩展后的内存
	}

    // 将新的 ExprEvalStep 复制到 steps 数组的末尾,并增加长度
	memcpy(&es->steps[es->steps_len++], s, sizeof(ExprEvalStep));
    // 示例:添加 EEOP_INNER_FETCHSOME,es->steps_len 从 0 增到 1
    // 结果:es->steps[0] = s(EEOP_INNER_FETCHSOME),es->steps_len = 1
}

作用

  ExprEvalPushStepPostgreSQL 执行器中的辅助函数,用于ExprStatesteps 数组动态添加新的 ExprEvalStep,支持构建表达式执行计划(如哈希计算、投影)。
  它确保数组大小足够,并在需要时扩展内存,广泛应用于 ExecBuildHash32FromAttrs 等函数中,添加步骤如 EEOP_INNER_FETCHSOME, EEOP_INNER_VAR, EEOP_HASHDATUM_FIRST, EEOP_HASHDATUM_NEXT32, EEOP_DONE

  • 动态分配:通过 pallocrepalloc 管理 steps 数组,初始分配 16 个步骤,数组满时加倍扩展。
  • 内存安全:注释警告在构建期间不得使用 steps 数组的指针,因为 repalloc 可能改变数组地址。
  • 上下文:在哈希计算中,ExecBuildHash32FromAttrs 调用 ExprEvalPushStep 多次,构建完整的哈希计算计划。

参数

  • esExprState *表达式执行计划,包含 steps(步骤数组)、steps_len(当前长度)、steps_alloc(分配大小)。
  • sconst ExprEvalStep *要添加的步骤,包含 opcode(如 EEOP_INNER_FETCHSOME)和相关数据(如 fetch, var, hashdatum)。

返回值

  • void无返回值,直接修改 es->steps 数组。

执行流程(结合示例)

  假设 ExecBuildHash32FromAttrsregiondepartment 构建哈希计算的 ExprState,调用 ExprEvalPushStep 添加步骤:

  1. 初始化检查

    • 检查 es->steps_alloc 是否为 0。
    • 示例:首次调用时,es->steps_alloc = 0,分配 16 个步骤的内存,es->steps 指向新内存,es->steps_alloc = 16
  2. 扩展检查

    • 如果 es->steps_len == es->steps_alloc,扩展数组。
    • 示例steps_len = 0steps_alloc = 16,无需扩展。后续若 steps_len = 16,则扩展到 steps_alloc = 32
  3. 添加步骤

    • 复制 ses->steps[es->steps_len]steps_len 增 1。
    • 示例:添加 EEOP_INNER_FETCHSOMEes->steps[0] = ssteps_len = 1
  4. 多次调用

    • ExecBuildHash32FromAttrs 调用多次,添加 [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_HASHDATUM_FIRST, EEOP_INNER_VAR {department}, EEOP_HASHDATUM_NEXT32, EEOP_DONE]
    • 示例:每次调用添加一个步骤,steps_len 从 0 增到 6,steps 包含完整计划。

示例调用

  • 首次调用(添加 EEOP_INNER_FETCHSOME):
    • 输入:es->steps_alloc = 0, es->steps_len = 0, s = {opcode = EEOP_INNER_FETCHSOME, last_var = 2, kind = TTSOpsMinimalTuple}
    • 执行:分配 es->steps16 个步骤),复制 ses->steps[0]es->steps_len = 1
  • 后续调用(如添加 EEOP_INNER_VAR):
    • 输入:es->steps_alloc = 16, es->steps_len = 1, s = {opcode = EEOP_INNER_VAR, attnum = 0}
    • 执行:复制 ses->steps[1]es->steps_len = 2
  • 结果:最终 es->steps 包含 6 个步骤,steps_len = 6

功能结构流程图(补充)

  以下是 ExecBuildHash32FromAttrsMermaid 流程图代码,结合 ExprEvalPushStepExecComputeSlotInfo 的调用,展示其完整功能结构。流程图采用纵向布局(TD),使用简洁中文描述,节点为矩形(操作)或菱形(条件),颜色区分不同阶段,清晰展示构建 ExprState 的过程,包括 ExprEvalPushStep 的作用。

开始: ExecBuildHash32FromAttrs
初始化 ExprState 和 scratch
设置 parent 和 last_attnum
numCols + init_value > 1?
分配中间结果 iresult
调用 ExecComputeSlotInfo
添加 EEOP_INNER_FETCHSOME
使用 ExprEvalPushStep
init_value == 0?
设置 opcode = EEOP_HASHDATUM_FIRST
添加 EEOP_HASHDATUM_SET_INITVAL
使用 ExprEvalPushStep
opcode = EEOP_HASHDATUM_NEXT32
遍历每列 keyColIdx
设置 EEOP_INNER_VAR
提取列值到 fcinfo->args
最后一列?
设置结果到 state->resvalue
设置结果到 iresult
添加哈希步骤
EEOP_HASHDATUM_FIRST/NEXT32
使用 ExprEvalPushStep
更新 opcode = EEOP_HASHDATUM_NEXT32
更多列?
添加 EEOP_DONE
使用 ExprEvalPushStep
调用 ExecReadyExpr
返回 ExprState
结束

流程图说明

  • 布局:纵向(TD),从上到下展示 ExecBuildHash32FromAttrs 的逻辑流程,清晰简洁。
  • 节点
    • 矩形表示操作(如初始化、提取列值),菱形表示条件(如 numCols + init_value > 1)。
    • 节点分组:初始化(init,绿色)、条件(condition,黄色)、循环(loop,浅红)、提取/设置(extract/setup,绿色)、存储/哈希(store/hash,蓝色)、结束(finish,灰色)。
  • 逻辑
    • 初始化 ExprStatescratch,设置 parentlast_attnum
    • 检查是否需要中间结果(iresult)。
    • 调用 ExecComputeSlotInfo,根据返回结果添加 EEOP_INNER_FETCHSOME(通过 ExprEvalPushStep)。
    • 根据 init_value 设置初始哈希操作(EEOP_HASHDATUM_FIRSTSET_INITVAL)。
    • 循环每列,添加 EEOP_INNER_VAREEOP_HASHDATUM_FIRST/NEXT32(通过 ExprEvalPushStep)。
      • 添加 EEOP_DONE(通过 ExprEvalPushStep),调用 ExecReadyExpr,返回 ExprState
  • 与 ExprEvalPushStep 的关联
    • 节点 F(EEOP_INNER_FETCHSOME)、I(EEOP_HASHDATUM_SET_INITVAL)、O(EEOP_HASHDATUM_FIRST/NEXT32)、R(EEOP_DONE)调用 ExprEvalPushStep 添加步骤。
    • 示例中,ExprEvalPushStep 被调用 6 次,构建 [EEOP_INNER_FETCHSOME, EEOP_INNER_VAR {region}, EEOP_HASHDATUM_FIRST, EEOP_INNER_VAR {department}, EEOP_HASHDATUM_NEXT32, EEOP_DONE]

示例执行总结

  对于查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department

  1. ExecBuildHash32FromAttrs 调用

    • 输入:desc = sales 表 TupleDesc, ops = TTSOpsMinimalTuple, hashfunctions = [hash_any, hash_any], collations = [C, C], numCols = 2, keyColIdx = [1, 2], parent = HashAggregate, init_value = 0
    • 过程:
      • 初始化 ExprStatees->steps_alloc = 0, steps_len = 0
      • 调用 ExecComputeSlotInfo,返回 true,触发 ExprEvalPushStep 添加 EEOP_INNER_FETCHSOMEes->steps[0], steps_len = 1)。
      • region 添加 EEOP_INNER_VAREEOP_HASHDATUM_FIRSTsteps_len = 3)。
      • department 添加 EEOP_INNER_VAREEOP_HASHDATUM_NEXT32steps_len = 5)。
      • 添加 EEOP_DONEsteps_len = 6)。
      • 调用 ExecReadyExpr,设置 es->evalfunc(如 ExecJustHashVarImpl)。
    • 输出:ExprState 包含 6 个步骤。
  2. ExprEvalPushStep 作用

    • 每次调用动态扩展 es->steps(初始分配 16,必要时扩展到 32)。
    • 示例:添加 EEOP_INNER_FETCHSOME 时,es->steps[0] 存储步骤,steps_len 从 0 增到 1;后续步骤类似。
  3. 最终效果

    • 生成的 ExprState 用于 HashAggregate,为每行元组(如 ("North", "Sales", 100))计算组合哈希值,步骤序列高效执行,支持 JIT 编译。

总结

  ExprEvalPushStepExecBuildHash32FromAttrs 的关键辅助函数,通过动态分配和扩展 ExprState->steps 数组,添加哈希计算的执行步骤(如 EEOP_INNER_FETCHSOME, EEOP_INNER_VAR, EEOP_HASHDATUM_*)。结合示例查询,ExprEvalPushStep 被调用 6 次,构建完整的哈希计算计划。流程图直观展示了 ExecBuildHash32FromAttrs 的逻辑,突出 ExprEvalPushStep 在添加步骤中的作用,确保计划构建的内存安全和高效性。


网站公告

今日签到

点亮在社区的每一天
去签到