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

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

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

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

引言

背景

PostgreSQL 是一种功能强大的开源关系型数据库管理系统,广泛应用于企业级应用和数据分析场景。在 PostgreSQL 的查询执行引擎中,哈希操作是许多查询计划的核心组件,例如 GROUP BYNOT IN 子查询中的哈希聚合Hash Aggregate)和哈希表查找 。这些操作依赖于高效的哈希计算和哈希表管理来实现快速的数据分组和查找。

然而,传统的哈希计算实现(如在 execGrouping.cnodeSubplan.c 中)存在性能瓶颈,尤其是在处理大量数据或复杂查询时。哈希计算通常涉及逐列调用哈希函数FmgrInfo),并通过位运算组合哈希值 ,这种方式会产生较多的函数调用和上下文切换开销。此外,传统的实现无法充分利用 PostgreSQLJIT(即时编译)功能,限制了性能优化空间。

为了解决这些问题,PostgreSQL 社区引入了基于 ExprState表达式状态 )的哈希计算优化补丁。该补丁通过重构哈希计算逻辑 ,利用 ExprState表达式执行框架减少函数调用开销 ,并支持 JIT 编译,从而显著提升哈希聚合和子查询的性能。此外,补丁改进了哈希值的扰动方式,减少哈希冲突,进一步优化哈希表操作。

补丁的意义

此补丁(提交标题为"Use ExprStates for hashing in GROUP BY and SubPlans")是 PostgreSQL 性能优化的重要一步。它不仅提升了特定查询场景(如 GROUP BYNOT IN 子查询)的执行效率,还为未来的 JIT 编译扩展奠定了基础。通过将哈希计算逻辑封装为 ExprState,补丁使得 PostgreSQL 的执行器能够更高效地处理复杂表达式,并为开发者提供了更灵活的优化空间。

补丁概述

JIT & LLVM

以下是针对JITLLVM定义的解释,帮助读者理解技术背景:

  1. JIT 编译(Just-In-Time Compilation):

    • JIT 编译是一种在程序运行时将代码(通常是中间表示或字节码)编译为本地机器码的技术,以提高执行效率。在 PostgreSQL 中,JIT 编译基于 LLVM(低级虚拟机)框架,用于优化查询执行计划中的表达式计算和哈希操作。
    • 传统的解释执行需要逐条解析和执行指令 ,而 JIT 编译将这些指令提前转换为机器码,减少运行时开销,特别适用于高频重复执行 的操作,如哈希计算
    • 参考:使用LLVM与JIT技术提升数据仓库性能的新探索什么时候会用JIT?
  2. LLVM (Low Level Virtual Machine)

    • LLVM 是一个 编译框架,提供了中间表示(IR)和优化、生成机器码的工具。
      • 数据库自己写一个 JIT 编译器成本太高,所以直接拿 LLVM 做后端:
      • 数据库把 SQL 表达式翻译成 LLVM IR
      • LLVM 负责优化并生成机器码。
      • 数据库调用这些机器码函数,代替原来的解释器逻辑。

实际例子(以 PostgreSQL 为例)

假设有个 SQL

sql 复制代码
SELECT sum(salary * 1.2 + bonus) 
FROM empsalary 
WHERE deptno = 10;

没有 JIT 的情况

  • PostgreSQL 执行 salary * 1.2 + bonus 时,会走解释器模式:
    • 取出元组的 salarybonus
    • 调用 ExecEvalFunc 执行乘法函数
    • 再调用加法函数
    • 逐行循环执行
    • 整个过程函数调用层级很多

JIT + LLVM 的情况

  • PostgreSQL 在执行计划时,发现表达式复杂度够高 ,就调用 LLVM JIT
  • 它会生成类似这样的 LLVM IR(简化过的):
bash 复制代码
define double @expr(double %salary, double %bonus) {
entry:
    %mul = fmul double %salary, 1.2
    %add = fadd double %mul, %bonus
    ret double %add
}
  • LLVM 把这段 IR 编译成机器码(比如 x86mulss, addss 指令)。
  • 查询执行时,每行数据直接调用这个机器码函数计算 → 避开了解释层 → 性能提升。

如果表够大,你会看到执行计划里写着:

sql 复制代码
JIT:
  Functions: 3
  Options: Inlining true, Optimization true, Expressions true, Deforming true

提交信息

下面为本次优化的提交信息,hash 值为:0f5738202b812a976e8612c85399b52d16a0abb6。对应的描述信息如下所示:

bash 复制代码
commit 0f5738202b812a976e8612c85399b52d16a0abb6
Author: David Rowley <drowley@postgresql.org>
Date:   Wed Dec 11 13:47:16 2024 +1300

    Use ExprStates for hashing in GROUP BY and SubPlans

    This speeds up obtaining hash values for GROUP BY and hashed SubPlans by
    using the ExprState support for hashing, thus allowing JIT compilation for
    obtaining hash values for these operations.

    This, even without JIT compilation, has been shown to improve Hash
    Aggregate performance in some cases by around 15% and hashed NOT IN
    queries in one case by over 30%, however, real-world cases are likely to
    see smaller gains as the test cases used were purposefully designed to
    have high hashing overheads by keeping the hash table small to prevent
    additional memory overheads that would be a factor when working with large
    hash tables.

    In passing, fix a hypothetical bug in ExecBuildHash32Expr() so that the
    initial value is stored directly in the ExprState's result field if
    there are no expressions to hash.  None of the current users of this
    function use an initial value, so the bug is only hypothetical.

    Reviewed-by: Andrei Lepikhov <lepihov@gmail.com>
    Discussion: https://postgr.es/m/CAApHDvpYSO3kc9UryMevWqthTBrxgfd9djiAjKHMPUSQeX9vdQ@mail.gmail.com

提交描述

该补丁通过以下方式优化了 PostgreSQL 的哈希计算性能:

