【PostgreSQL内核学习:基于 ExprState 的哈希计算优化—— GROUP BY 与 SubPlan 的性能提升】

发布于:2025-09-09 ⋅ 阅读:(16) ⋅ 点赞:(0)

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

一、引言

  在 PostgreSQL 中,GROUP BY 和部分 SubPlan(例如 NOT IN 子查询)需要依赖哈希表来完成分组与查找操作哈希值的计算效率直接影响聚合与子查询的整体性能
  在此前的实现中,哈希函数的调用主要通过 FunctionCall 接口完成,这种方式虽然通用,但存在以下问题:

  1. 函数调用开销大:每次都要经过函数指针间接调用,无法充分利用编译器优化。
  2. 缺乏 JIT 支持:由于不是基于 ExprState 的执行框架,JIT 编译无法将哈希过程内联,从而失去潜在的性能提升空间。

  本次补丁的意义在于:将哈希计算迁移到 ExprState 机制中,从而支持 JIT 编译和更高效的执行路径,进而优化 HashAggregateSubPlan 的性能。


二、补丁概述

1. 相关定义

  • ExprStatePostgreSQL 执行器中用于表达式求值的核心结构,支持解释执行和 JIT 编译。
  • ExecEvalStepExprState 的执行步骤,每一步对应一次计算动作。
  • 哈希聚合 (HashAggregate):通过哈希表对输入元组进行分组并聚合。
  • 哈希子计划 (Hashed SubPlan):用于 INNOT IN 等子查询条件的哈希表查找。

提交信息

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

commit adf97c1562380e02acd60dc859c289ed3a8352ee
Author: David Rowley <drowley@postgresql.org>
Date:   Tue Aug 20 13:38:22 2024 +1200

    Speed up Hash Join by making ExprStates support hashing

    Here we add ExprState support for obtaining a 32-bit hash value from a
    list of expressions.  This allows both faster hashing and also JIT
    compilation of these expressions.  This is especially useful when hash
    joins have multiple join keys as the previous code called ExecEvalExpr on
    each hash join key individually and that was inefficient as tuple
    deformation would have only taken into account one key at a time, which
    could lead to walking the tuple once for each join key.  With the new
    code, we'll determine the maximum attribute required and deform the tuple
    to that point only once.

    Some performance tests done with this change have shown up to a 20%
    performance increase of a query containing a Hash Join without JIT
    compilation and up to a 26% performance increase when JIT is enabled and
    optimization and inlining were performed by the JIT compiler.  The
    performance increase with 1 join column was less with a 14% increase
    with and without JIT.  This test was done using a fairly small hash
    table and a large number of hash probes.  The increase will likely be
    less with large tables, especially ones larger than L3 cache as memory
    pressure is more likely to be the limiting factor there.

    This commit only addresses Hash Joins, but lays expression evaluation
    and JIT compilation infrastructure for other hashing needs such as Hash
    Aggregate.

    Author: David Rowley
    Reviewed-by: Alexey Dvoichenkov <alexey@hyperplane.net>
    Reviewed-by: Tels <nospam-pg-abuse@bloodgate.com>
    Discussion: https://postgr.es/m/CAApHDvoexAxgQFNQD_GRkr2O_eJUD1-wUGm%3Dm0L%2BGc%3DT%3DkEa4g%40mail.gmail.com

2. 提交描述

核心改动:

  1. 新增函数 ExecBuildHash32FromAttrs,用于生成一个可执行的 ExprState,内部包含哈希函数的执行步骤。
  2. 修改 execGrouping.cnodeSubplan.c,将原先的 FunctionCall 方式替换为 ExprState 执行方式。
  3. 修复了 ExecBuildHash32Expr() 的潜在 bug:当没有列需要哈希但指定了初始值时,之前并不会将初始值写入结果,现在已修复。
  4. 更新头文件结构体,增加 ExprState *hashExpr 字段,用于存放新的哈希表达式。

3. 优化目的

  这个 Patch 的主要目的是优化 PostgreSQL 中的哈希连接(Hash Join)操作,特别是当连接涉及多个键(join keys)时。通过引入对 ExprState 的新支持,它允许一次性计算多个表达式的 32 位哈希值,并支持 JITJust-In-Time)编译,从而提高哈希计算的效率。

  • 性能提升:减少函数调用开销,在部分构造的测试场景中,HashAggregate 提升约 15%NOT IN 查询提升超过 30%
  • JIT 支持:哈希表达式现在可以直接编译成本地机器码,进一步减少运行时消耗。
  • 代码一致性:统一通过 ExprState 框架来处理哈希逻辑,与其他表达式求值保持一致。
  • 健壮性提升:修复了潜在的初始值丢失问题,确保边界条件下计算结果正确。
    在这里插入图片描述

源码解读

添加核心函数:构建哈希表达式的 ExprState(execExpr.c 和 executor.h)

修改内容:

  • src/backend/executor/execExpr.c 中新增函数 ExecBuildHash32Expr
  • 这个函数构建一个 ExprState,用于计算给定表达式列表(hash_exprs)的 32 位哈希值。
  • 参数包括元组描述符(desc)、哈希函数 OID 数组(hashfunc_oids)、排序规则(collations)、严格性数组(opstrict)、初始种子值(init_value)和是否保留 NULLkeep_nulls)。
  • 逻辑:初始化 ExprState,处理初始值(如果非 0,则插入设置步骤),然后循环每个表达式:
    • 分配哈希函数信息(FmgrInfoFunctionCallInfo)。
    • 构建表达式评估步骤(ExecInitExprRec),将结果存入哈希函数参数。
    • 根据严格性和 keep_nulls 选择操作码(EEOP_HASHDATUM_FIRSTNEXT32,带 _STRICT 表示严格模式下遇 NULL 提前返回)。
    • 记录跳转目标(用于 NULL 处理跳过),并在后续键中组合哈希(旋转 + 异或)。
  • 最后调整跳转、添加结束步骤,并调用 ExecReadyExpr 准备 ExprState
  • src/include/executor/executor.h 中声明这个函数。

ExecBuildHash32Expr 函数

  ExecBuildHash32ExprPostgreSQL 执行器(executor)中用于构建一个 ExprState 结构体的函数。这个 ExprState 用于高效计算一个或多个表达式的 32 位哈希值(hash value)。当有多个表达式时,它会调用对应的哈希函数(通过 OID 指定)来计算每个表达式的哈希值,然后通过位旋转(rotate)和异或(XOR)操作将这些哈希值组合成一个单一的 32 位哈希值。
  这个函数特别适用于哈希连接(Hash Join)或其他需要哈希的场景(如哈希聚合),因为它支持:

  • 批量处理多个键:在哈希连接中,如果有多个连接键,它可以一次性计算所有键的哈希值,避免多次遍历元组,提高效率。
  • NULL 值处理:根据 keep_nullsopstrict 参数,决定如何处理 NULL 输入(例如,严格模式下返回 NULL,或跳过 NULL)。
  • JIT 编译支持:构建的 ExprState 可以被 JIT(Just-In-Time)编译器优化,进一步加速执行。
  • 初始化种子值:允许使用非零初始值来“种子”哈希计算,这在某些哈希算法中用于避免碰撞或特定场景。

  函数的核心是构建一个步骤序列(ExprEvalStep),这些步骤会被推入 ExprState 的步骤列表中,最终通过 ExecReadyExpr 准备好执行。执行时,这些步骤会顺序运行,计算哈希值。

/*
 * 构建一个 ExprState,用于调用给定的哈希函数列表,对给定的 'hash_exprs' 列表进行哈希计算。
 * 当有多个表达式时,每个哈希函数返回的哈希值会被组合成一个单一的哈希值。
 *
 * desc: 要哈希表达式的元组描述符
 * ops: TupleDesc 的 TupleTableSlotOps
 * hashfunc_oids: 每个哈希函数的 Oid,一个对应每个 'hash_expr'
 * collations: 调用哈希函数时使用的排序规则列表
 * hash_expr: 要哈希值的表达式列表
 * opstrict: 与 'hashfunc_oids' 对应的数组,存储 op_strict() 值
 * parent: 'hash_exprs' 将被评估的 PlanState 节点
 * init_value: 通常为 0,但可设置为其他值来种子哈希。使用非零值稍低效但有用。
 * keep_nulls: 如果为 true,当哈希函数严格且要哈希的 Datum 为 null 时,提前返回 NULL;
 *             否则,跳过任何 NULL 输入 Datum。
 */
