PostgreSQL内核学习:通过 ExprState 提升哈希聚合与子计划执行效率(二)
- 引言
- ExecBuildHash32FromAttrs
-
- [示例 SQL 查询](#示例 SQL 查询)
- 函数运行流程与代码解释
- [最终 ExprState 结构](#最终 ExprState 结构)
- 执行示例
- 总结
- ExecComputeSlotInfo
- ExprEvalPushStep
-
- [示例 SQL 查询(上下文)](#示例 SQL 查询(上下文))
- 函数注释与解释
- 功能结构流程图(补充)
- 示例执行总结
- 总结
声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书
引言
为了更清晰地理解 ExecBuildHash32FromAttrs 函数的运行流程,我将通过一个详细的 SQL 查询示例,结合每行代码的执行,逐步说明该函数如何构建哈希计算的 ExprState。
ExecBuildHash32FromAttrs 是 PostgreSQL 中用于为指定列构建哈希计算执行计划的核心函数,常用于 GROUP BY 或 NOT IN 子查询的哈希操作,特别是在优化补丁("Use ExprStates for hashing in GROUP BY and SubPlans")中。本例将使用一个具体的 SQL 查询,覆盖每行代码的解释,并以清晰的方式展示函数的逻辑流程。
其他详细描述可见: 【PostgreSQL内核学习:通过 ExprState 提升哈希聚合与子计划执行效率(一)】
ExecBuildHash32FromAttrs
示例 SQL 查询
假设有以下 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
-
-
查询目标 :按
region和department分组,计算amount的总和。 -
哈希计算 :需要为
region和department列生成组合哈希值,用于哈希表分组。
在执行计划中,PostgreSQL 会调用 ExecBuildHash32FromAttrs 为 GROUP BY 的列(region 和 department)构建哈希计算的 ExprState,以高效计算哈希值并支持 JIT 编译。
函数运行流程与代码解释
以下是 ExecBuildHash32FromAttrs 的代码,结合示例查询,逐行解释其执行过程,展示如何为 region 和 department 构建哈希计算的执行计划。
c
/*
* 构建一个 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 包含以下步骤序列:
-
EEOP_INNER_FETCHSOME:
last_var = 2(提取到department列)。fixed = false,kind = TTSOpsMinimalTuple,known_desc = sales 表的 TupleDesc。- 作用:解构输入元组,准备提取
region和department。
-
EEOP_INNER_VAR (region):
attnum = 0,vartype = VARCHAR。resvalue = fcinfo[0]->args[0].value,resnull = fcinfo[0]->args[0].isnull。- 作用:提取
region列值,存储到第一个哈希函数的参数。
-
EEOP_HASHDATUM_FIRST:
finfo = hash_any,fcinfo_data = hash_any 的调用信息,fn_addr = hash_any。resvalue = iresult->value,resnull = iresult->isnull。- 作用:调用
hash_any计算region的哈希值,存储到中间结果。
-
EEOP_INNER_VAR (department):
attnum = 1,vartype = VARCHAR。resvalue = fcinfo[1]->args[0].value,resnull = fcinfo[1]->args[0].isnull。- 作用:提取
department列值,存储到第二个哈希函数的参数。
-
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的哈希值,与中间结果组合,存储最终哈希值。
-
EEOP_DONE:
- 作用:终止执行计划。
执行示例
假设处理元组 (region = "North", department = "Sales", amount = 100):
-
调用:
ExecBuildHash32FromAttrs(desc, TTSOpsMinimalTuple, [hash_any, hash_any], [C, C], 2, [1, 2], HashAggregate, 0)。- 输入:
desc是sales表的描述符,keyColIdx = [1, 2](region和department),init_value = 0。
-
执行步骤:
- EEOP_INNER_FETCHSOME :解构元组,提取
region和department。 - 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)。
- EEOP_INNER_FETCHSOME :解构元组,提取
-
结果:
- 返回的
ExprState被HashAggregate节点使用,哈希值0x7890abcd用于哈希表插入或查找。
- 返回的
总结
ExecBuildHash32FromAttrs 通过构建 ExprState 和一系列 ExprEvalStep,为 GROUP BY 的列(region 和 department)生成高效的哈希计算计划。每个步骤(提取元组、获取列值、调用哈希函数)被精确配置,减少函数调用开销,支持 JIT 编译。示例查询展示了如何从输入参数到最终 ExprState 的构建,覆盖了每行代码的作用,最终生成一个包含提取、哈希和终止步骤的执行计划,用于高效哈希表操作。
ExecComputeSlotInfo
ExecComputeSlotInfo 是 PostgreSQL 执行器中用于确定元组槽(TupleTableSlot)是否为固定格式的辅助函数,决定是否需要添加解构步骤(EEOP_*_FETCHSOME),常用于哈希计算等场景。以下内容分为两部分:ExecComputeSlotInfo 的代码注释与解释,以及 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
-
-
查询目标 :按
region和department分组,计算amount的总和。 -
上下文 :
ExecBuildHash32FromAttrs调用ExecComputeSlotInfo来确定是否需要添加EEOP_INNER_FETCHSOME步骤,用于提取region和department的值以进行哈希计算。
函数注释与解释
以下是 PostgreSQL 中 ExecComputeSlotInfo 函数的每行代码,添加中文注释并结合 SQL 查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department 解释其作用。
该函数用于确定元组槽(TupleTableSlot)是否为固定格式(fixed),以决定是否需要添加解构步骤(EEOP_*_FETCHSOME),在哈希计算(如 ExecBuildHash32FromAttrs)中确保正确提取列值。
c
/*
* 计算 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 步骤
}
作用
ExecComputeSlotInfo 是 PostgreSQL 执行器中的辅助函数,用于为 EEOP_*_FETCHSOME 操作(提取元组列值的步骤)计算元组槽的附加信息,确定槽是否为"固定"格式(即每次求值时槽类型和描述符一致)。它在 ExecBuildHash32FromAttrs 中被调用,决定是否需要添加 EEOP_INNER_FETCHSOME 步骤来解构元组(如提取 region 和 department 的值)。其核心目标是:
- 确定槽类型 :检查槽是否为固定格式(
fixed),并获取槽操作类型(tts_ops)和描述符(desc)。 - 优化性能 :如果槽为虚拟槽(
TTSOpsVirtual),无需解构,返回false,减少步骤;否则返回true,添加解构步骤。
参数
state:ExprState *,表达式执行计划,包含父节点(parent)信息。op:ExprEvalStep *,当前操作步骤,包含opcode(如EEOP_INNER_FETCHSOME)和fetch子结构(known_desc,kind,fixed)。
返回值
bool:true表示需要解构步骤(添加EEOP_*_FETCHSOME),false表示无需解构。
执行流程(结合示例)
-
初始化:
- 获取
parent(HashAggregate节点),opcode(EEOP_INNER_FETCHSOME),初始化desc、tts_ops和isfixed。 - 示例:
parent是HashAggregate,opcode = EEOP_INNER_FETCHSOME。
- 获取
-
检查已知描述符:
- 如果
op->d.fetch.known_desc不为空,直接使用已知的desc和tts_ops,设置isfixed。 - 示例:
known_desc = sales 表 TupleDesc,kind = TTSOpsMinimalTuple,isfixed = true。
- 如果
-
处理内层计划(
EEOP_INNER_FETCHSOME):- 获取内层计划(
SeqScan节点),检查parent->inneropsset和inneropsfixed。 - 示例:
innerops = TTSOpsMinimalTuple,inneropsfixed = true,desc = sales 表 TupleDesc。
- 获取内层计划(
-
设置槽信息:
- 如果
isfixed = true且desc和tts_ops不为空,设置op->d.fetch.fixed = true,kind = TTSOpsMinimalTuple,known_desc = sales 表 TupleDesc。 - 示例:设置
op->d.fetch为固定格式,kind = TTSOpsMinimalTuple。
- 如果
-
检查虚拟槽:
- 如果槽固定且为
TTSOpsVirtual,返回false(无需解构)。 - 示例:
kind = TTSOpsMinimalTuple,非虚拟槽,返回true。
- 如果槽固定且为
-
返回结果:
- 示例:返回
true,表示需要添加EEOP_INNER_FETCHSOME步骤。
- 示例:返回
与 ExecBuildHash32FromAttrs 的关系
在 ExecBuildHash32FromAttrs,ExecComputeSlotInfo 被调用以确定是否需要 EEOP_INNER_FETCHSOME 步骤:
- 输入 :
state是新建的ExprState,op是scratch(opcode = EEOP_INNER_FETCHSOME,last_var = 2,kind = TTSOpsMinimalTuple,known_desc = sales 表 TupleDesc)。 - 输出 :返回
true,触发ExprEvalPushStep(state, &scratch),添加解构步骤。 - 作用 :确保
region和department的值被正确提取,供后续哈希计算使用。
功能结构流程图
以下是 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,灰色)。
- 矩形表示操作(如初始化、提取列值),菱形表示条件(如
- 逻辑 :
- 初始化
ExprState和scratch,设置parent和last_attnum。 - 检查是否需要中间结果(
iresult)。 - 调用
ExecComputeSlotInfo添加EEOP_INNER_FETCHSOME。 - 根据
init_value设置初始哈希操作(EEOP_HASHDATUM_FIRST或SET_INITVAL)。 - 循环每列,添加
EEOP_INNER_VAR和EEOP_HASHDATUM_FIRST/NEXT32,存储结果到iresult或state->resvalue。 - 添加
EEOP_DONE,调用ExecReadyExpr,返回ExprState。
- 初始化
- 与 ExecComputeSlotInfo 的关联 :
- 在节点 F(
EEOP_INNER_FETCHSOME),调用ExecComputeSlotInfo确定槽格式,决定是否添加解构步骤。 - 示例中,
ExecComputeSlotInfo返回true,触发添加EEOP_INNER_FETCHSOME {last_var=2, kind=TTSOpsMinimalTuple}。
- 在节点 F(
示例执行总结
对于查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department:
-
ExecBuildHash32FromAttrs 调用:
- 输入:
desc = sales 表 TupleDesc,ops = TTSOpsMinimalTuple,hashfunctions = [hash_any, hash_any],collations = [C, C],numCols = 2,keyColIdx = [1, 2],parent = HashAggregate,init_value = 0。 - 过程:
- 初始化
ExprState,last_attnum = 2。 - 调用
ExecComputeSlotInfo,确认槽为固定格式(TTSOpsMinimalTuple),返回true,添加EEOP_INNER_FETCHSOME。 - 为
region添加EEOP_INNER_VAR和EEOP_HASHDATUM_FIRST(结果存iresult)。 - 为
department添加EEOP_INNER_VAR和EEOP_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]。
- 输入:
-
ExecComputeSlotInfo 作用:
- 确认槽为
TTSOpsMinimalTuple,固定格式,返回true,确保EEOP_INNER_FETCHSOME正确解构元组,提取region和department。
- 确认槽为
-
最终效果:
- 生成的
ExprState用于HashAggregate,为每行元组(如("North", "Sales", 100))计算组合哈希值,支持高效分组。
- 生成的
总结
ExecComputeSlotInfo 是 ExecBuildHash32FromAttrs 的关键辅助函数,通过确定元组槽的固定性和类型,确保正确添加解构步骤(EEOP_INNER_FETCHSOME),为哈希计算准备列值。结合示例查询,ExecComputeSlotInfo 返回 true,触发解构步骤的添加,而 ExecBuildHash32FromAttrs 构建完整的 ExprState,包含提取、哈希和终止步骤,支持高效哈希表操作和 JIT 编译。流程图直观展示了 ExecBuildHash32FromAttrs 的逻辑,突出 ExecComputeSlotInfo 的作用。
为了帮助你理解 ExprEvalPushStep 函数的运行流程,我将结合 ExecBuildHash32FromAttrs 的上下文,通过之前的 SQL 查询示例,逐行解释 ExprEvalPushStep 的代码,展示其在哈希计算中如何向 ExprState 添加执行步骤。同时,我会为 ExecBuildHash32FromAttrs 提供一个补充的 Mermaid 流程图代码,进一步澄清其逻辑流程,确保与 ExprEvalPushStep 的调用关系清晰。ExprEvalPushStep 是 PostgreSQL 执行器中用于向 ExprState 的步骤数组添加新 ExprEvalStep 的辅助函数,广泛用于构建表达式执行计划。
ExprEvalPushStep
示例 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
-
-
查询目标 :按
region和department分组,计算amount的总和。 -
上下文 :
ExecBuildHash32FromAttrs调用ExprEvalPushStep将多个ExprEvalStep(如EEOP_INNER_FETCHSOME,EEOP_INNER_VAR,EEOP_HASHDATUM_FIRST,EEOP_HASHDATUM_NEXT32,EEOP_DONE)添加到ExprState->steps,构建哈希计算的执行计划。
函数注释与解释
以下是 PostgreSQL 中 ExprEvalPushStep 函数的每行代码,添加中文注释并结合 SQL 查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department 解释其作用。该函数用于向 ExprState 的步骤数组(steps)添加新的 ExprEvalStep,支持动态扩展数组,确保构建表达式执行计划(如哈希计算)的正确性。
c
/*
* 向 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
}
作用
ExprEvalPushStep 是 PostgreSQL 执行器中的辅助函数 ,用于向 ExprState 的 steps 数组动态添加新的 ExprEvalStep,支持构建表达式执行计划(如哈希计算、投影)。
它确保数组大小足够,并在需要时扩展内存,广泛应用于 ExecBuildHash32FromAttrs 等函数中,添加步骤如 EEOP_INNER_FETCHSOME, EEOP_INNER_VAR, EEOP_HASHDATUM_FIRST, EEOP_HASHDATUM_NEXT32, EEOP_DONE。
- 动态分配 :通过
palloc和repalloc管理steps数组,初始分配 16 个步骤,数组满时加倍扩展。 - 内存安全 :注释警告在构建期间不得使用
steps数组的指针,因为repalloc可能改变数组地址。 - 上下文 :在哈希计算中,
ExecBuildHash32FromAttrs调用ExprEvalPushStep多次,构建完整的哈希计算计划。
参数
es:ExprState *,表达式执行计划 ,包含steps(步骤数组)、steps_len(当前长度)、steps_alloc(分配大小)。s:const ExprEvalStep *,要添加的步骤 ,包含opcode(如EEOP_INNER_FETCHSOME)和相关数据(如fetch,var,hashdatum)。
返回值
void:无返回值 ,直接修改es->steps数组。
执行流程(结合示例)
假设 ExecBuildHash32FromAttrs 为 region 和 department 构建哈希计算的 ExprState,调用 ExprEvalPushStep 添加步骤:
-
初始化检查:
- 检查
es->steps_alloc是否为 0。 - 示例 :首次调用时,
es->steps_alloc = 0,分配 16 个步骤的内存,es->steps指向新内存,es->steps_alloc = 16。
- 检查
-
扩展检查:
- 如果
es->steps_len == es->steps_alloc,扩展数组。 - 示例 :
steps_len = 0,steps_alloc = 16,无需扩展。后续若steps_len = 16,则扩展到steps_alloc = 32。
- 如果
-
添加步骤:
- 复制
s到es->steps[es->steps_len],steps_len增 1。 - 示例 :添加
EEOP_INNER_FETCHSOME,es->steps[0] = s,steps_len = 1。
- 复制
-
多次调用:
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->steps(16个步骤),复制s到es->steps[0],es->steps_len = 1。
- 输入:
- 后续调用 (如添加
EEOP_INNER_VAR):- 输入:
es->steps_alloc = 16,es->steps_len = 1,s = {opcode = EEOP_INNER_VAR, attnum = 0}。 - 执行:复制
s到es->steps[1],es->steps_len = 2。
- 输入:
- 结果 :最终
es->steps包含6个步骤,steps_len = 6。
功能结构流程图(补充)
以下是 ExecBuildHash32FromAttrs 的 Mermaid 流程图代码,结合 ExprEvalPushStep 和 ExecComputeSlotInfo 的调用,展示其完整功能结构。流程图采用纵向布局(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,灰色)。
- 矩形表示操作(如初始化、提取列值),菱形表示条件(如
- 逻辑 :
- 初始化
ExprState和scratch,设置parent和last_attnum。 - 检查是否需要中间结果(
iresult)。 - 调用
ExecComputeSlotInfo,根据返回结果添加EEOP_INNER_FETCHSOME(通过ExprEvalPushStep)。 - 根据
init_value设置初始哈希操作(EEOP_HASHDATUM_FIRST或SET_INITVAL)。 - 循环每列,添加
EEOP_INNER_VAR和EEOP_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]。
- 节点 F(
示例执行总结
对于查询 SELECT region, department, SUM(amount) FROM sales GROUP BY region, department:
-
ExecBuildHash32FromAttrs 调用:
- 输入:
desc = sales 表 TupleDesc,ops = TTSOpsMinimalTuple,hashfunctions = [hash_any, hash_any],collations = [C, C],numCols = 2,keyColIdx = [1, 2],parent = HashAggregate,init_value = 0。 - 过程:
- 初始化
ExprState,es->steps_alloc = 0,steps_len = 0。 - 调用
ExecComputeSlotInfo,返回true,触发ExprEvalPushStep添加EEOP_INNER_FETCHSOME(es->steps[0],steps_len = 1)。 - 为
region添加EEOP_INNER_VAR和EEOP_HASHDATUM_FIRST(steps_len = 3)。 - 为
department添加EEOP_INNER_VAR和EEOP_HASHDATUM_NEXT32(steps_len = 5)。 - 添加
EEOP_DONE(steps_len = 6)。 - 调用
ExecReadyExpr,设置es->evalfunc(如ExecJustHashVarImpl)。
- 初始化
- 输出:
ExprState包含 6 个步骤。
- 输入:
-
ExprEvalPushStep 作用:
- 每次调用动态扩展
es->steps(初始分配16,必要时扩展到32)。 - 示例:添加
EEOP_INNER_FETCHSOME时,es->steps[0]存储步骤,steps_len从 0 增到 1;后续步骤类似。
- 每次调用动态扩展
-
最终效果:
- 生成的
ExprState用于HashAggregate,为每行元组(如("North", "Sales", 100))计算组合哈希值,步骤序列高效执行,支持 JIT 编译。
- 生成的
总结
ExprEvalPushStep 是 ExecBuildHash32FromAttrs 的关键辅助函数,通过动态分配和扩展 ExprState->steps 数组,添加哈希计算的执行步骤(如 EEOP_INNER_FETCHSOME, EEOP_INNER_VAR, EEOP_HASHDATUM_*)。结合示例查询,ExprEvalPushStep 被调用 6 次,构建完整的哈希计算计划。流程图直观展示了 ExecBuildHash32FromAttrs 的逻辑,突出 ExprEvalPushStep 在添加步骤中的作用,确保计划构建的内存安全和高效性。