引入 ExprState 进行哈希计算:

  • 新增了 ExecBuildHash32FromAttrs 函数,用于构建一个 ExprState,以计算 GROUP BY哈希子查询hashed SubPlans)中涉及的哈希值。
  • ExprState 将哈希计算分解为一系列可优化的操作步骤(ExprEvalStep),例如提取列值EEOP_INNER_VAR)、调用哈希函数EEOP_HASHDATUM_FIRSTEEOP_HASHDATUM_NEXT32)和组合哈希值。
  • 传统的哈希计算方式直接调用 FmgrInfo哈希函数逐列计算并组合哈希值 。新方法通过 ExprState 一次性执行所有列的哈希计算,减少了函数调用开销。

支持 JIT 编译:

  • ExprState 的结构与 PostgreSQLJIT 编译器(基于 LLVM)兼容,允许将哈希计算逻辑编译为本地机器码,从而减少解释执行的开销。
  • 即使未启用 JIT 编译,ExprState 的优化仍然能带来性能提升,因为它通过更高效的内存管理和指令调度减少了函数调用的次数。

改进哈希值扰动:

  • 传统方法通过位旋转pg_rotate_left32)和异或操作 组合哈希值,可能导致哈希值分布不够均匀增加冲突概率
  • 新方法在组合哈希值后,使用 murmurhash32 进行额外的哈希扰动,生成更均匀的哈希值,从而减少哈希冲突,提高哈希表效率。

代码结构调整:

  • execGrouping.c 中,TupleHashTableData 结构中的 tab_hash_funcsin_hash_funcs 被替换为 tab_hash_exprin_hash_expr,以使用 ExprState 表示哈希计算逻辑。
  • nodeSubplan.c 中,SubPlanState 结构中的 lhs_hash_funcs 被替换为 lhs_hash_expr支持子查询的哈希计算优化
  • 修改了 FindTupleHashEntry 函数,支持跨类型比较,确保不同数据类型的哈希和比较操作正确执行。

优化目的

这个补丁通过优化 PostgreSQLGROUP BYNOT IN 子查询的哈希计算过程,显著提升了查询性能。传统方法在计算哈希值时,逐列调用哈希函数导致大量函数调用和上下文切换开销 ,尤其在数据量大时效率低下。

补丁引入了 ExprState 机制,将哈希计算整合为一系列高效的执行步骤,减少了重复的函数调用,并支持即时编译(JIT),将计算逻辑转化为机器码以进一步加速处理。这种方式让哈希计算更加流畅,降低了性能瓶颈。

同时,补丁改进了哈希值的生成方式,通过 murmurhash32 算法对哈希值进行扰动 ,生成更均匀的分布 ,从而减少哈希表中的冲突。这提高了哈希表操作的效率,特别是在哈希表较小的场景下。此外,补丁修复了一个潜在问题,确保在没有需要哈希的表达式时,初始哈希值能正确存储,增强了代码的可靠性和扩展性。

例如,对于查询 SELECT region, SUM(amount) FROM sales GROUP BY region,优化前处理 1000 万行可能需要 100 秒,因为每行都需要多次调用哈希函数 。优化后,ExprStateJIT 编译将计算过程简化为高效的机器码 ,时间可能缩短到 85 秒。对于 NOT IN 子查询,如 SELECT product_id FROM products WHERE product_id NOT IN (SELECT product_id FROM sales),性能提升可能更明显,时间从 100 秒减少到 70 秒。这些改进解决了哈希计算开销大、冲突频繁和缺乏 JIT 支持的问题,为复杂查询提供了更高效的执行方式,同时为未来优化奠定了基础。

源码解读