ExprState *
ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops,
					const Oid *hashfunc_oids, const List *collations,
					const List *hash_exprs, const bool *opstrict,
					PlanState *parent, uint32 init_value, bool keep_nulls)
{
	// 创建一个新的 ExprState 结构体,用于存储表达式评估状态
	ExprState  *state = makeNode(ExprState);
	// 定义一个临时步骤结构体,用于构建表达式评估步骤
	ExprEvalStep scratch = {0};
	// 用于存储中间哈希值的 NullableDatum 数组(如果需要)
	NullableDatum *iresult = NULL;
	// 记录需要调整跳转目标的步骤索引列表
	List	   *adjust_jumps = NIL;
	// 循环变量,用于遍历 hash_exprs 和 collations
	ListCell   *lc;
	ListCell   *lc2;
	// 严格模式下的操作码(用于处理 NULL 时提前返回)
	intptr_t	strict_opcode;
	// 非严格模式下的操作码(用于跳过 NULL)
	intptr_t	opcode;
	// 计算表达式数量,用于后续判断是否需要中间存储
	int			num_exprs = list_length(hash_exprs);

	// 断言:确保表达式列表和排序规则列表长度一致
	Assert(num_exprs == list_length(collations));

	// 设置 ExprState 的父级 PlanState
	state->parent = parent;

	/* 插入必要的设置步骤,用于准备表达式评估环境 */
	ExecCreateExprSetupSteps(state, (Node *) hash_exprs);

	/*
	 * 如果有多个表达式要哈希,或者初始值不为 0,则分配中间哈希值存储空间。
	 * 这用于在后续表达式间暂存哈希值,避免多次计算。
	 */
	if ((int64) num_exprs + (init_value != 0) > 1)
		iresult = palloc(sizeof(NullableDatum));

	// 处理初始值不为 0 的情况
	if (init_value == 0)
	{
		/*
		 * 无初始值,因此可以直接将第一个 hash_expr 的哈希函数结果赋值给结果,
		 * 无需担心与初始值的组合。
		 */
		strict_opcode = EEOP_HASHDATUM_FIRST_STRICT;
		opcode = EEOP_HASHDATUM_FIRST;
	}
	else
	{
		/*
		 * 设置初始值的操作步骤。通常存储在中间哈希值位置,
		 * 但如果没有表达式要哈希,则直接存储在 ExprState 的结果字段中。
		 */
		scratch.opcode = EEOP_HASHDATUM_SET_INITVAL;
		scratch.d.hashdatum_initvalue.init_value = UInt32GetDatum(init_value);
		scratch.resvalue = num_exprs > 0 ? &iresult->value : &state->resvalue;
		scratch.resnull = num_exprs > 0 ? &iresult->isnull : &state->resnull;

		// 将初始值设置步骤推入 ExprState 的步骤列表
		ExprEvalPushStep(state, &scratch);

		/*
		 * 使用初始值时,后续操作码使用 NEXT32/NEXT32_STRICT,
		 * 因为 FIRST/FIRST_STRICT 会覆盖初始值。
		 */
		strict_opcode = EEOP_HASHDATUM_NEXT32_STRICT;
		opcode = EEOP_HASHDATUM_NEXT32;
	}

	// 同时遍历 hash_exprs 和 collations 列表,为每个表达式构建哈希步骤
	forboth(lc, hash_exprs, lc2, collations)
	{
		// 当前表达式
		Expr	   *expr = (Expr *) lfirst(lc);
		// 哈希函数的查找信息
		FmgrInfo   *finfo;
		// 函数调用信息
		FunctionCallInfo fcinfo;
		// 当前索引
		int			i = foreach_current_index(lc);
		// 哈希函数 OID
		Oid			funcid;
		// 输入排序规则
		Oid			inputcollid = lfirst_oid(lc2);

		// 获取当前哈希函数的 OID
		funcid = hashfunc_oids[i];

		/* 分配哈希函数查找数据空间 */
		finfo = palloc0(sizeof(FmgrInfo));
		fcinfo = palloc0(SizeForFunctionCallInfo(1));

		// 初始化哈希函数信息
		fmgr_info(funcid, finfo);

		/*
		 * 构建评估哈希函数参数的步骤,将表达式值存储在哈希函数的第 0 个参数中。
		 */
		ExecInitExprRec(expr,
						state,
						&fcinfo->args[0].value,
						&fcinfo->args[0].isnull);

		// 根据是否是最后一个表达式,决定结果存储位置
		if (i == num_exprs - 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;

		/* 初始化函数调用参数结构 */
		InitFunctionCallInfoData(*fcinfo, finfo, 1, inputcollid, NULL, NULL);

		// 设置哈希函数相关字段
		scratch.d.hashdatum.finfo = finfo;
		scratch.d.hashdatum.fcinfo_data = fcinfo;
		scratch.d.hashdatum.fn_addr = finfo->fn_addr;

		// 根据是否严格和 keep_nulls 选择操作码
		scratch.opcode = opstrict[i] && !keep_nulls ? strict_opcode : opcode;
		scratch.d.hashdatum.jumpdone = -1;

		// 将当前步骤推入 ExprState 的步骤列表
		ExprEvalPushStep(state, &scratch);
		// 记录此步骤的索引,用于后续调整跳转
		adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1);

		/*
		 * 对于后续键,我们必须将哈希值与前一个哈希组合。
		 * 更新操作码为 NEXT32 类型。
		 */
		strict_opcode = EEOP_HASHDATUM_NEXT32_STRICT;
		opcode = EEOP_HASHDATUM_NEXT32;
	}

	/* 调整所有步骤的跳转目标 */
	foreach(lc, adjust_jumps)
	{
		// 获取当前步骤
		ExprEvalStep *as = &state->steps[lfirst_int(lc)];

		// 断言:确保操作码是哈希相关的
		Assert(as->opcode == EEOP_HASHDATUM_FIRST ||
			   as->opcode == EEOP_HASHDATUM_FIRST_STRICT ||
			   as->opcode == EEOP_HASHDATUM_NEXT32 ||
			   as->opcode == EEOP_HASHDATUM_NEXT32_STRICT);
		// 断言:确保跳转目标未设置
		Assert(as->d.hashdatum.jumpdone == -1);
		// 设置跳转目标为当前步骤列表末尾(表达式结束位置)
		as->d.hashdatum.jumpdone = state->steps_len;
	}

	// 设置结束步骤:清除结果指针,返回最终值
	scratch.resvalue = NULL;
	scratch.resnull = NULL;
	scratch.opcode = EEOP_DONE_RETURN;
	// 推入结束步骤
	ExprEvalPushStep(state, &scratch);

	// 准备 ExprState,包括可能的 JIT 编译
	ExecReadyExpr(state);

	// 返回构建好的 ExprState
	return state;
}

逻辑流程

  1. 初始化 ExprState 和变量:
    • 创建一个新的 ExprState
    • 检查表达式和排序规则列表长度一致。
    • 如果有多个表达式或初始值不为 0,分配中间结果存储(NullableDatum)用于暂存哈希值。
  2. 处理初始值:
    • 如果 init_value0,直接从第一个表达式开始计算。
    • 否则,插入一个步骤来设置初始值,并调整后续操作码为 “NEXT32” 类型(因为需要组合初始值)。
  3. 为每个表达式构建步骤:
    • 循环处理每个 hash_expr
      • 准备哈希函数的查找信息(FmgrInfoFunctionCallInfo)。
      • 构建表达式评估步骤,将结果存入哈希函数的参数中。
      • 根据是否严格和 keep_nulls,选择适当的操作码(FIRSTNEXT32,带 STRICT 后缀表示严格模式)。
      • 推入步骤,并记录跳转目标(用于 NULL 处理时的跳过)。
    • 对于多个表达式,后续步骤会旋转并异或前一个哈希值。
  4. 调整跳转和结束步骤:
    • 更新所有步骤的跳转目标(jumpdone),指向表达式结束位置。
    • 添加一个结束步骤(EEOP_DONE_RETURN),返回最终哈希值。
  5. 准备 ExprState
    • 调用 ExecReadyExpr 完成 ExprState 的准备,包括可能的 JIT 编译。
一致
开始: ExecBuildHash32Expr
初始化 ExprState 和变量
检查表达式数量 num_exprs 和 collations 长度是否一致
设置 state->parent = parent
插入表达式设置步骤: ExecCreateExprSetupSteps
需要中间存储?
分配中间结果存储 iresult
init_value == 0?
设置操作码:
strict_opcode = EEOP_HASHDATUM_FIRST_STRICT
opcode = EEOP_HASHDATUM_FIRST
插入初始值设置步骤:
opcode = EEOP_HASHDATUM_SET_INITVAL
存储到 iresult 或 state->resvalue
设置操作码:
strict_opcode = EEOP_HASHDATUM_NEXT32_STRICT
opcode = EEOP_HASHDATUM_NEXT32
遍历 hash_exprs 和 collations
获取当前表达式 expr 和 collation
分配并初始化哈希函数信息: finfo, fcinfo
构建表达式评估步骤: ExecInitExprRec
i == num_exprs - 1?
结果存储到 state->resvalue 和 state->resnull
结果存储到 iresult->value 和 iresult->isnull
设置哈希函数相关字段:
finfo, fcinfo_data, fn_addr
设置 iresult 字段给 NEXT32 操作码
初始化函数调用参数: InitFunctionCallInfoData
操作码是否严格且不保留 NULL?
选择 strict_opcode
选择 opcode
推入步骤到 ExprState 并记录跳转索引
更新操作码为 NEXT32/NEXT32_STRICT
还有更多表达式?
调整跳转目标: 设置 jumpdone 为步骤列表末尾
插入结束步骤: EEOP_DONE_RETURN
准备 ExprState: ExecReadyExpr
返回 ExprState
结束

