PostgreSQL内核学习:通过 ExprState 提升哈希聚合与子计划执行效率(二)
声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 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
查询:
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
构建哈希计算的执行计划。
/*
* 构建一个 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 查询
与之前一致,使用以下查询作为上下文:
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
)中确保正确提取列值。
/*
* 计算 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
的过程。
流程图说明
- 布局:纵向(
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 查询(上下文)
继续使用之前的查询:
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
,支持动态扩展数组,确保构建表达式执行计划(如哈希计算)的正确性。
/*
* 向 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
的作用。
流程图说明
- 布局:纵向(
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
在添加步骤中的作用,确保计划构建的内存安全和高效性。