定义


  1. ExprState(表达式状态):
    • ExprStatePostgreSQL 执行器中用于表示表达式执行计划的数据结构。它将复杂的 SQL 表达式分解为一系列操作步骤(ExprEvalStep),例如提取列值调用函数组合结果
    • ExprState 支持高效的表达式计算 ,并与 JIT 编译器兼容,允许将表达式逻辑编译为机器码,从而减少解释执行的开销。

  1. FmgrInfo(函数管理信息)
    • FmgrInfoPostgreSQL 中用于存储函数调用信息的结构,包含函数的地址参数数量调用方式 等信息。在传统的哈希计算中,FmgrInfo 用于调用每个列的哈希函数(如 hash_any 用于字符串)。
    • 每次调用 FmgrInfo 的函数都会产生一定的开销,尤其是在处理多列或大量数据时。

  1. murmurhash32
    • murmurhash32 是一种高效的非加密哈希算法 ,用于生成均匀分布的 32 位哈希值。它在 PostgreSQL 中用于扰动(perturbation)哈希值,以减少哈希冲突
    • 相比简单的位旋转和异或操作murmurhash32 能生成更均匀的哈希值,从而提高哈希表的性能。

  1. 哈希聚合(Hash Aggregate
    • 哈希聚合是 PostgreSQL 中处理 GROUP BY 查询的一种执行策略。它通过构建一个哈希表,将数据按照分组键(group key)存储,并在表中累积聚合结果(如 SUMCOUNT)。
    • 哈希聚合的性能依赖于哈希计算的效率哈希表的冲突率

  1. 哈希子查询(Hashed SubPlan
    • 哈希子查询是指在子查询(如 NOT ININ)中使用哈希表来加速查找的执行策略。PostgreSQL 将子查询的结果存储在哈希表中,然后对主查询的每一行进行哈希查找以检查匹配。
    • 哈希子查询的性能同样受哈希计算效率哈希表冲突的影响。

新增函数 ExecBuildHash32FromAttrs

ExecBuildHash32FromAttrsPostgreSQL 中新增的一个函数,旨在优化 GROUP BYNOT IN 子查询中的哈希计算过程。传统方法通过逐列调用哈希函数FmgrInfo计算哈希值 ,导致函数调用开销较大,尤其在处理多列或大量数据时效率低下。

该函数通过构建一个 ExprState表达式状态 )对象,将哈希计算逻辑封装为一系列可执行的操作步骤(ExprEvalStep),从而减少函数调用次数,并支持即时编译(JIT),将计算逻辑转化为高效的机器码。此外,函数支持初始哈希值(init_value)的设置,并通过统一的执行计划优化多列哈希值的组合过程。函数源码如下所示:

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);
	// 初始化一个临时的 ExprEvalStep 结构,用于构建执行步骤
	ExprEvalStep scratch = {0};
	// 初始化中间结果存储,用于存储多列哈希计算的中间值
	NullableDatum *iresult = NULL;
	// 定义操作码,用于指定当前步骤的类型(如提取列值或调用哈希函数)
	intptr_t	opcode;
	// 记录最大列编号,用于确定需要解构的元组范围
	AttrNumber	last_attnum = 0;

	// 断言列数非负,确保输入参数有效
	Assert(numCols >= 0);

	// 设置 ExprState 的父节点为传入的 PlanState,用于上下文关联
	state->parent = parent;

	/*
	 * 如果有多于一个列需要哈希,或者有一个列且有非零初始值,
	 * 分配内存用于存储中间哈希值,以便在多列计算时进行组合
	 */
	if ((int64) numCols + (init_value != 0) > 1)
		iresult = palloc(sizeof(NullableDatum));

	/* 遍历所有列,找到最大的列编号,以便解构元组到该位置 */
	for (int i = 0; i < numCols; i++)
		last_attnum = Max(last_attnum, keyColIdx[i]);

	// 设置操作码为提取部分列值(EEOP_INNER_FETCHSOME),准备从元组中提取数据
	scratch.opcode = EEOP_INNER_FETCHSOME;
	// 指定需要提取的最大列编号
	scratch.d.fetch.last_var = last_attnum;
	// 设置非固定格式,允许动态解构元组
	scratch.d.fetch.fixed = false;
	// 指定元组操作类型(如 TTSOpsMinimalTuple)
	scratch.d.fetch.kind = ops;
	// 设置元组描述符,用于定义列的结构
	scratch.d.fetch.known_desc = desc;
	// 计算元组槽信息并检查是否需要添加提取步骤
	if (ExecComputeSlotInfo(state, &scratch))
		// 将提取步骤添加到 ExprState 的执行计划中
		ExprEvalPushStep(state, &scratch);

	// 如果初始哈希值为 0
	if (init_value == 0)
	{
		/*
		 * 没有初始值,直接使用第一个列的哈希函数结果,无需与初始值组合
		 * 设置操作码为 EEOP_HASHDATUM_FIRST,表示首次哈希计算
		 */
		opcode = EEOP_HASHDATUM_FIRST;
	}
	else
	{
		/*
		 * 设置初始哈希值的操作,存储到中间结果或 ExprState 的结果字段
		 * 如果有列要哈希,存储到中间结果;否则直接存储到 ExprState
		 */
		scratch.opcode = EEOP_HASHDATUM_SET_INITVAL;
		// 将初始值转换为 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;
		// 初始化函数调用信息结构
		FunctionCallInfo fcinfo;
		// 获取当前列的排序规则
		Oid			inputcollid = collations[i];
		// 列编号从 1 开始,转换为 0 基索引
		AttrNumber	attnum = keyColIdx[i] - 1;

		// 获取当前列的哈希函数
		finfo = &hashfunctions[i];
		// 分配并初始化函数调用信息结构,参数数量为 1
		fcinfo = palloc0(SizeForFunctionCallInfo(1));

		// 初始化函数调用信息,设置函数、参数数量和排序规则
		InitFunctionCallInfoData(*fcinfo, finfo, 1, inputcollid, NULL, NULL);

		/*
		 * 设置提取列值的步骤(EEOP_INNER_VAR),将指定列的值存储到哈希函数的第一个参数
		 */
		scratch.opcode = EEOP_INNER_VAR;
		// 设置存储目标为哈希函数的第一个参数
		scratch.resvalue = &fcinfo->args[0].value;
		scratch.resnull = &fcinfo->args[0].isnull;
		// 设置要提取的列编号
		scratch.d.var.attnum = attnum;
		// 设置列的数据类型
		scratch.d.var.vartype = TupleDescAttr(desc, attnum)->atttypid;

		// 将提取列值的步骤添加到执行计划
		ExprEvalPushStep(state, &scratch);

		// 设置调用哈希函数的步骤,使用之前确定的操作码
		scratch.opcode = opcode;

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

			// 中间列的哈希结果存储到中间结果中
			scratch.resvalue = &iresult->value;
			scratch.resnull = &iresult->isnull;
		}

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

		// 设置哈希函数信息
		scratch.d.hashdatum.finfo = finfo;
		// 设置函数调用信息
		scratch.d.hashdatum.fcinfo_data = fcinfo;
		// 设置函数地址
		scratch.d.hashdatum.fn_addr = finfo->fn_addr;
		// 设置跳转标志,初始为 -1
		scratch.d.hashdatum.jumpdone = -1;

		// 将哈希函数调用步骤添加到执行计划
		ExprEvalPushStep(state, &scratch);

		// 后续列使用 EEOP_HASHDATUM_NEXT32,以组合前面的哈希值
		opcode = EEOP_HASHDATUM_NEXT32;
	}

	// 设置终止步骤,清除结果指针
	scratch.resvalue = NULL;
	scratch.resnull = NULL;
	// 设置操作码为 EEOP_DONE,表示执行计划结束
	scratch.opcode = EEOP_DONE;
	// 将终止步骤添加到执行计划
	ExprEvalPushStep(state, &scratch);

	// 准备 ExprState,使其可执行
	ExecReadyExpr(state);

	// 返回构建完成的 ExprState
	return state;
}

实例说明

假设有一个查询 SELECT region, SUM(amount) FROM sales GROUP BY region,其中 sales 表包含 region(字符串类型)和 amount(数值类型)两列。我们需要对 region 列进行哈希计算以构建哈希表。以下通过示例说明每行代码的作用:

示例场景

输入参数:

  • descsales 表的元组描述符 ,包含 region(列 1,类型 VARCHAR)和 amount(列 2,类型 NUMERIC)。
  • opsTTSOpsMinimalTuple,用于操作最小元组。
  • hashfunctions:包含一个哈希函数 (如 hash_any 用于字符串)。
  • collations字符串的排序规则 (如 C 排序)。
  • numCols1(仅对 region 列哈希)。
  • keyColIdx[1](表示哈希 region 列)。
  • parentHashAggregate 节点的状态。
  • init_value0(无初始哈希值)。

每行代码的作用

  1. 创建 ExprState