优化效果:

  • 允许一次性计算多键哈希,避免重复评估表达式。
  • 支持 NULL 跳过或返回,提高灵活性(取决于连接类型)。
  • 为 JIT 奠基,因为 ExprState 可以被编译。

添加解释器支持:处理新操作码(execExprInterp.c 和 execExpr.h)

修改内容:

  • src/backend/executor/execExprInterp.cExecInterpExpr 函数中,添加对新操作码的 CASE 处理:
    • EEOP_HASHDATUM_SET_INITVAL:设置初始哈希值(非 NULL)。
    • EEOP_HASHDATUM_FIRST:计算第一个表达式的哈希,如果非 NULL 则保存,否则存 0
    • EEOP_HASHDATUM_FIRST_STRICT:严格模式下,如果 NULL 则返回 NULL 并跳转。
    • EEOP_HASHDATUM_NEXT32:旋转前哈希值,如果非 NULL 则计算新哈希并异或,否则保持原值。
    • EEOP_HASHDATUM_NEXT32_STRICT:严格模式下类似,但遇 NULL 直接返回 NULL
  • 这些操作码在解释器中实现哈希计算逻辑,包括旋转(pg_rotate_left32)和异或。
  • src/include/executor/execExpr.h 中,添加新枚举值(EEOP_HASHDATUM_*)到 ExprEvalOp,并扩展 ExprEvalStep 结构体以支持哈希相关字段(hashdatum_initvaluehashdatum)。

ExecInterpExpr 函数

@@ -477,6 +477,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
		&&CASE_EEOP_DOMAIN_TESTVAL,
		&&CASE_EEOP_DOMAIN_NOTNULL,
		&&CASE_EEOP_DOMAIN_CHECK,
+		// 添加新的哈希相关操作码到解释器跳转表
+		&&CASE_EEOP_HASHDATUM_SET_INITVAL,  // 设置初始哈希值
+		&&CASE_EEOP_HASHDATUM_FIRST,        // 计算第一个表达式的哈希值(非严格模式)
+		&&CASE_EEOP_HASHDATUM_FIRST_STRICT, // 计算第一个表达式的哈希值(严格模式)
+		&&CASE_EEOP_HASHDATUM_NEXT32,       // 计算后续表达式的哈希值(非严格模式)
+		&&CASE_EEOP_HASHDATUM_NEXT32_STRICT,// 计算后续表达式的哈希值(严格模式)
 		&&CASE_EEOP_CONVERT_ROWTYPE,
 		&&CASE_EEOP_SCALARARRAYOP,
 		&&CASE_EEOP_HASHED_SCALARARRAYOP,
@@ -1543,6 +1548,111 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();  // 继续执行下一个步骤
 		}