c 复制代码
ExprState *state = makeNode(ExprState);
  • 作用 :创建一个新的 ExprState 节点,用于存储哈希计算的执行计划。
  • 示例 :为 region 列的哈希计算创建一个空的 ExprState,后续将填充执行步骤。

  1. 初始化临时步骤和变量:
c 复制代码
ExprEvalStep scratch = {0};
NullableDatum *iresult = NULL;
intptr_t opcode;
AttrNumber last_attnum = 0;
  • 作用 :初始化 scratch 用于构建执行步骤iresult 用于存储中间哈希值opcode 指定步骤类型last_attnum 记录最大列编号
  • 示例scratch 初始化为空,iresult 初始为 NULL(因为只有一列,暂不需要),opcodelast_attnum 待后续赋值。

  1. 设置父节点:
c 复制代码
state->parent = parent;
  • 作用 :将 ExprState 的父节点设置为传入的 PlanState,关联查询计划上下文。
  • 示例 :将 state 关联到 HashAggregate 节点,确保执行时使用正确的上下文。

  1. 分配中间结果存储:
c 复制代码
if ((int64) numCols + (init_value != 0) > 1)
    iresult = palloc(sizeof(NullableDatum));
  • 作用:如果有多列或有初始值,分配内存存储中间哈希值。
  • 示例 :因为 numCols = 1init_value = 0,无需分配 iresult,保持为 NULL

  1. 确定最大列编号:
c 复制代码
for (int i = 0; i < numCols; i++)
    last_attnum = Max(last_attnum, keyColIdx[i]);
  • 作用 :遍历 keyColIdx,找到最大列编号,用于解构元组。
  • 示例keyColIdx = [1]last_attnum 设置为 1,表示只需解构到 region 列。

  1. 设置提取列值的步骤:
c 复制代码
// 设置操作码为提取部分列值(EEOP_INNER_FETCHSOME),准备从元组中提取数据
scratch.opcode = EEOP_INNER_FETCHSOME;
// 指定需要提取的最大列编号
scratch.d.fetch.last_var = last_attnum;
// 设置非固定格式,允许动态解构元组
scratch.d.fetch.fixed = false;
// 指定元组操作类型(如 TTSOpsMinimalTuple)
scratch.d.fetch.kind = ops;
// 设置元组描述符,用于定义列的结构
scratch.d.fetch.known_desc = desc;
// 计算元组槽信息并检查是否需要添加提取步骤
if (ExecComputeSlotInfo(state, &scratch))
	// 将提取步骤添加到 ExprState 的执行计划中
	ExprEvalPushStep(state, &scratch);
  • 作用 :构建提取列值的步骤(EEOP_INNER_FETCHSOME),指定最大列编号元组操作类型描述符,并添加到执行计划。
  • 示例 :生成一个步骤,解构元组到列 1region),使用 TTSOpsMinimalTuple 操作,确保能提取 region 的值。

  1. 检查初始值并设置操作码:
c 复制代码
if (init_value == 0)
{
    opcode = EEOP_HASHDATUM_FIRST;
}
else
{
    ...
}
  • 作用 :如果初始值为 0,设置操作码为 EEOP_HASHDATUM_FIRST直接使用第一个列的哈希值;否则设置初始值步骤(本例不触发)。
  • 示例init_value = 0,设置 opcode = EEOP_HASHDATUM_FIRST,表示直接计算 region 的哈希值。

  1. 遍历列,构建哈希计算步骤:
c 复制代码
for (int i = 0; i < numCols; i++)
{
    FmgrInfo *finfo;
    FunctionCallInfo fcinfo;
    Oid inputcollid = collations[i];
    AttrNumber attnum = keyColIdx[i] - 1;
    finfo = &hashfunctions[i];
    fcinfo = palloc0(SizeForFunctionCallInfo(1));
    InitFunctionCallInfoData(*fcinfo, finfo, 1, inputcollid, NULL, NULL);
  • 作用 :为每一列初始化哈希函数和函数调用信息,设置排序规则和列编号。
  • 示例 :为 region 列(keyColIdx[0] = 1)获取哈希函数 hash_any,初始化函数调用信息,设置排序规则为 C

  1. 提取列值步骤:
c 复制代码
scratch.opcode = EEOP_INNER_VAR;
scratch.resvalue = &fcinfo->args[0].value;
scratch.resnull = &fcinfo->args[0].isnull;
scratch.d.var.attnum = attnum;
scratch.d.var.vartype = TupleDescAttr(desc, attnum)->atttypid;
ExprEvalPushStep(state, &scratch);
  • 作用 :生成提取列值的步骤(EEOP_INNER_VAR),将值存储到哈希函数的第一个参数。
  • 示例 :生成步骤提取 region 列(attnum = 0)的值,存储到 fcinfo->args[0],类型为 VARCHAR

  1. 提取列值步骤:
c 复制代码
scratch.opcode = opcode;
if (i == numCols - 1)
{
    scratch.resvalue = &state->resvalue;
    scratch.resnull = &state->resnull;
}
else
{
    Assert(iresult != NULL);
    scratch.resvalue = &iresult->value;
    scratch.resnull = &iresult->isnull;
}
scratch.d.hashdatum.iresult = iresult;
scratch.d.hashdatum.finfo = finfo;
scratch.d.hashdatum.fcinfo_data = fcinfo;
scratch.d.hashdatum.fn_addr = finfo->fn_addr;
scratch.d.hashdatum.jumpdone = -1;
ExprEvalPushStep(state, &scratch);
opcode = EEOP_HASHDATUM_NEXT32;
  • 作用 :生成调用哈希函数的步骤(EEOP_HASHDATUM_FIRSTEEOP_HASHDATUM_NEXT32),存储结果到 ExprState最后一列 )或中间结果非最后一列),设置函数信息并添加到执行计划。
  • 示例 :为 region 列生成步骤,使用 EEOP_HASHDATUM_FIRST,调用 hash_any,结果存储到 state->resvalue(因为是最后一列),设置函数信息并添加步骤。

  1. 添加终止步骤:
c 复制代码
scratch.resvalue = NULL;
scratch.resnull = NULL;
scratch.opcode = EEOP_DONE;
ExprEvalPushStep(state, &scratch);
  • 作用 :添加终止步骤(EEOP_DONE),表示执行计划结束。
  • 示例 :生成一个终止步骤,确保 ExprState 执行到此结束。

  1. 准备并返回 ExprState:
c 复制代码
ExecReadyExpr(state);
return state;
  • 作用 :准备 ExprState 使其可执行,并返回给调用者。
  • 示例 :完成 ExprState 的初始化,返回包含提取 region 值和调用 hash_any 的执行计划。

执行计划示例

生成的 ExprState 包含以下步骤:

  1. EEOP_INNER_FETCHSOME:解构元组,提取 region 列。
  2. EEOP_INNER_VAR:提取 region 值,存储到哈希函数参数。
  3. EEOP_HASHDATUM_FIRST:调用 hash_any,生成哈希值 ,存储到 state->resvalue
  4. EEOP_DONE:结束执行。

当执行 SELECT region, SUM(amount) FROM sales GROUP BY region 时,ExprState 会被 ExecEvalExpr 调用,快速计算 region 的哈希值,用于哈希表操作。如果启用 JITLLVM 会将这些步骤编译为机器码,减少执行时间。

ExprState 结构体

ExprEvalStepPostgreSQL 执行器中用于定义 ExprState 执行计划中单个步骤的数据结构。每个步骤通过 opcode 指定操作类型 (如提取列值调用函数处理条件 等),并通过 resvalueresnull 存储结果。d 联合体根据操作类型存储特定的上下文数据(如哈希函数信息跳转目标 等),支持多种表达式操作,包括哈希计算类型转换聚合函数 等。

在补丁中,ExecBuildHash32FromAttrs 使用 ExprEvalStep 定义了提取列值EEOP_INNER_VAR)、设置初始哈希值EEOP_HASHDATUM_SET_INITVAL)和调用哈希函数EEOP_HASHDATUM_FIRSTEEOP_HASHDATUM_NEXT32)的步骤。这些步骤支持高效执行,并可通过 JIT 编译优化为机器码,提升性能。

c 复制代码
typedef struct ExprState
{
    // 节点类型,标识这是 ExprState 节点
    NodeTag		type;

    // 标志位,存储 EEO_FLAG_* 位掩码,用于控制表达式执行行为
    uint8		flags;			/* bitmask of EEO_FLAG_* bits, see above */

    /*
     * 用于存储标量表达式的结果值,或由 ExecBuildProjectionInfo() 构建的表达式中各列的结果
     */
#define FIELDNO_EXPRSTATE_RESNULL 2
    // 结果是否为 NULL
    bool		resnull;
#define FIELDNO_EXPRSTATE_RESVALUE 3
    // 结果值,存储为 Datum 类型
    Datum		resvalue;

    /*
     * 如果表达式生成元组结果,则存储结果的 TupleTableSlot;否则为 NULL
     */
#define FIELDNO_EXPRSTATE_RESULTSLOT 4
    TupleTableSlot *resultslot;

    /*
     * 计算表达式返回值的指令序列,存储一系列 ExprEvalStep
     */
    struct ExprEvalStep *steps;

    /*
     * 实际执行表达式的函数指针,根据表达式复杂度设置为不同函数
     */
    ExprStateEvalFunc evalfunc;

    // 原始表达式树,仅用于调试
    Expr	   *expr;

    // 执行函数的私有状态数据
    void	   *evalfunc_private;

    /*
     * 以下字段仅在编译期间(ExecInitExpr)需要,之后可丢弃
     */

    // 当前步骤数量
    int			steps_len;		/* number of steps currently */
    // 步骤数组的分配长度
    int			steps_alloc;	/* allocated length of steps array */

#define FIELDNO_EXPRSTATE_PARENT 11
    // 父 PlanState 节点,若存在则关联查询计划上下文
    struct PlanState *parent;	/* parent PlanState node, if any */
    // 外部参数信息,用于编译 PARAM_EXTERN 节点
    ParamListInfo ext_params;	/* for compiling PARAM_EXTERN nodes */

    // 最内层 CASE 表达式的值
    Datum	   *innermost_caseval;
    // 最内层 CASE 表达式是否为 NULL
    bool	   *innermost_casenull;

    // 最内层域约束的值
    Datum	   *innermost_domainval;
    // 最内层域约束是否为 NULL
    bool	   *innermost_domainnull;

    /*
     * 用于支持软错误处理的上下文。如果调用者希望抛出错误,设为 NULL;
     * 若不希望抛出错误,调用者在调用 ExecInitExprRec() 前设置有效的 ErrorSaveContext
     */
    ErrorSaveContext *escontext;
} ExprState;

ExprEvalStep 结构体

以下是 PostgreSQLExprEvalStep 结构体的每一行代码添加中文注释,解释每个字段的作用。ExprEvalStepExprState 中用于定义表达式执行计划中单个步骤的数据结构,描述了执行操作的类型、输入输出以及相关上下文信息。在补丁(如 ExecBuildHash32FromAttrs)中,它用于构建哈希计算的步骤,支持高效执行和 JIT 编译。