+		// 处理 EEOP_HASHDATUM_SET_INITVAL 操作码:设置哈希计算的初始值
+		EEO_CASE(EEOP_HASHDATUM_SET_INITVAL)
+		{
+			// 将初始值(init_value)存储到结果值指针(resvalue)
+			*op->resvalue = op->d.hashdatum_initvalue.init_value;
+			// 设置结果非 NULL(初始值总是非 NULL)
+			*op->resnull = false;
+
+			// 继续执行下一个步骤
+			EEO_NEXT();
+		}
+
+		// 处理 EEOP_HASHDATUM_FIRST 操作码:计算第一个表达式的哈希值(非严格模式)
+		EEO_CASE(EEOP_HASHDATUM_FIRST)
+		{
+			// 获取哈希函数的调用信息
+			FunctionCallInfo fcinfo = op->d.hashdatum.fcinfo_data;
+
+			/*
+			 * 对于非 NULL 输入,调用哈希函数并保存结果;
+			 * 对于 NULL 输入,存储 0,以便后续 NEXT32 操作使用初始化值进行组合
+			 */
+			if (!fcinfo->args[0].isnull)
+				// 如果输入非 NULL,调用哈希函数并存储结果
+				*op->resvalue = op->d.hashdatum.fn_addr(fcinfo);
+			else
+				// 如果输入是 NULL,存储 0 作为默认哈希值
+				*op->resvalue = (Datum) 0;
+
+			// 设置结果非 NULL(即使输入是 NULL)
+			*op->resnull = false;
+
+			// 继续执行下一个步骤
+			EEO_NEXT();
+		}
+
+		// 处理 EEOP_HASHDATUM_FIRST_STRICT 操作码:计算第一个表达式的哈希值(严格模式)
+		EEO_CASE(EEOP_HASHDATUM_FIRST_STRICT)
+		{
+			// 获取哈希函数的调用信息
+			FunctionCallInfo fcinfo = op->d.hashdatum.fcinfo_data;
+
+			// 检查输入是否为 NULL
+			if (fcinfo->args[0].isnull)
+			{
+				/*
+				 * 在严格模式下,如果输入为 NULL,表达式返回 NULL,
+				 * 并直接跳转到结束(jumpdone),无需进一步处理
+				 */
+				*op->resnull = true;  // 设置结果为 NULL
+				*op->resvalue = (Datum) 0;  // 结果值设为 0
+				// 跳转到指定的结束步骤
+				EEO_JUMP(op->d.hashdatum.jumpdone);
+			}
+
+			// 如果输入非 NULL,调用哈希函数并保存结果
+			*op->resvalue = op->d.hashdatum.fn_addr(fcinfo);
+			// 设置结果非 NULL
+			*op->resnull = false;
+
+			// 继续执行下一个步骤
+			EEO_NEXT();
+		}
+
+		// 处理 EEOP_HASHDATUM_NEXT32 操作码:计算后续表达式的哈希值(非严格模式)
+		EEO_CASE(EEOP_HASHDATUM_NEXT32)
+		{
+			// 获取哈希函数的调用信息
+			FunctionCallInfo fcinfo = op->d.hashdatum.fcinfo_data;
+			// 获取当前存储的哈希值(前一步的结果)
+			uint32		existing_hash = DatumGetUInt32(*op->resvalue);
+
+			// 将现有哈希值左旋转 1 位,以准备与新哈希值组合
+			existing_hash = pg_rotate_left32(existing_hash, 1);
+
+			// 对于 NULL 输入,保持现有哈希值不变
+			if (!fcinfo->args[0].isnull)
+			{
+				// 定义变量存储新计算的哈希值
+				uint32		hashvalue;
+
+				// 调用哈希函数计算新哈希值,并与旋转后的现有哈希值异或
+				hashvalue = DatumGetUInt32(op->d.hashdatum.fn_addr(fcinfo));
+				existing_hash = existing_hash ^ hashvalue;
+			}
+
+			// 将组合后的哈希值存储到结果指针
+			*op->resvalue = UInt32GetDatum(existing_hash);
+			// 设置结果非 NULL
+			*op->resnull = false;
+
+			// 继续执行下一个步骤
+			EEO_NEXT();
+		}
+
+		// 处理 EEOP_HASHDATUM_NEXT32_STRICT 操作码:计算后续表达式的哈希值(严格模式)
+		EEO_CASE(EEOP_HASHDATUM_NEXT32_STRICT)
+		{
+			// 获取哈希函数的调用信息
+			FunctionCallInfo fcinfo = op->d.hashdatum.fcinfo_data;
+
+			// 检查输入是否为 NULL
+			if (fcinfo->args[0].isnull)
+			{
+				/*
+				 * 在严格模式下,如果输入为 NULL,表达式返回 NULL,
+				 * 并直接跳转到结束(jumpdone),无需进一步处理
+				 */
+				*op->resnull = true;  // 设置结果为 NULL
+				*op->resvalue = (Datum) 0;  // 结果值设为 0
+				// 跳转到指定的结束步骤
+				EEO_JUMP(op->d.hashdatum.jumpdone);
+			}
+			else
+			{
+				// 获取当前存储的哈希值
+				uint32		existing_hash = DatumGetUInt32(*op->resvalue);
+				// 定义变量存储新计算的哈希值
+				uint32		hashvalue;
+
+				// 将现有哈希值左旋转 1 位
+				existing_hash = pg_rotate_left32(existing_hash, 1);
+
+				// 调用哈希函数计算新哈希值,并与旋转后的现有哈希值异或
+				hashvalue = DatumGetUInt32(op->d.hashdatum.fn_addr(fcinfo));
+				*op->resvalue = UInt32GetDatum(existing_hash ^ hashvalue);
+				// 设置结果非 NULL
+				*op->resnull = false;
+			}
+
+			// 继续执行下一个步骤
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_XMLEXPR)
 		{
 			/* 过于复杂,无法内联实现 */

优化效果:

  • 使新 ExprState 能在解释器中执行,支持高效的哈希计算链(chain)。
  • 处理 NULL 的分支优化,减少不必要计算。

修改 Hash 节点:使用新 ExprState 计算内侧哈希(nodeHash.c 和 nodeHash.h)

修改内容:

  • src/backend/executor/nodeHash.c 中:
    • 修改 ExecInitHash:移除旧的 hashkeys 初始化(ExecInitExprList),设置 hash_expr = NULL(延迟到 HashJoin 初始化)。
    • 修改 ExecHashTableCreate:移除参数 hashOperatorshashCollationskeepNulls;移除哈希函数数组分配(outer_hashfunctions 等),因为现在由 ExecBuildHash32Expr 处理。
    • 修改 MultiExecPrivateHashMultiExecParallelHash:使用 ExecEvalExprSwitchContext(node->hash_expr, ...) 计算哈希值,替换旧的 ExecHashGetHashValue 调用;添加 ResetExprContext 以重置上下文。
    • 修改 ExecHashBuildSkewHash:添加参数 hashstate,使用 hashstate->skew_hashfunction 计算倾斜桶(skew bucket)的哈希,替换旧的 hashfunctions[0]
  • src/include/executor/nodeHash.h 中:修改 ExecHashTableCreateExecHashBuildSkewHash 的签名,移除旧参数;移除 ExecHashGetHashValue 函数声明。

ExecHashBuildSkewHash 函数

  *	基于可用内存构建倾斜哈希表
  */
 static void
-ExecHashBuildSkewHash(HashJoinTable hashtable, Hash *node, int mcvsToUse)
+// 构建倾斜哈希表,用于优化常见值(MCV)的哈希连接
+ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
+					  Hash *node, int mcvsToUse)
 {
 	// 统计信息元组,用于获取 MCV 数据
 	HeapTupleData *statsTuple;
 	// 属性统计槽,用于存储 MCV 值和频率
 	AttStatsSlot sslot;
@@ -2400,7 +2288,6 @@ ExecHashBuildSkewHash(HashJoinTable hashtable, Hash *node, int mcvsToUse)
 	{
 		// 每个 MCV 的频率
 		double		frac;
 		// 哈希表桶数量
 		int			nbuckets;
-		// 哈希函数信息数组
-		FmgrInfo   *hashfunctions;
 		// 循环变量,用于遍历 MCV
 		int			i;
 
 		// 检查 MCV 数量是否超过统计槽中的值数量
 		if (mcvsToUse > sslot.nvalues)
@@ -2468,15 +2355,14 @@ ExecHashBuildSkewHash(HashJoinTable hashtable, Hash *node, int mcvsToUse)
 		 *(由 ExecHashRemoveNextSkewBucket 调用),我们希望最不常见的 MCV 优先移除
 		 */
 
-		// 获取外侧哈希函数数组
-		hashfunctions = hashtable->outer_hashfunctions;
 
 		// 遍历每个 MCV 值
 		for (i = 0; i < mcvsToUse; i++)
 		{
 			// 存储计算的哈希值
 			uint32		hashvalue;
 			// 哈希值对应的桶编号
 			int			bucket;
 
-			// 调用第一个哈希函数计算 MCV 值的哈希,带排序规则
-			hashvalue = DatumGetUInt32(FunctionCall1Coll(&hashfunctions[0],
-														 hashtable->collations[0],
+			// 使用 hashstate 中的倾斜哈希函数和排序规则计算 MCV 值的哈希
+			hashvalue = DatumGetUInt32(FunctionCall1Coll(hashstate->skew_hashfunction,
+														 hashstate->skew_collation,
 														 sslot.values[i]));
 
 			/*

ExecInitHash 函数

@@ -371,8 +382,8 @@ ExecInitHash(Hash *node, EState *estate, int eflags)
 	// 设置 HashState 的基本属性:计划节点、执行状态和执行函数
 	hashstate->ps.plan = (Plan *) node;
 	hashstate->ps.state = estate;
 	hashstate->ps.ExecProcNode = ExecHash;
+	// 延迟哈希表构建,直到执行器运行时调用 ExecHashTableCreate()
 	hashstate->hashtable = NULL;
-	// hashkeys 初始为空,将由父节点 HashJoin 设置
-	hashstate->hashkeys = NIL;	/* will be set by parent HashJoin */
 
 	/*
 	 * 其他初始化工作
@@ -393,12 +404,16 @@ ExecInitHash(Hash *node, EState *estate, int eflags)
 	// 初始化结果元组槽,使用最小元组操作(TTSOpsMinimalTuple)
 	ExecInitResultTupleSlotTL(&hashstate->ps, &TTSOpsMinimalTuple);
 	// 设置投影信息为空
 	hashstate->ps.ps_ProjInfo = NULL;
 
+	// 断言:确保计划节点的 qual 条件为空(哈希节点不处理条件)
+	Assert(node->plan.qual == NIL);
+
 	/*
-	 * 初始化子表达式
+	 * 延迟 hash_expr 的初始化,直到 ExecInitHashJoin() 调用。
+	 * 因为在当前阶段无法确定哈希连接的类型(join type),
+	 * 而调用 ExecBuildHash32Expr 需要知道连接类型以设置 keep_nulls 参数
 	 */
-	Assert(node->plan.qual == NIL);
-	// 初始化哈希键表达式列表,基于输入的 hashkeys
-	hashstate->hashkeys =
-		ExecInitExprList(node->hashkeys, (PlanState *) hashstate);
+	// 设置 hash_expr 为空,等待后续初始化
+	hashstate->hash_expr = NULL;
 
 	// 返回初始化完成的 HashState
 	return hashstate;
 }

ExecHashTableCreate 函数

  * ----------------------------------------------------------------
  */
 HashJoinTable
-ExecHashTableCreate(HashState *state, List *hashOperators, List *hashCollations, bool keepNulls)
+// 创建哈希连接表,返回构建好的 HashJoinTable
+ExecHashTableCreate(HashState *state)
 {
 	// 获取 Hash 计划节点
 	Hash	   *node;
 	// 定义哈希连接表结构体
 	HashJoinTable hashtable;
@@ -440,10 +455,6 @@ ExecHashTableCreate(HashState *state, List *hashOperators, List *hashCollations,
 	// 计划节点的预期行数
 	double		rows;
 	// 倾斜优化的 MCV(最常见值)数量
 	int			num_skew_mcvs;
 	// 哈希表桶数量的对数(log2)
 	int			log2_nbuckets;
-	// 哈希键数量
-	int			nkeys;
-	// 循环变量,用于遍历哈希操作符和排序规则
-	ListCell   *ho;
-	ListCell   *hc;
 	// 保存旧的内存上下文
 	MemoryContext oldcxt;
 
 	/*
@@ -487,7 +498,6 @@ ExecHashTableCreate(HashState *state, List *hashOperators, List *hashCollations,
 	// 设置哈希表桶数量的对数
 	hashtable->log2_nbuckets = log2_nbuckets;
 	// 设置初始最优桶数量的对数
 	hashtable->log2_nbuckets_optimal = log2_nbuckets;
 	// 初始化未共享的桶数组为空
 	hashtable->buckets.unshared = NULL;
-	// 设置是否保留 NULL 元组(由参数传入)
-	hashtable->keepNulls = keepNulls;
 	// 初始化倾斜优化为禁用
 	hashtable->skewEnabled = false;
 	// 初始化倾斜桶数组为空
 	hashtable->skewBucket = NULL;
 	// 初始化倾斜桶数组长度为 0
 	hashtable->skewBucketLen = 0;
@@ -540,32 +550,6 @@ ExecHashTableCreate(HashState *state, List *hashOperators, List *hashCollations,
 
 	// 切换到哈希表的专用内存上下文
 	oldcxt = MemoryContextSwitchTo(hashtable->hashCxt);
 
-	/*
-	 * 获取每个哈希键使用的哈希函数信息,并记录连接操作符是否严格
-	 */
-	// 计算哈希键数量
-	nkeys = list_length(hashOperators);
-	// 为外侧哈希函数分配内存
-	hashtable->outer_hashfunctions = palloc_array(FmgrInfo, nkeys);
-	// 为内侧哈希函数分配内存
-	hashtable->inner_hashfunctions = palloc_array(FmgrInfo, nkeys);
-	// 分配数组存储操作符是否严格
-	hashtable->hashStrict = palloc_array(bool, nkeys);
-	// 分配数组存储排序规则
-	hashtable->collations = palloc_array(Oid, nkeys);
-	// 初始化循环变量
-	i = 0;
-	// 同时遍历哈希操作符和排序规则
-	forboth(ho, hashOperators, hc, hashCollations)
-	{
-		// 获取当前哈希操作符的 OID
-		Oid			hashop = lfirst_oid(ho);
-		// 定义外侧和内侧哈希函数的 OID
-		Oid			left_hashfn;
-		Oid			right_hashfn;
-
-		// 获取哈希操作符对应的哈希函数,若失败则报错
-		if (!get_op_hash_functions(hashop, &left_hashfn, &right_hashfn))
-			elog(ERROR, "无法为哈希操作符 %u 找到哈希函数", hashop);
-		// 初始化外侧哈希函数信息
-		fmgr_info(left_hashfn, &hashtable->outer_hashfunctions[i]);
-		// 初始化内侧哈希函数信息
-		fmgr_info(right_hashfn, &hashtable->inner_hashfunctions[i]);
-		// 设置操作符是否严格
-		hashtable->hashStrict[i] = op_strict(hashop);
-		// 保存排序规则
-		hashtable->collations[i] = lfirst_oid(hc);
-		// 增加循环计数
-		i++;
-	}
-
 	// 如果批次数量大于 1 且未启用并行状态
 	if (nbatch > 1 && hashtable->parallel_state == NULL)
 	{
 		// 保存当前内存上下文
 		MemoryContext oldctx;
@@ -652,7 +636,7 @@ ExecHashTableCreate(HashState *state, List *hashOperators, List *hashCollations,
 		 * 如果批次数量大于 1,则构建倾斜哈希表
 		 */
 		if (nbatch > 1)
-			ExecHashBuildSkewHash(hashtable, node, num_skew_mcvs);
+			// 调用 ExecHashBuildSkewHash 构建倾斜哈希表,传入 state
+			ExecHashBuildSkewHash(state, hashtable, node, num_skew_mcvs);
 
 		// 恢复旧的内存上下文
 		MemoryContextSwitchTo(oldcxt);
 	}

修改 HashJoin 节点:使用新 ExprState 计算外侧哈希,并整合初始化(nodeHashjoin.c 和 hashjoin.h)

修改内容:

  • src/backend/executor/nodeHashjoin.c 中:
    • 修改 ExecInitHashJoin:在初始化内侧 HashState 后,构建外侧(outer)和内侧的 ExprState
      • 计算哈希函数 OID 和严格性数组。
      • 调用 ExecBuildHash32Expr 构建 hj_OuterHash(外侧)和 hashstate->hash_expr(内侧),传入适当的 keep_nulls(基于连接类型,如 HJ_FILL_OUTERHJ_FILL_INNER)。
      • 为倾斜表设置 skew_hashfunction
      • 移除旧的 hj_OuterHashKeyshj_HashOperatorshj_Collations 初始化。
    • 修改 ExecHashJoinImplExecHashJoinOuterGetTupleExecParallelHashJoinOuterGetTupleExecParallelHashJoinPartitionOuter:使用 ExecEvalExprSwitchContext(hjstate->hj_OuterHash, ...) 计算外侧哈希,替换旧的 ExecHashGetHashValue;添加 ResetExprContext
  • src/include/executor/hashjoin.h 中:移除 keepNullsouter_hashfunctions 等字段,因为哈希逻辑移到 ExprState

ExecInitHashJoin 函数

  ExecInitHashJoinPostgreSQL 数据库执行器中用于初始化哈希连接(Hash Join)节点的函数。哈希连接是一种常见的数据库查询执行策略,用于处理 SQL 中的 JOIN 操作(例如 INNER JOINLEFT JOIN 等)。该函数的主要任务是:

  1. 创建并初始化哈希连接的状态结构HashJoinState),为执行哈希连接操作做准备。
  2. 初始化子节点,包括外表outer)和内表inner,通常是哈希表)的计划节点。
  3. 设置表达式上下文,包括哈希键连接条件和其他资格条件(qual)的初始化。
  4. 优化哈希值计算通过 ExecBuildHash32Expr 为内外表的哈希键构建高效的表达式状态(ExprState),支持单次元组解构和 JIT 编译
  5. 处理连接类型,根据连接类型(INNERLEFTRIGHT 等)设置空元组槽(null tuple slot)以支持外连接。
  6. 初始化哈希表相关信息,为后续哈希表创建和元组插入做准备。

  该函数是哈希连接执行的入口点,确保所有必要的状态和资源在执行查询前都已正确配置。补丁优化了哈希键的处理方式,通过 ExecBuildHash32Expr 减少元组解构次数并支持 JIT 编译,从而提升性能。

代码逐段分析与中文注释

  以下是 ExecInitHashJoin 函数的代码,带有详细的中文注释,并附上每段的解读。

/* ----------------------------------------------------------------
 *		ExecInitHashJoin
 *
 *		哈希连接节点的初始化例程。
 *		为哈希连接操作创建状态结构并初始化所有相关资源,包括子节点、表达式上下文、哈希键和哈希表。
 * ----------------------------------------------------------------
 */
HashJoinState *
ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
{
    /* 定义哈希连接状态结构和其他局部变量 */
    HashJoinState *hjstate;  // 哈希连接状态
    Plan	   *outerNode;    // 外表计划节点
    Hash	   *hashNode;    // 内表(哈希表)计划节点
    TupleDesc	outerDesc,    // 外表元组描述符
                innerDesc;    // 内表元组描述符
    const TupleTableSlotOps *ops;  // 元组表槽操作

    /* 检查不支持的标志,确保不包含向后扫描或标记 */
    Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
  • 作用:函数开始定义了必要的变量,并检查执行标志(eflags)以确保不支持向后扫描(EXEC_FLAG_BACKWARD)或标记(EXEC_FLAG_MARK),因为哈希连接不支持这些操作。
  • 变量说明:
    • hjstate:哈希连接的状态结构,存储所有运行时信息。
    • outerNodehashNode:分别表示外表和内表(哈希表)的计划节点。
    • outerDescinnerDesc:元组描述符,描述内外表的结构。
    • ops:元组表槽的操作接口,用于处理元组。
  • 检查标志Assert 确保查询计划符合哈希连接的要求,避免不兼容的执行模式。

    /*
     * 创建哈希连接状态结构并设置基本属性
     */
    hjstate = makeNode(HashJoinState);  // 创建哈希连接状态节点
    hjstate->js.ps.plan = (Plan *) node;  // 设置计划节点
    hjstate->js.ps.state = estate;  // 设置执行状态
  • 作用:创建 HashJoinState 结构并初始化其基本属性。
  • 细节
    • makeNode(HashJoinState):分配并初始化 HashJoinState 节点。
    • hjstate->js.ps.plan:将输入的哈希连接计划节点(HashJoin)关联到状态结构。
    • hjstate->js.ps.state:设置执行状态(EState),提供查询执行的全局上下文。

    /*
     * 设置执行函数,可能被并行版本替换
     * 如果启用了并行查询,ExecHashJoinInitializeDSM 和 ExecHashJoinInitializeWorker 可能会替换默认执行函数
     */
    hjstate->js.ps.ExecProcNode = ExecHashJoin;  // 设置默认执行函数
    hjstate->js.jointype = node->join.jointype;  // 设置连接类型(如 INNER、LEFT 等)
  • 作用:设置哈希连接的执行函数和连接类型。
  • 细节
    • hjstate->js.ps.ExecProcNode:指定默认的执行函数为 ExecHashJoin,但在并行查询中可能被替换为并行版本(如 ExecHashJoinInitializeDSM)。
    • hjstate->js.jointype:记录连接类型(INNERLEFTRIGHT 等),决定后续的元组处理逻辑(例如是否需要处理 NULL 元组)。

    /*
     * 初始化表达式上下文
     * 为哈希连接节点创建表达式上下文,用于评估连接条件和哈希键
     */
    ExecAssignExprContext(estate, &hjstate->js.ps);
  • 作用:为哈希连接节点分配表达式上下文(ExprContext)。
  • 细节
    • ExecAssignExprContext:创建并初始化表达式上下文,用于存储运行时表达式评估的状态(如连接条件、哈希键表达式等)。
    • 表达式上下文在后续的哈希值计算和条件检查中非常关键。

    /*
     * 初始化子节点
     * 初始化外表和内表(哈希表)的计划节点,并获取其元组描述符
     * 注意:可以选择禁用内表的 REWIND 标志以优化单批次哈希,但目前未实现
     */
    outerNode = outerPlan(node);  // 获取外表计划节点
    hashNode = (Hash *) innerPlan(node);  // 获取内表(哈希表)计划节点

    outerPlanState(hjstate) = ExecInitNode(outerNode, estate, eflags);  // 初始化外表节点
    outerDesc = ExecGetResultType(outerPlanState(hjstate));  // 获取外表元组描述符
    innerPlanState(hjstate) = ExecInitNode((Plan *) hashNode, estate, eflags);  // 初始化内表节点
    innerDesc = ExecGetResultType(innerPlanState(hjstate));  // 获取内表元组描述符
  • 作用:初始化外表和内表的计划节点,并获取它们的元组描述符。
  • 细节
    • outerPlaninnerPlan:从哈希连接计划节点中提取外表和内表(哈希表)的子计划。
    • ExecInitNode:递归初始化子计划节点,返回对应的状态结构(PlanState)。
    • ExecGetResultType:获取内外表的元组描述符,描述元组的结构(如列数、类型等)。
    • 注释中的优化点:提到可以禁用内表的 REWIND 标志以优化单批次哈希,但目前未实现,因为不确定是否总能带来性能提升。

    /*
     * 初始化结果元组槽、类型和投影信息
     */
    ExecInitResultTupleSlotTL(&hjstate->js.ps, &TTSOpsVirtual);  // 初始化结果元组槽
    ExecAssignProjectionInfo(&hjstate->js.ps, NULL);  // 设置投影信息
  • 作用:初始化哈希连接的输出结果元组槽和投影信息。
  • 细节
    • ExecInitResultTupleSlotTL:为哈希连接的输出结果分配元组槽,使用虚拟元组表槽操作(TTSOpsVirtual)。
    • ExecAssignProjectionInfo:设置投影信息,用于从内外表的元组中提取需要的列,生成最终的输出元组。

    /*
     * 初始化元组表槽
     * 为外表创建额外的元组槽,用于存储外表元组
     */
    ops = ExecGetResultSlotOps(outerPlanState(hjstate), NULL);  // 获取外表元组槽操作
    hjstate->hj_OuterTupleSlot = ExecInitExtraTupleSlot(estate, outerDesc, ops);  // 初始化外表元组槽
  • 作用:为外表元组分配额外的元组槽,用于在哈希连接过程中存储外表元组。
  • 细节
    • ExecGetResultSlotOps:获取外表计划节点的元组槽操作接口。
    • ExecInitExtraTupleSlot:为外表创建一个额外的元组槽,基于外表的元组描述符和操作接口,用于临时存储外表元组。

    /*
     * 检测是否只需考虑第一个匹配的内表元组
     * 根据连接类型或 inner_unique 属性设置 single_match 标志
     */
    hjstate->js.single_match = (node->join.inner_unique || node->join.jointype == JOIN_SEMI);
  • 作用:确定是否只需要考虑内表的第一个匹配元组。
  • 细节
    • node->join.inner_unique:表示内表元组是否唯一(由查询优化器确定)。
    • node->join.jointype == JOIN_SEMI:半连接(SEMI JOIN)只需要返回外表元组是否匹配内表,不需要所有匹配的内表元组。
    • single_match:如果为 true,哈希连接只需处理第一个匹配的内表元组,优化性能。

    /*
     * 为外连接设置空元组槽
     * 根据连接类型(如 LEFT、RIGHT、FULL)初始化空元组槽,用于处理无匹配的情况
     */
    switch (node->join.jointype)
    {
        case JOIN_INNER:
        case JOIN_SEMI:
        case JOIN_RIGHT_SEMI:
            break;  // 内连接和半连接无需空元组槽
        case JOIN_LEFT:
        case JOIN_ANTI:
            hjstate->hj_NullInnerTupleSlot =
                ExecInitNullTupleSlot(estate, innerDesc, &TTSOpsVirtual);  // 为左连接和反连接设置内表空元组槽
            break;
        case JOIN_RIGHT:
        case JOIN_RIGHT_ANTI:
            hjstate->hj_NullOuterTupleSlot =
                ExecInitNullTupleSlot(estate, outerDesc, &TTSOpsVirtual);  // 为右连接和右反连接设置外表空元组槽
            break;
        case JOIN_FULL:
            hjstate->hj_NullOuterTupleSlot =
                ExecInitNullTupleSlot(estate, outerDesc, &TTSOpsVirtual);  // 为全连接设置外表空元组槽
            hjstate->hj_NullInnerTupleSlot =
                ExecInitNullTupleSlot(estate, innerDesc, &TTSOpsVirtual);  // 为全连接设置内表空元组槽
            break;
        default:
            elog(ERROR, "无法识别的连接类型: %d", (int) node->join.jointype);  // 抛出错误,处理未知连接类型
    }
  • 作用:根据连接类型(jointype)为外连接初始化空元组槽,用于处理无匹配的元组。
  • 细节
    • JOIN_INNER JOIN_SEMI:不需要空元组槽,因为无匹配的元组不会出现在结果中。
    • JOIN_LEFT JOIN_ANTI:为内表分配空元组槽(hj_NullInnerTupleSlot),用于左连接中外表元组无匹配内表元组的情况。
    • JOIN_RIGHT JOIN_RIGHT_ANTI:为外表分配空元组槽(hj_NullOuterTupleSlot)。
    • JOIN_FULL:同时为内外表分配空元组槽。
    • ExecInitNullTupleSlot:创建表示 NULL 的元组槽,用于填充无匹配的列。
    • 如果遇到未知的连接类型,抛出错误。

    /*
     * 设置哈希表元组槽
     * 使用内表(哈希节点)的结果元组槽作为哈希表的元组槽
     * 这是因为哈希节点通过 ExecScanHashBucket 获取元组,而不是通过 ExecProcNode
     */
    {
        HashState  *hashstate = (HashState *) innerPlanState(hjstate);  // 获取内表状态
        Hash	   *hash = (Hash *) hashstate->ps.plan;  // 获取内表计划节点
        TupleTableSlot *slot = hashstate->ps.ps_ResultTupleSlot;  // 获取内表结果元组槽
        Oid		   *outer_hashfuncid;  // 外表哈希函数 ID 数组
        Oid		   *inner_hashfuncid;  // 内表哈希函数 ID 数组
        bool	   *hash_strict;  // 哈希操作符是否为严格模式的数组
        ListCell   *lc;  // 遍历哈希操作符的列表单元
        int			nkeys;  // 哈希键数量

        hjstate->hj_HashTupleSlot = slot;  // 设置哈希表元组槽
  • 作用:将内表(哈希节点)的结果元组槽设置为哈希连接的哈希表元组槽。
  • 细节
    • 哈希节点(Hash)不通过 ExecProcNode 返回元组,而是通过 ExecScanHashBucket 从哈希表中获取元组,因此可以直接复用其结果元组槽(hj_HashTupleSlot)。
    • 这是一种优化,避免为哈希表额外分配元组槽,减少内存开销。
    • 定义了局部变量(如 outer_hashfuncidinner_hashfuncid 等),为后续哈希键初始化做准备。

        /*
         * 构建内外表的哈希值表达式状态
         * 在此构建 ExprState 以生成哈希值,因为 ExecBuildHash32Expr 需要知道连接类型来处理 NULL 值
         * 必须在 ExecHashTableCreate 之前构建,以正确归因哈希表达式中的子计划
         */
        nkeys = list_length(node->hashoperators);  // 获取哈希操作符数量(即连接键数量)

        outer_hashfuncid = palloc_array(Oid, nkeys);  // 分配外表哈希函数 ID 数组
        inner_hashfuncid = palloc_array(Oid, nkeys);  // 分配内表哈希函数 ID 数组
        hash_strict = palloc_array(bool, nkeys);  // 分配严格模式标志数组

        /*
         * 确定每个哈希操作符的内外表哈希函数
         */
        foreach(lc, node->hashoperators)
        {
            Oid			hashop = lfirst_oid(lc);  // 获取当前哈希操作符 ID
            int			i = foreach_current_index(lc);  // 获取当前索引

            if (!get_op_hash_functions(hashop, &outer_hashfuncid[i], &inner_hashfuncid[i]))
                elog(ERROR, "无法为哈希操作符 %u 找到哈希函数", hashop);  // 找不到哈希函数则抛出错误
            hash_strict[i] = op_strict(hashop);  // 设置操作符是否为严格模式
        }
  • 作用:为内外表的哈希键分配哈希函数,并确定操作符是否为严格模式。
  • 细节
    • list_length(node->hashoperators):获取连接键的数量(nkeys)。
    • palloc_array:为哈希函数 ID 和严格模式标志分配数组。
    • foreach 循环遍历 hashoperators(哈希操作符列表),为每个操作符调用 get_op_hash_functions,获取对应的外表和内表哈希函数 ID
    • op_strict(hashop):检查操作符是否为严格模式(严格模式下,NULL 输入会导致整个表达式返回 NULL)。
    • 如果无法找到哈希函数,抛出错误。

        /*
         * 为外表的哈希键构建 ExprState
         * 如果 HJ_FILL_OUTER 为 true,则生成完整的哈希值;否则,遇到 NULL 提前终止
         */
        hjstate->hj_OuterHash =
            ExecBuildHash32Expr(hjstate->js.ps.ps_ResultTupleDesc,  // 外表元组描述符
                                hjstate->js.ps.resultops,  // 外表元组槽操作
                                outer_hashfuncid,  // 外表哈希函数 ID
                                node->hashcollations,  // 哈希键的排序规则
                                node->hashkeys,  // 外表哈希键表达式
                                hash_strict,  // 严格模式标志
                                &hjstate->js.ps,  // 父计划状态
                                0,  // 初始哈希值
                                HJ_FILL_OUTER(hjstate));  // 是否保留外表 NULL 元组
  • 作用:为外表的哈希键构建 ExprState,用于高效计算哈希值。
  • 细节
    • ExecBuildHash32Expr补丁引入的新函数,创建 ExprState 来计算哈希值,支持单次元组解构和 JIT 编译
    • 参数说明:
      • hjstate->js.ps.ps_ResultTupleDescresultops:提供外表的元组描述符和操作接口。
      • outer_hashfuncidnode->hashcollations:指定哈希函数和排序规则。
      • node->hashkeys:外表的哈希键表达式。
      • hash_strict:指示哪些哈希函数是严格模式的。
      • &hjstate->js.ps:父计划状态,用于关联子计划。
      • 0:初始哈希值,通常为 0
      • HJ_FILL_OUTER(hjstate):根据连接类型(如 LEFT JOIN)决定是否保留外表的 NULL 元组(true 表示保留)。
    • 这一步优化了哈希值计算,减少元组解构次数,并支持 JIT 编译。

        /*
         * 为内表的哈希键构建 ExprState
         * 与外表类似,但使用内表的元组描述符和哈希键
         */
        hashstate->hash_expr =
            ExecBuildHash32Expr(hashstate->ps.ps_ResultTupleDesc,  // 内表元组描述符
                                hashstate->ps.resultops,  // 内表元组槽操作
                                inner_hashfuncid,  // 内表哈希函数 ID
                                node->hashcollations,  // 哈希键的排序规则
                                hash->hashkeys,  // 内表哈希键表达式
                                hash_strict,  // 严格模式标志
                                &hashstate->ps,  // 父计划状态
                                0,  // 初始哈希值
                                HJ_FILL_INNER(hjstate));  // 是否保留内表 NULL 元组
  • 作用:为内表(哈希表)的哈希键构建 ExprState,与外表类似。
  • 细节
    • 与外表哈希键的初始化类似,但使用内表的元组描述符(hashstate->ps.ps_ResultTupleDesc)、操作接口(resultops)和哈希键表达式(hash->hashkeys)。
    • HJ_FILL_INNER(hjstate):根据连接类型(如 RIGHT JOIN)决定是否保留内表的 NULL 元组。
    • 这一步确保内表元组的哈希值计算也受益于补丁的优化(如单次元组解构和 JIT 支持)。

        /*
         * 设置倾斜表哈希函数
         * 如果启用了倾斜优化(skew optimization),为第一个哈希键设置哈希函数和排序规则
         */
        if (OidIsValid(hash->skewTable))
        {
            hashstate->skew_hashfunction = palloc0(sizeof(FmgrInfo));  // 分配哈希函数信息
            hashstate->skew_collation = linitial_oid(node->hashcollations);  // 获取第一个哈希键的排序规则
            fmgr_info(outer_hashfuncid[0], hashstate->skew_hashfunction);  // 设置倾斜表哈希函数
        }
  • 作用:为倾斜表(skew table)初始化哈希函数,用于优化高频值(MCV,most common values)的处理。
  • 细节
    • hash->skewTable:如果启用了倾斜优化(针对高频值的特殊处理),则为第一个哈希键设置哈希函数。
    • palloc0(sizeof(FmgrInfo)):分配哈希函数信息结构。
    • linitial_oid(node->hashcollations):获取第一个哈希键的排序规则。
    • fmgr_info:初始化哈希函数信息,使用外表的第一个哈希函数 ID(outer_hashfuncid[0])
    • 倾斜优化可以减少高频值导致的哈希冲突,提高性能。

        /*
         * 释放临时分配的内存
         */
        pfree(outer_hashfuncid);  // 释放外表哈希函数 ID 数组
        pfree(inner_hashfuncid);  // 释放内表哈希函数 ID 数组
        pfree(hash_strict);  // 释放严格模式标志数组
    }
  • 作用:释放为哈希函数和严格模式标志分配的临时内存。
  • 细节
    • pfree:释放之前通过 palloc_array 分配的数组(outer_hashfuncidinner_hashfuncidhash_strict)。
    • 这些数组仅在初始化哈希键表达式时使用,完成后释放以避免内存泄漏。

    /*
     * 初始化子表达式
     * 初始化连接条件、额外资格条件和哈希子句
     */
    hjstate->js.ps.qual =
        ExecInitQual(node->join.plan.qual, (PlanState *) hjstate);  // 初始化计划资格条件
    hjstate->js.joinqual =
        ExecInitQual(node->join.joinqual, (PlanState *) hjstate);  // 初始化连接资格条件
    hjstate->hashclauses =
        ExecInitQual(node->hashclauses, (PlanState *) hjstate);  // 初始化哈希子句
  • 作用:初始化哈希连接的资格条件(qual)和哈希子句。
  • 细节
    • ExecInitQual:初始化表达式列表,生成对应的 ExprState
    • node->join.plan.qual:计划的资格条件(如 WHERE 子句)。
    • node->join.joinqual:连接的额外资格条件(如 ON 子句中的额外条件)。
    • node->hashclauses:哈希连接的子句(通常是 ON 子句中的等值条件,如 a.customer_id = b.customer_id)。
    • 这些条件将在执行时用于过滤元组。

    /*
     * 初始化哈希表相关信息
     * 设置初始状态,准备后续哈希表创建和元组处理
     */
    hjstate->hj_HashTable = NULL;  // 哈希表尚未创建
    hjstate->hj_FirstOuterTupleSlot = NULL;  // 第一个外表元组槽为空

    hjstate->hj_CurHashValue = 0;  // 当前哈希值初始化为 0
    hjstate->hj_CurBucketNo = 0;  // 当前桶编号初始化为 0
    hjstate->hj_CurSkewBucketNo = INVALID_SKEW_BUCKET_NO;  // 当前倾斜桶编号初始化为无效
    hjstate->hj_CurTuple = NULL;  // 当前元组指针初始化为空

    hjstate->hj_JoinState = HJ_BUILD_HASHTABLE;  // 设置初始连接状态为构建哈希表
    hjstate->hj_MatchedOuter = false;  // 外表元组尚未匹配
    hjstate->hj_OuterNotEmpty = false;  // 外表尚未确认非空
  • 作用:初始化哈希表和相关状态变量,为后续执行做准备。
  • 细节
    • hj_HashTable:哈希表指针初始化为 NULL,将在 ExecHashTableCreate 中创建。
    • hj_FirstOuterTupleSlot:用于存储第一个外表元组槽,初始化为 NULL
    • hj_CurHashValuehj_CurBucketNo 等:初始化哈希值、桶编号等运行时状态。
    • hj_JoinState:设置为 HJ_BUILD_HASHTABLE,表示执行的第一个阶段是构建哈希表。
    • hj_MatchedOuterhj_OuterNotEmpty:跟踪外表元组的匹配状态和是否非空,用于优化重新扫描(rescan)。

    /*
     * 返回初始化的哈希连接状态
     */
    return hjstate;
}
  • 作用:返回完全初始化的 HashJoinState 结构,供后续执行使用。
  • 细节
    • hjstate 包含所有初始化好的状态,包括子节点、表达式、元组槽和哈希表相关信息。
    • 该结构将在执行阶段(如 ExecHashJoin)中使用,驱动哈希连接的实际操作。

添加 JIT 支持:LLVM 编译新操作码(llvmjit_expr.c)

修改内容:

  • src/backend/jit/llvm/llvmjit_expr.cllvm_compile_expr 中,添加对新操作码的 LLVM IR 生成:
    • EEOP_HASHDATUM_SET_INITVAL:存储初始值。
    • EEOP_HASHDATUM_FIRST*NEXT32*:生成 NULL 检查、旋转、函数调用(BuildV1Call)、异或和跳转的 IR 代码。
      处理严格/非严格模式下的 NULL 行为。

llvm_compile_expr 函数

@@ -1900,6 +1900,210 @@ llvm_compile_expr(ExprState *state)
				// 跳转到下一个操作块
				LLVMBuildBr(b, opblocks[opno + 1]);
				break;

+			// 处理 EEOP_HASHDATUM_SET_INITVAL 操作码:生成设置初始哈希值的 LLVM IR
+			case EEOP_HASHDATUM_SET_INITVAL:
+				{
+					// 定义初始值变量
+					LLVMValueRef v_initvalue;
+
+					// 将初始值转换为 LLVM 常量(size_t 类型)
+					v_initvalue = l_sizet_const(op->d.hashdatum_initvalue.init_value);
+
+					// 将初始值存储到结果值指针(resvaluep)
+					LLVMBuildStore(b, v_initvalue, v_resvaluep);
+					// 设置结果非 NULL(存储 false 到 resnullp)
+					LLVMBuildStore(b, l_sbool_const(0), v_resnullp);
+					// 跳转到下一个操作块
+					LLVMBuildBr(b, opblocks[opno + 1]);
+					break;
+				}
+
+			// 处理哈希相关操作码:EEOP_HASHDATUM_FIRST、FIRST_STRICT、NEXT32、NEXT32_STRICT
+			case EEOP_HASHDATUM_FIRST:
+			case EEOP_HASHDATUM_FIRST_STRICT:
+			case EEOP_HASHDATUM_NEXT32:
+			case EEOP_HASHDATUM_NEXT32_STRICT:
+				{
+					// 获取哈希函数的调用信息
+					FunctionCallInfo fcinfo = op->d.hashdatum.fcinfo_data;
+					// 定义 LLVM 变量:函数调用信息
+					LLVMValueRef v_fcinfo;
+					// 定义函数调用是否返回 NULL 的标志
+					LLVMValueRef v_fcinfo_isnull;
+					// 定义哈希函数返回值
+					LLVMValueRef v_retval;
+					// 定义检查输入是否 NULL 的代码块
+					LLVMBasicBlockRef b_checkargnull;
+					// 定义输入非 NULL 时的代码块
+					LLVMBasicBlockRef b_ifnotnull;
+					// 定义输入为 NULL 时的代码块
+					LLVMBasicBlockRef b_ifnullblock;
+					// 定义输入是否 NULL 的标志
+					LLVMValueRef v_argisnull;
+					// 定义前一个哈希值,初始为空
+					LLVMValueRef v_prevhash = NULL;
+
+					/*
+					 * 对于 NEXT32(非严格模式),在检查 NULL 之前先旋转前一个哈希值,
+					 * 即使输入是 NULL 也要这样做。在严格模式下,旋转在 NULL 检查之后,
+					 * 以避免在返回 NULL 时浪费旋转操作。
+					 */
+					if (opcode == EEOP_HASHDATUM_NEXT32)
+					{
+						// 定义临时变量用于旋转计算
+						LLVMValueRef v_tmp1;
+						LLVMValueRef v_tmp2;
+
+						/*
+						 * 从 EEOP_HASHDATUM_FIRST 操作存储的位置加载前一个哈希值
+						 */
+						v_prevhash = l_load(b, TypeSizeT, v_resvaluep,
+											"prevhash");
+
+						/*
+						 * 将哈希值左移 1 位,注意避免 uint32 在 size_t 中的溢出
+						 */
+						v_tmp1 = LLVMBuildShl(b, v_prevhash, l_sizet_const(1),
+											  "");
+						// 使用掩码确保结果在 uint32 范围内
+						v_tmp1 = LLVMBuildAnd(b, v_tmp1,
+											  l_sizet_const(0xffffffff), "");
+						// 将原始值右移 31 位,获取最高位
+						v_tmp2 = LLVMBuildLShr(b, v_prevhash,
+											   l_sizet_const(31), "");
+						// 合并左移和右移结果,完成 1 位左旋转
+						v_prevhash = LLVMBuildOr(b, v_tmp1, v_tmp2,
+												 "rotatedhash");
+					}
+
+					/*
+					 * 定义实际调用哈希函数的代码块(输入非 NULL 时)
+					 */
+					b_ifnotnull = l_bb_before_v(opblocks[opno + 1],
+												"b.%d.ifnotnull",
+												opno);
+
+					// 确保哈希函数只有一个参数,否则报错
+					if (fcinfo->nargs != 1)
+						elog(ERROR, "哈希函数参数数量错误");
+
+					// 将函数调用信息转换为 LLVM 常量指针
+					v_fcinfo = l_ptr_const(fcinfo,
+										   l_ptr(StructFunctionCallInfoData));
+
+					// 定义检查输入参数是否 NULL 的代码块
+					b_checkargnull = l_bb_before_v(b_ifnotnull,
+												   "b.%d.isnull.0", opno);
+
+					// 跳转到 NULL 检查代码块
+					LLVMBuildBr(b, b_checkargnull);
+
+					/*
+					 * 根据操作码决定输入为 NULL 时的行为
+					 */
+					if (opcode == EEOP_HASHDATUM_FIRST_STRICT ||
+						opcode == EEOP_HASHDATUM_NEXT32_STRICT)
+					{
+						// 定义严格模式下输入为 NULL 的代码块
+						b_ifnullblock = l_bb_before_v(b_ifnotnull,
+													  "b.%d.strictnull",
+													  opno);
+
+						// 设置代码生成位置到 NULL 处理块
+						LLVMPositionBuilderAtEnd(b, b_ifnullblock);
+
+						/*
+						 * 在严格模式下,输入为 NULL 时返回 NULL,
+						 * 保存 NULL 结果并跳转到 jumpdone
+						 */
+						LLVMBuildStore(b, l_sbool_const(1), v_resnullp);
+						LLVMBuildStore(b, l_sizet_const(0), v_resvaluep);
+						LLVMBuildBr(b, opblocks[op->d.hashdatum.jumpdone]);
+					}
+					else
+					{
+						// 定义非严格模式下输入为 NULL 的代码块
+						b_ifnullblock = l_bb_before_v(b_ifnotnull,
+													  "b.%d.null",
+													  opno);
+
+						// 设置代码生成位置到 NULL 处理块
+						LLVMPositionBuilderAtEnd(b, b_ifnullblock);
+
+						// 设置结果非 NULL
+						LLVMBuildStore(b, l_sbool_const(0), v_resnullp);
+
+						if (opcode == EEOP_HASHDATUM_NEXT32)
+						{
+							// 断言:确保 v_prevhash 已初始化
+							Assert(v_prevhash != NULL);
+
+							/*
+							 * 保存旋转后的哈希值,并跳转到下一个操作
+							 */
+							LLVMBuildStore(b, v_prevhash, v_resvaluep);
+						}
+						else
+						{
+							// 断言:确保操作码是 EEOP_HASHDATUM_FIRST
+							Assert(opcode == EEOP_HASHDATUM_FIRST);
+
+							/*
+							 * 当输入为 NULL 时,存储 0 作为哈希值
+							 */
+							LLVMBuildStore(b, l_sizet_const(0), v_resvaluep);
+						}
+
+						// 跳转到下一个操作块
+						LLVMBuildBr(b, opblocks[opno + 1]);
+					}
+
+					// 设置代码生成位置到 NULL 检查块
+					LLVMPositionBuilderAtEnd(b, b_checkargnull);
+
+					// 生成检查输入参数是否 NULL 的代码
+					v_argisnull = l_funcnull(b, v_fcinfo, 0);
+					// 生成条件跳转:如果输入为 NULL 跳转到 b_ifnullblock,否则到 b_ifnotnull
+					LLVMBuildCondBr(b,
+									LLVMBuildICmp(b,
+												  LLVMIntEQ,
+												  v_argisnull,
+												  l_sbool_const(1),
+												  ""),
+									b_ifnullblock,
+									b_ifnotnull);
+
+					// 设置代码生成位置到非 NULL 处理块
+					LLVMPositionBuilderAtEnd(b, b_ifnotnull);
+
+					/*
+					 * 对于 NEXT32_STRICT,在非 NULL 时旋转前一个哈希值;
+					 * 非严格模式下已在 NULL 检查前完成旋转
+					 */
+					if (opcode == EEOP_HASHDATUM_NEXT32_STRICT)
+					{
+						// 定义临时变量用于旋转计算
+						LLVMValueRef v_tmp1;
+						LLVMValueRef v_tmp2;
+
+						/*
+						 * 从 EEOP_HASHDATUM_FIRST_STRICT 操作存储的位置加载前一个哈希值
+						 */
+						v_prevhash = l_load(b, TypeSizeT, v_resvaluep,
+											"prevhash");
+
+						/*
+						 * 将哈希值左移 1 位,注意避免 uint32 在 size_t 中的溢出
+						 */
+						v_tmp1 = LLVMBuildShl(b, v_prevhash, l_sizet_const(1),
+											  "");
+						// 使用掩码确保结果在 uint32 范围内
+						v_tmp1 = LLVMBuildAnd(b, v_tmp1,
+											  l_sizet_const(0xffffffff), "");
+						// 将原始值右移 31 位,获取最高位
+						v_tmp2 = LLVMBuildLShr(b, v_prevhash,
+											   l_sizet_const(31), "");
+						// 合并左移和右移结果,完成 1 位左旋转
+						v_prevhash = LLVMBuildOr(b, v_tmp1, v_tmp2,
+												 "rotatedhash");
+					}
+
+					// 调用哈希函数,生成返回值
+					v_retval = BuildV1Call(context, b, mod, fcinfo,
+										   &v_fcinfo_isnull);
+
+					/*
+					 * 对于 NEXT32 或 NEXT32_STRICT 操作,将返回的哈希值与前一个哈希值异或
+					 */
+					if (opcode == EEOP_HASHDATUM_NEXT32 ||
+						opcode == EEOP_HASHDATUM_NEXT32_STRICT)
+						v_retval = LLVMBuildXor(b, v_prevhash, v_retval,
+												"xorhash");
+
+					// 将最终哈希值存储到结果值指针
+					LLVMBuildStore(b, v_retval, v_resvaluep);
+					// 设置结果非 NULL
+					LLVMBuildStore(b, l_sbool_const(0), v_resnullp);
+
+					// 跳转到下一个操作块
+					LLVMBuildBr(b, opblocks[opno + 1]);
+					break;
+				}
+
 			case EEOP_CONVERT_ROWTYPE:
 				// 调用 ExecEvalConvertRowtype 处理行类型转换
 				build_EvalXFunc(b, mod, "ExecEvalConvertRowtype",
 								v_state, op, v_econtext);

总结

  这个 PostgreSQL 优化 Patch 针对哈希连接(Hash Join)引入了新的 ExprState 支持,通过 ExecBuildHash32Expr 函数一次性计算多个连接键的 32 位哈希值,显著减少了元组变形(tuple deformation)和重复表达式评估的开销。
  Patch 修改了 execExpr.cexecExprInterp.c 添加哈希操作码支持,更新了 nodeHash.cnodeHashjoin.c 使用新的 hash_expr 机制替换旧的 hashkeys,并通过 llvmjit_expr.c 提供 JIT 编译支持,进一步提升性能。关键优化包括:统一内外侧哈希计算、优化 NULL 处理、延迟初始化以适配连接类型,以及移除冗余哈希函数数组。测试显示性能提升高达 20%(无 JIT)至 26%(有 JIT),尤其在多键连接和小哈希表场景下效果显著,同时为其他哈希操作(如 Hash Aggregate)奠定了基础。


网站公告

今日签到

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