c 复制代码
typedef struct ExprEvalStep
{
    /*
     * 要执行的指令。在准备阶段为 ExprEvalOp 枚举值,
     * 之后可能改为其他类型(如计算跳转的指针),因此使用 intptr_t
     */
    intptr_t	opcode;

    // 存储当前步骤结果的指针
    Datum	   *resvalue;
    // 存储当前步骤结果是否为 NULL 的指针
    bool	   *resnull;

    /*
     * 操作的内联数据,访问速度快但会增加指令大小。
     * 联合体大小控制在 64 位系统上不超过 40 字节,确保整个结构不超过 64 字节(单缓存行)
     */
    union
    {
        // 用于 EEOP_INNER/OUTER/SCAN_FETCHSOME,提取元组列值
        struct
        {
            // 要提取的最高列编号(包含)
            int			last_var;
            // 每次调用时槽类型是否固定
            bool		fixed;
            // 已知的元组描述符
            TupleDesc	known_desc;
            // 槽类型,仅当 fixed 为 true 时可信
            const TupleTableSlotOps *kind;
        }			fetch;

        // 用于 EEOP_INNER/OUTER/SCAN_[SYS]VAR[_FIRST],提取单个列值
        struct
        {
            // 列编号(普通变量为 attr 编号 - 1,系统变量为负值)
            int			attnum;
            // 变量的数据类型 OID
            Oid			vartype;	/* type OID of variable */
        }			var;

        // 用于 EEOP_WHOLEROW,处理整行引用
        struct
        {
            // 原始 Var 节点
            Var		   *var;	/* original Var node in plan tree */
            // 是否首次执行,需要初始化
            bool		first;	/* first time through, need to initialize? */
            // 是否需要运行时检查 NULL 值
            bool		slow;	/* need runtime check for nulls? */
            // 结果元组的描述符
            TupleDesc	tupdesc;	/* descriptor for resulting tuples */
            // 移除无关列的过滤器
            JunkFilter *junkFilter; /* JunkFilter to remove resjunk cols */
        }			wholerow;

        // 用于 EEOP_ASSIGN_*_VAR,赋值列值到结果槽
        struct
        {
            // 目标在结果槽中的索引
            int			resultnum;
            // 源列编号 - 1
            int			attnum;
        }			assign_var;

        // 用于 EEOP_ASSIGN_TMP[_MAKE_RO],赋值临时值
        struct
        {
            // 目标在结果槽中的索引
            int			resultnum;
        }			assign_tmp;

        // 用于 EEOP_CONST,处理常量值
        struct
        {
            // 常量的值
            Datum		value;
            // 常量是否为 NULL
            bool		isnull;
        }			constval;

        // 用于 EEOP_FUNCEXPR_* / NULLIF / DISTINCT,调用函数
        struct
        {
            // 函数查找信息
            FmgrInfo   *finfo;	/* function's lookup data */
            // 函数调用参数等信息
            FunctionCallInfo fcinfo_data;	/* arguments etc */
            // 直接访问的函数调用地址,减少间接调用开销
            PGFunction	fn_addr;	/* actual call address */
            // 参数数量
            int			nargs;	/* number of arguments */
            // 是否将第一个参数设为只读(仅用于 NULLIF)
            bool		make_ro;	/* make arg0 R/O (used only for NULLIF) */
        }			func;

        // 用于 EEOP_BOOL_*_STEP,布尔表达式处理
        struct
        {
            // 跟踪是否有输入为 NULL
            bool	   *anynull;	/* track if any input was NULL */
            // 结果确定时跳转的目标
            int			jumpdone;	/* jump here if result determined */
        }			boolexpr;

        // 用于 EEOP_QUAL,条件判断
        struct
        {
            // 条件为假或 NULL 时跳转的目标
            int			jumpdone;	/* jump here on false or null */
        }			qualexpr;

        // 用于 EEOP_JUMP[_CONDITION],无条件或条件跳转
        struct
        {
            // 跳转目标指令的索引
            int			jumpdone;	/* target instruction's index */
        }			jump;

        // 用于 EEOP_NULLTEST_ROWIS[NOT]NULL,检查整行是否为 NULL
        struct
        {
            // 复合类型的缓存描述符,运行时填充
            ExprEvalRowtypeCache rowcache;
        }			nulltest_row;

        // 用于 EEOP_PARAM_EXEC/EXTERN 和 EEOP_PARAM_SET,处理参数
        struct
        {
            // 参数的数字 ID
            int			paramid;	/* numeric ID for parameter */
            // 参数的数据类型 OID
            Oid			paramtype;	/* OID of parameter's datatype */
        }			param;

        // 用于 EEOP_PARAM_CALLBACK,参数回调处理
        struct
        {
            // 附加的评估子程序
            ExecEvalSubroutine paramfunc;	/* add-on evaluation subroutine */
            // 子程序的私有数据
            void	   *paramarg;	/* private data for same */
            // 参数的数字 ID
            int			paramid;	/* numeric ID for parameter */
            // 参数的数据类型 OID
            Oid			paramtype;	/* OID of parameter's datatype */
        }			cparam;

        // 用于 EEOP_CASE_TESTVAL/DOMAIN_TESTVAL,CASE 或域测试值
        struct
        {
            // 要返回的值
            Datum	   *value;	/* value to return */
            // 值是否为 NULL
            bool	   *isnull;
        }			casetest;

        // 用于 EEOP_MAKE_READONLY,将值设为只读
        struct
        {
            // 要强制设为只读的值
            Datum	   *value;	/* value to coerce to read-only */
            // 值是否为 NULL
            bool	   *isnull;
        }			make_readonly;

        // 用于 EEOP_IOCOERCE,输入/输出类型转换
        struct
        {
            // 源类型的输出函数查找和调用信息
            FmgrInfo   *finfo_out;
            FunctionCallInfo fcinfo_data_out;
            // 目标类型的输入函数查找和调用信息
            FmgrInfo   *finfo_in;
            FunctionCallInfo fcinfo_data_in;
        }			iocoerce;

        // 用于 EEOP_SQLVALUEFUNCTION,SQL 值函数
        struct
        {
            // SQL 值函数的原始节点
            SQLValueFunction *svf;
        }			sqlvaluefunction;

        // 用于 EEOP_NEXTVALUEEXPR,序列的下一个值
        struct
        {
            // 序列的 OID
            Oid			seqid;
            // 序列的数据类型 OID
            Oid			seqtypid;
        }			nextvalueexpr;

        // 用于 EEOP_ARRAYEXPR,数组表达式
        struct
        {
            // 元素值的存储数组
            Datum	   *elemvalues; /* element values get stored here */
            // 元素是否为 NULL 的标志数组
            bool	   *elemnulls;
            // 元素数量
            int			nelems; /* length of the above arrays */
            // 数组元素类型
            Oid			elemtype;	/* array element type */
            // 元素类型的存储长度
            int16		elemlength; /* typlen of the array element type */
            // 元素类型是否按值传递
            bool		elembyval;	/* is the element type pass-by-value? */
            // 元素类型的对齐方式
            char		elemalign;	/* typalign of the element type */
            // 是否为多维数组
            bool		multidims;	/* is array expression multi-D? */
        }			arrayexpr;

        // 用于 EEOP_ARRAYCOERCE,数组类型转换
        struct
        {
            // 每个元素的表达式状态,空则无需元素处理
            ExprState  *elemexprstate;	/* null if no per-element work */
            // 结果数组的元素类型
            Oid			resultelemtype; /* element type of result array */
            // 数组映射的工作空间
            struct ArrayMapState *amstate;	/* workspace for array_map */
        }			arraycoerce;

        // 用于 EEOP_ROW,构造行
        struct
        {
            // 结果元组的描述符
            TupleDesc	tupdesc;	/* descriptor for result tuples */
            // 构成行的值的工作空间
            Datum	   *elemvalues;
            // 构成行的值的 NULL 标志
            bool	   *elemnulls;
        }			row;

        // 用于 EEOP_ROWCOMPARE_STEP,行比较步骤
        struct
        {
            // 列比较函数的查找和调用数据
            FmgrInfo   *finfo;
            FunctionCallInfo fcinfo_data;
            // 比较函数的调用地址
            PGFunction	fn_addr;
            // 比较结果为 NULL 时的跳转目标
            int			jumpnull;
            // 比较结果不等时的跳转目标
            int			jumpdone;
        }			rowcompare_step;

        // 用于 EEOP_ROWCOMPARE_FINAL,行比较最终结果
        struct
        {
            // 行比较类型(如等于、大于)
            RowCompareType rctype;
        }			rowcompare_final;

        // 用于 EEOP_MINMAX,计算最小/最大值
        struct
        {
            // 参数值的工作空间
            Datum	   *values;
            // 参数是否为 NULL 的标志
            bool	   *nulls;
            // 参数数量
            int			nelems;
            // 操作类型(GREATEST 或 LEAST)
            MinMaxOp	op;
            // 比较函数的查找和调用数据
            FmgrInfo   *finfo;
            FunctionCallInfo fcinfo_data;
        }			minmax;

        // 用于 EEOP_FIELDSELECT,字段选择
        struct
        {
            // 要提取的字段编号
            AttrNumber	fieldnum;	/* field number to extract */
            // 字段的类型
            Oid			resulttype; /* field's type */
            // 复合类型的缓存描述符,运行时填充
            ExprEvalRowtypeCache rowcache;
        }			fieldselect;

        // 用于 EEOP_FIELDSTORE_DEFORM / FIELDSTORE_FORM,字段存储
        struct
        {
            // 原始 FieldStore 表达式节点
            FieldStore *fstore;
            // 复合类型的缓存描述符,运行时填充(DEFORM 和 FORM 共享)
            ExprEvalRowtypeCache *rowcache;
            // 列值的工作空间
            Datum	   *values;
            // 列值的 NULL 标志
            bool	   *nulls;
            // 列数量
            int			ncolumns;
        }			fieldstore;

        // 用于 EEOP_SBSREF_SUBSCRIPTS,下标引用
        struct
        {
            // 下标评估子程序
            ExecEvalBoolSubroutine subscriptfunc;	/* evaluation subroutine */
            // 下标状态数据(过大,无法内联)
            struct SubscriptingRefState *state;
            // NULL 值时的跳转目标
            int			jumpdone;	/* jump here on null */
        }			sbsref_subscript;

        // 用于 EEOP_SBSREF_OLD / ASSIGN / FETCH,下标引用操作
        struct
        {
            // 下标评估子程序
            ExecEvalSubroutine subscriptfunc;	/* evaluation subroutine */
            // 下标状态数据(过大,无法内联)
            struct SubscriptingRefState *state;
        }			sbsref;

        // 用于 EEOP_DOMAIN_NOTNULL / DOMAIN_CHECK,域约束检查
        struct
        {
            // 约束名称
            char	   *constraintname;
            // CHECK 约束结果的存储位置
            Datum	   *checkvalue;
            // CHECK 约束结果是否为 NULL
            bool	   *checknull;
            // 域类型的 OID
            Oid			resulttype;
            // 错误处理上下文
            ErrorSaveContext *escontext;
        }			domaincheck;

        // 用于 EEOP_HASH_SET_INITVAL,设置初始哈希值
        struct
        {
            // 初始哈希值
            Datum		init_value;
        }			hashdatum_initvalue;

        // 用于 EEOP_HASHDATUM_(FIRST|NEXT32)[_STRICT],哈希计算
        struct
        {
            // 哈希函数查找信息
            FmgrInfo   *finfo;	/* function's lookup data */
            // 哈希函数调用参数等信息
            FunctionCallInfo fcinfo_data;	/* arguments etc */
            // 直接访问的哈希函数调用地址,减少间接调用开销
            PGFunction	fn_addr;	/* actual call address */
            // NULL 值时的跳转目标
            int			jumpdone;	/* jump here on null */
            // 中间哈希结果的存储位置
            NullableDatum *iresult; /* intermediate hash result */
        }			hashdatum;

        // 用于 EEOP_CONVERT_ROWTYPE,行类型转换
        struct
        {
            // 输入复合类型
            Oid			inputtype;	/* input composite type */
            // 输出复合类型
            Oid			outputtype; /* output composite type */
            // 输入类型的缓存,运行时填充
            ExprEvalRowtypeCache *incache;	/* cache for input type */
            // 输出类型的缓存,运行时填充
            ExprEvalRowtypeCache *outcache; /* cache for output type */
            // 列映射
            TupleConversionMap *map;	/* column mapping */
        }			convert_rowtype;

        // 用于 EEOP_SCALARARRAYOP,标量数组操作
        struct
        {
            // 元素类型,运行时填充
            Oid			element_type;	/* InvalidOid if not yet filled */
            // 使用 OR 或 AND 语义
            bool		useOr;	/* use OR or AND semantics? */
            // 数组元素类型的存储长度
            int16		typlen; /* array element type storage info */
            // 元素类型是否按值传递
            bool		typbyval;
            // 元素类型的对齐方式
            char		typalign;
            // 函数查找信息
            FmgrInfo   *finfo;	/* function's lookup data */
            // 函数调用参数等信息
            FunctionCallInfo fcinfo_data;	/* arguments etc */
            // 直接访问的函数调用地址,减少间接调用开销
            PGFunction	fn_addr;	/* actual call address */
        }			scalararrayop;

        // 用于 EEOP_HASHED_SCALARARRAYOP,哈希标量数组操作
        struct
        {
            // 是否包含 NULL 值
            bool		has_nulls;
            // IN 或 NOT IN 操作
            bool		inclause;	/* true for IN and false for NOT IN */
            // 元素哈希表
            struct ScalarArrayOpExprHashTable *elements_tab;
            // 函数查找信息
            FmgrInfo   *finfo;	/* function's lookup data */
            // 函数调用参数等信息
            FunctionCallInfo fcinfo_data;	/* arguments etc */
            // 标量数组操作的原始节点
            ScalarArrayOpExpr *saop;
        }			hashedscalararrayop;

        // 用于 EEOP_XMLEXPR,XML 表达式
        struct
        {
            // 原始 XML 表达式节点
            XmlExpr    *xexpr;	/* original expression node */
            // 命名参数的工作空间
            Datum	   *named_argvalue;
            // 命名参数的 NULL 标志
            bool	   *named_argnull;
            // 未命名参数的工作空间
            Datum	   *argvalue;
            // 未命名参数的 NULL 标志
            bool	   *argnull;
        }			xmlexpr;

        // 用于 EEOP_JSON_CONSTRUCTOR,JSON 构造函数
        struct
        {
            // JSON 构造函数状态
            struct JsonConstructorExprState *jcstate;
        }			json_constructor;

        // 用于 EEOP_AGGREF,聚合函数引用
        struct
        {
            // 聚合函数编号
            int			aggno;
        }			aggref;

        // 用于 EEOP_GROUPING_FUNC,分组函数
        struct
        {
            // 列编号的整数列表
            List	   *clauses;	/* integer list of column numbers */
        }			grouping_func;

        // 用于 EEOP_WINDOW_FUNC,窗口函数
        struct
        {
            // 窗口函数状态,由 nodeWindowAgg.c 修改
            WindowFuncExprState *wfstate;
        }			window_func;

        // 用于 EEOP_SUBPLAN,子查询
        struct
        {
            // 子查询状态,由 nodeSubplan.c 创建
            SubPlanState *sstate;
        }			subplan;

        // 用于 EEOP_AGG_*DESERIALIZE,聚合反序列化
        struct
        {
            // 函数调用信息
            FunctionCallInfo fcinfo_data;
            // NULL 值时的跳转目标
            int			jumpnull;
        }			agg_deserialize;

        // 用于 EEOP_AGG_STRICT_INPUT_CHECK_NULLS / STRICT_INPUT_CHECK_ARGS,严格输入检查
        struct
        {
            /*
             * EEOP_AGG_STRICT_INPUT_CHECK_ARGS:args 指向需要检查 NULL 的 NullableDatum
             * EEOP_AGG_STRICT_INPUT_CHECK_NULLS:nulls 指向需要检查 NULL 的布尔值
             * 两种情况分别处理 TupleTableSlot 和 FunctionCallInfo 的 NULL 检查
             */
            NullableDatum *args;
            bool	   *nulls;
            // 参数数量
            int			nargs;
            // NULL 值时的跳转目标
            int			jumpnull;
        }			agg_strict_input_check;

        // 用于 EEOP_AGG_PLAIN_PERGROUP_NULLCHECK,聚合分组 NULL 检查
        struct
        {
            // 分组集的偏移量
            int			setoff;
            // NULL 值时的跳转目标
            int			jumpnull;
        }			agg_plain_pergroup_nullcheck;

        // 用于 EEOP_AGG_PRESORTED_DISTINCT_{SINGLE,MULTI},预排序去重检查
        struct
        {
            // 聚合转换状态
            AggStatePerTrans pertrans;
            // 聚合上下文
            ExprContext *aggcontext;
            // 去重失败时的跳转目标
            int			jumpdistinct;
        }			agg_presorted_distinctcheck;

        // 用于 EEOP_AGG_PLAIN_TRANS_[INIT_][STRICT_]{BYVAL,BYREF} 和 EEOP_AGG_ORDERED_TRANS_{DATUM,TUPLE},聚合转换
        struct
        {
            // 聚合转换状态
            AggStatePerTrans pertrans;
            // 聚合上下文
            ExprContext *aggcontext;
            // 集合编号
            int			setno;
            // 转换编号
            int			transno;
            // 集合偏移量
            int			setoff;
        }			agg_trans;

        // 用于 EEOP_IS_JSON,JSON 类型检查
        struct
        {
            // 原始 JSON 谓词节点
            JsonIsPredicate *pred;	/* original expression node */
        }			is_json;

        // 用于 EEOP_JSONEXPR_PATH,JSON 路径表达式
        struct
        {
            // JSON 表达式状态
            struct JsonExprState *jsestate;
        }			jsonexpr;

        // 用于 EEOP_JSONEXPR_COERCION,JSON 表达式类型转换
        struct
        {
            // 目标类型
            Oid			targettype;
            // 目标类型的模式
            int32		targettypmod;
            // 是否省略引号
            bool		omit_quotes;
            // 仅用于 JSON_EXISTS_OP 的字段
            bool		exists_coerce;
            bool		exists_cast_to_int;
            bool		exists_check_domain;
            // JSON 类型转换缓存
            void	   *json_coercion_cache;
            // 错误处理上下文
            ErrorSaveContext *escontext;
        }			jsonexpr_coercion;
    }			d;
} ExprEvalStep;

需要注意的是,并非每个参数或子结构都会在每次使用 ExprEvalStep 时被用到。联合体的设计使得在任一时刻,只有与当前 opcode 对应的子结构会被使用,其余子结构的内容未定义且不会被访问。

例如,在补丁中的 ExecBuildHash32FromAttrs 函数,生成的步骤可能包括:

  • EEOP_INNER_FETCHSOME:使用 fetch 子结构提取元组列值
  • EEOP_INNER_VAR:使用 var 子结构提取单个列值
  • EEOP_HASHDATUM_FIRSTEEOP_HASHDATUM_NEXT32:使用 hashdatum 子结构调用哈希函数

其他子结构(如 wholerowarrayexpr)在哈希计算场景中不会被使用