当前位置: 首页 > article >正文

【PostgreSQL内核学习 —— (WindowAgg(三))】

WindowAgg

  • set_subquery_pathlist 部分函数解读
  • check_and_push_window_quals 函数
  • find_window_run_conditions 函数
    • 执行案例
    • 总结
  • 计划器模块(set_plan_refs函数)
  • set_windowagg_runcondition_references 函数
    • 执行案例
  • fix_windowagg_condition_expr 函数
  • fix_windowagg_condition_expr_mutator 函数
    • 执行案例

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

  在【PostgreSQL内核学习 —— (WindowAgg(一))】中,我们介绍了窗口函数以及窗口聚合的核心计算过程。其次,在【PostgreSQL内核学习 —— (WindowAgg(二))】一文中介绍了WindowAgg算子的具体实现逻辑。本文将进一步来学习一下WindowAgg中的条件下推的优化逻辑。

set_subquery_pathlist 部分函数解读

/*
 * 如果子查询关系上有附加的限制条件,考虑将它们推送到子查询中,作为子查询的 WHERE 或 HAVING 过滤条件。
 * 这种转化很有用,因为它可能帮助我们为子查询生成更好的执行计划,避免首先评估所有子查询的输出行再过滤。
 *
 * 有几种情况是不能推送限制条件的。涉及子查询的限制条件由 subquery_is_pushdown_safe() 检查。
 * 对单个条件的检查由 qual_is_pushdown_safe() 进行。此外,我们不希望推送伪常量(pseudoconstant)条件,
 * 这种情况下,最好将控制节点放在子查询上方。
 *
 * 未被推送下去的条件会作为 SubqueryScan 节点的 qpquals 被评估。
 *
 * XXX 是否有一些情况我们应该决定不推送可推送的条件,因为它可能导致更差的执行计划?
 */
if (rel->baserestrictinfo != NIL && // 如果有限制条件并且可以安全推送
    subquery_is_pushdown_safe(subquery, subquery, &safetyInfo)) // 检查子查询是否支持限制条件的推送
{
    // 可以考虑推送单个条件
    List *upperrestrictlist = NIL; // 存储未推送的限制条件
    ListCell *l;

    // 遍历所有的限制条件
    foreach(l, rel->baserestrictinfo)
    {
        RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); // 当前限制条件
        Node *clause = (Node *) rinfo->clause; // 限制条件的表达式

        if (rinfo->pseudoconstant) // 如果是伪常量条件
        {
            upperrestrictlist = lappend(upperrestrictlist, rinfo); // 不推送,保留在父查询中
            continue;
        }

        // 检查当前条件是否可以推送到子查询中
        switch (qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo))
        {
            case PUSHDOWN_SAFE:
                // 条件安全可以推送
                subquery_push_qual(subquery, rte, rti, clause); // 将条件推送到子查询
                break;

            case PUSHDOWN_WINDOWCLAUSE_RUNCOND:
                /*
                 * 如果条件涉及窗口函数,并且不能推送到子查询中,
                 * 检查条件是否可以作为 WindowAgg 的运行条件。
                 */
                if (!subquery->hasWindowFuncs || // 子查询没有窗口函数,或者该条件不是合适的窗口运行条件
                    check_and_push_window_quals(subquery, rte, rti, clause, &run_cond_attrs))
                {
                    // 子查询没有窗口函数,或者条件不适合窗口运行条件,或者该条件是合适的窗口条件,但需要保留在上层查询
                    upperrestrictlist = lappend(upperrestrictlist, rinfo);
                }
                break;

            case PUSHDOWN_UNSAFE:
                // 条件不安全,不能推送
                upperrestrictlist = lappend(upperrestrictlist, rinfo); // 保留在父查询中
                break;
        }
    }

    // 更新查询的限制条件
    rel->baserestrictinfo = upperrestrictlist;
    /* 不必重新计算 baserestrict_min_security */
}

代码逻辑总结:

  这段代码的核心任务是评估是否能够将某些限制条件推送到子查询中,以便优化执行计划。推送的条件会直接成为子查询的过滤条件,从而避免父查询在处理子查询结果时进行重复过滤。具体流程如下:

  1. 判断是否可以推送条件:

    • 先检查子查询是否允许推送限制条件(subquery_is_pushdown_safe)。
    • 如果子查询允许推送,则会遍历所有附加在外部查询的限制条件(rel->baserestrictinfo)。
  2. 处理每个限制条件:

    • 如果限制条件是伪常量,它就不能被推送下去,因为它不依赖于实际数据,可以直接在外层查询中处理。
    • 对于其他条件,使用 qual_is_pushdown_safe 来判断该条件是否可以安全地推送到子查询中。
  3. 推送限制条件:

    • 如果条件安全推送,使用 subquery_push_qual 将条件推送到子查询。
    • 如果条件涉及窗口函数并且不能推送,它可能会被保留在外层查询中(例如作为窗口聚合的运行条件)。
    • 对于无法推送的条件,它们将被保留在父查询中。

  我们重点聚焦函数 check_and_push_window_quals,该函数在查询优化中起着至关重要的作用,特别是在涉及窗口函数的情况下。为了更好地理解这个函数的作用和它在整个查询优化中的地位,下面将从它的功能、参数、调用背景和优化目标等方面进行详细分析。

check_and_push_window_quals 函数

  check_and_push_window_quals 函数的作用是在查询优化过程中判断某个过滤条件(clause)是否可以下推到窗口聚合节点(WindowAgg)的运行条件(runCondition。如果条件可以下推,执行时可以跳过一些不必要的工作,进而优化查询的性能。
  check_and_push_window_quals 函数检查给定的条件是否可以作为窗口函数的“运行条件”(runCondition)被下推至子查询中的窗口聚合节点。如果可以下推,那么这些条件会被推到窗口聚合的执行过程中,用来优化执行过程,避免不必要的计算。
  具体来说,它首先确认条件是一个操作符表达式OpExpr)且符合一定的条件,例如操作符必须是严格的(即在条件成立时可以安全地停止评估窗口函数)。然后,代码会检查操作符的左右两边是否涉及到子查询中的窗口函数,如果涉及到,就会尝试通过find_window_run_conditions函数检查该条件是否适合作为运行条件。最终,如果条件符合要求,它会将该条件下推并返回是否保留原始条件。如果条件不适合下推,或者无法找到合适的窗口函数,代码会保留原始条件,确保执行计划的正确性。函数源码如下所示:(路径:postgresql-15.10\src\backend\optimizer\path\allpaths.c

参数作用总结

  • subquery:提供关于子查询的详细信息,尤其是目标列信息,帮助解析窗口函数。
  • rte:表示当前的表项,帮助确定条件是否可以与窗口函数关联。
  • rti:用于标识rte在子查询中的位置,帮助定位表项。
  • clause:是需要检查是否可以下推的过滤条件,通常是操作符表达式。
  • run_cond_attrs:记录下推成功的窗口函数条件的目标列索引,优化窗口聚合操作。
static bool
check_and_push_window_quals(Query *subquery, RangeTblEntry *rte, Index rti,
							Node *clause, Bitmapset **run_cond_attrs)
{
	OpExpr	   *opexpr = (OpExpr *) clause;  // 将输入的条件转换为操作符表达式 (OpExpr)
	bool		keep_original = true;  // 标记是否保留原始的过滤条件
	Var		   *var1;  // 定义变量,用于存储操作符表达式左侧的变量
	Var		   *var2;  // 定义变量,用于存储操作符表达式右侧的变量

	/* 我们只能处理有两个操作数的OpExpr */
	if (!IsA(opexpr, OpExpr))  // 检查输入条件是否为操作符表达式
		return true;  // 如果不是操作符表达式,则返回true,无法下推该条件

	if (list_length(opexpr->args) != 2)  // 确保操作符表达式有两个操作数
		return true;  // 如果操作符的操作数不是两个,则返回true,无法下推

	/*
	 * 当前,这个优化仅限于严格的OpExpr。原因是,在执行时,一旦运行条件变为假,
	 * 我们停止计算窗口函数。为了避免留下过时的窗口函数结果值,我们将它们设置为NULL。
	 * 只有严格的OpExpr才能确保在WindowAgg节点中正确地过滤掉带有NULL值的元组。
	 */
	set_opfuncid(opexpr);  // 设置操作符的函数ID
	if (!func_strict(opexpr->opfuncid))  // 检查操作符是否为严格运算符
		return true;  // 如果操作符不是严格运算符,返回true,无法下推

	/*
	 * 检查是否有引用子查询中窗口函数的变量。
	 * 如果找到,我们将调用find_window_run_conditions()来检查该'opexpr'是否可以
	 * 作为运行条件的一部分。
	 */
	/* 检查操作符表达式的左侧 */
	var1 = linitial(opexpr->args);  // 获取操作符表达式的左侧操作数
	if (IsA(var1, Var) && var1->varattno > 0)  // 检查左侧操作数是否是窗口函数引用的变量
	{
		TargetEntry *tle = list_nth(subquery->targetList, var1->varattno - 1);  // 获取对应的目标列
		WindowFunc *wfunc = (WindowFunc *) tle->expr;  // 获取窗口函数
		/* 使用find_window_run_conditions检查该条件是否可以作为窗口聚合的运行条件 */
		if (find_window_run_conditions(subquery, rte, rti, tle->resno, wfunc,
									   opexpr, true, &keep_original,
									   run_cond_attrs))
			return keep_original;  // 如果可以作为运行条件,返回是否保留原始条件
	}

	/* 检查操作符表达式的右侧 */
	var2 = lsecond(opexpr->args);  // 获取操作符表达式的右侧操作数
	if (IsA(var2, Var) && var2->varattno > 0)  // 检查右侧操作数是否是窗口函数引用的变量
	{
		TargetEntry *tle = list_nth(subquery->targetList, var2->varattno - 1);  // 获取对应的目标列
		WindowFunc *wfunc = (WindowFunc *) tle->expr;  // 获取窗口函数
		/* 使用find_window_run_conditions检查该条件是否可以作为窗口聚合的运行条件 */
		if (find_window_run_conditions(subquery, rte, rti, tle->resno, wfunc,
									   opexpr, false, &keep_original,
									   run_cond_attrs))
			return keep_original;  // 如果可以作为运行条件,返回是否保留原始条件
	}

	return true;  // 如果没有条件可以下推,返回true,保留原始条件
}

具体案例说明
  假设我们有以下SQL查询:

SELECT 
    department,
    AVG(salary) OVER (PARTITION BY department) 
FROM 
    employees 
WHERE 
    salary > 50000;

  这里,employees是一个包含多个字段的表,包括departmentsalary。该查询通过AVG(salary)计算每个department的平均薪资,并且只考虑salary > 50000的记录。我们现在要理解check_and_push_window_quals函数在此查询中的执行逻辑。

  1. 初始条件

  我们需要将查询中的WHERE条件(即salary > 50000)尝试下推到窗口函数(AVG(salary) OVER (PARTITION BY department))中,来优化查询性能。如果条件可以下推,PostgreSQL将避免在窗口函数计算过程中返回不符合条件的行,从而减少不必要的计算。

  1. 函数执行逻辑
  • 步骤1:检查clause类型

    • clausesalary > 50000,它是一个操作符表达式OpExpr),因此check_and_push_window_quals会继续处理。
  • 步骤2:确认操作符类型

    • clause会被解析为一个OpExpr,我们通过set_opfuncidfunc_strict检查操作符是否严格(strict)。在此例中,>是一个严格操作符,因此可以进行进一步的优化。
  • 步骤3:处理操作符的两侧(左侧和右侧)

    • clause是一个二元操作符表达式:salary > 50000
      • 左侧操作数salary 是一个Var类型的字段,它是一个表中的列。PostgreSQL通过subquery->targetList查找salary的目标列(Var)。
      • 右侧操作数:常量50000是一个常量,不需要进一步处理。
  • 步骤4:检查是否有窗口函数

    • salary作为窗口函数的输入列之一,因此我们需要查看是否有窗口函数依赖于它。如果该列被窗口函数引用,check_and_push_window_quals会进一步检查是否可以将条件salary > 50000下推到窗口函数。
    • 在这种情况下,AVG(salary) OVER (PARTITION BY department)正是一个窗口函数salary列被窗口函数使用,因此我们调用find_window_run_conditions来检查是否可以将salary > 50000作为窗口函数的运行条件run condition)。
  • 步骤5:调用find_window_run_conditions

    • 通过find_window_run_conditions,我们检查该条件是否可以作为AVG(salary)窗口函数的运行条件run condition)。如果可以,那么PostgreSQL将优化该查询,窗口函数只计算符合salary > 50000条件的行,而不必返回所有记录,再进行筛选。
    • 如果该条件可以下推,run_cond_attrs会被更新,标记出哪些目标列(如salary)需要作为窗口函数的运行条件。
  • 步骤6:返回结果

    • 如果salary > 50000条件被成功下推到窗口函数中,check_and_push_window_quals函数将返回false,表示可以忽略原始的WHERE条件,因为它已经被包含在窗口函数的运行条件中。
    • 否则,true会被返回,表示需要保留原始的WHERE条件,并且窗口函数的执行依然基于完整的查询结果。

find_window_run_conditions 函数

  find_window_run_conditions函数的作用是确定是否可以利用操作符表达式(OpExpr)来短路窗口函数的执行,以提高查询效率。具体来说,它检查窗口函数的单调性,如果窗口函数的输出是单调递增或递减的,则可以利用这个特性在外层查询中利用WHERE子句的过滤条件来提前停止窗口函数的计算。例如,如果一个ROW_NUMBER()窗口函数输出的值是递增的,并且外部查询要求只返回ROW_NUMBER() <= 10的结果,那么窗口函数的计算可以在ROW_NUMBER()达到11时停止,从而避免对后续行进行不必要的计算。函数源码如下所示:(路径:postgresql-15.10\src\backend\optimizer\path\allpaths.c

/*
 * find_window_run_conditions
 * 确定 'wfunc' 是否真的是一个窗口函数,并调用其支持函数来确定该函数的单调性属性。
 * 然后检查 'opexpr' 是否能用于短路执行。如果可以,它将帮助跳过不必要的计算。
 */
static bool
find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
						   AttrNumber attno, WindowFunc *wfunc, OpExpr *opexpr,
						   bool wfunc_left, bool *keep_original,
						   Bitmapset **run_cond_attrs)
{
	Oid			prosupport;  // 存储窗口函数的支持函数的 OID
	Expr	   *otherexpr;  // 存储操作符表达式中的另一个表达式
	SupportRequestWFuncMonotonic req;  // 存储单调性请求
	SupportRequestWFuncMonotonic *res;  // 存储单调性结果
	WindowClause *wclause;  // 存储窗口子句
	List	   *opinfos;  // 存储操作符的详细信息列表
	OpExpr	   *runopexpr;  // 存储要用作运行条件的操作符表达式
	Oid			runoperator;  // 存储运行条件的操作符
	ListCell   *lc;  // 用于遍历操作符信息的列表单元

	*keep_original = true;  // 默认为保留原始条件

	// 如果窗口函数是一个类型转换表达式(RelabelType),递归到窗口函数本身
	while (IsA(wfunc, RelabelType))
		wfunc = (WindowFunc *) ((RelabelType *) wfunc)->arg;

	// 如果不是窗口函数,则返回 false
	if (!IsA(wfunc, WindowFunc))
		return false;

	// 如果窗口函数中包含子查询,则无法优化,返回 false
	if (contain_subplans((Node *) wfunc))
		return false;

	// 获取窗口函数的支持函数 OID
	prosupport = get_func_support(wfunc->winfnoid);

	// 如果窗口函数没有支持函数,返回 false
	if (!OidIsValid(prosupport))
		return false;

	// 获取操作符表达式的另一侧的表达式(左侧或右侧)
	if (wfunc_left)
		otherexpr = lsecond(opexpr->args);  // 获取右侧表达式
	else
		otherexpr = linitial(opexpr->args);  // 获取左侧表达式

	// 要比较的值在窗口分区的评估过程中必须保持不变
	if (!is_pseudo_constant_clause((Node *) otherexpr))
		return false;

	// 获取与窗口函数关联的窗口子句
	wclause = (WindowClause *) list_nth(subquery->windowClause, wfunc->winref - 1);

	// 设置单调性请求参数
	req.type = T_SupportRequestWFuncMonotonic;
	req.window_func = wfunc;
	req.window_clause = wclause;

	// 调用窗口函数的支持函数来获取单调性属性
	res = (SupportRequestWFuncMonotonic *) 
		DatumGetPointer(OidFunctionCall1(prosupport, PointerGetDatum(&req)));

	// 如果窗口函数的单调性不是单调递增或单调递减,返回 false
	if (res == NULL || res->monotonic == MONOTONICFUNC_NONE)
		return false;

	runopexpr = NULL;  // 初始化运行条件表达式为空
	runoperator = InvalidOid;  // 初始化运行条件操作符为空
	opinfos = get_op_btree_interpretation(opexpr->opno);  // 获取操作符的详细信息

	// 遍历操作符信息
	foreach(lc, opinfos)
	{
		OpBtreeInterpretation *opinfo = (OpBtreeInterpretation *) lfirst(lc);
		int			strategy = opinfo->strategy;  // 获取操作符策略

		// 处理 < / <= 操作符
		if (strategy == BTLessStrategyNumber || strategy == BTLessEqualStrategyNumber)
		{
			// 如果是单调递增函数,支持 <wfunc> op <pseudoconst>
			// 如果是单调递减函数,支持 <pseudoconst> op <wfunc>
			if ((wfunc_left && (res->monotonic & MONOTONICFUNC_INCREASING)) ||
				(!wfunc_left && (res->monotonic & MONOTONICFUNC_DECREASING)))
			{
				*keep_original = false;  // 不再保留原始条件
				runopexpr = opexpr;  // 设置运行条件表达式为当前的操作符表达式
				runoperator = opexpr->opno;  // 设置运行条件操作符
			}
			break;  // 结束遍历
		}
		// 处理 > / >= 操作符
		else if (strategy == BTGreaterStrategyNumber || strategy == BTGreaterEqualStrategyNumber)
		{
			// 单调递减函数支持 <wfunc> op <pseudoconst>
			// 单调递增函数支持 <pseudoconst> op <wfunc>
			if ((wfunc_left && (res->monotonic & MONOTONICFUNC_DECREASING)) ||
				(!wfunc_left && (res->monotonic & MONOTONICFUNC_INCREASING)))
			{
				*keep_original = false;
				runopexpr = opexpr;
				runoperator = opexpr->opno;
			}
			break;
		}
		// 处理 = 操作符
		else if (strategy == BTEqualStrategyNumber)
		{
			int16		newstrategy;

			// 如果函数既是单调递增的又是单调递减的,窗口函数的返回值在每次计算时是相同的
			// 在这种情况下,直接使用原始的操作符表达式作为运行条件
			if ((res->monotonic & MONOTONICFUNC_BOTH) == MONOTONICFUNC_BOTH)
			{
				*keep_original = false;
				runopexpr = opexpr;
				runoperator = opexpr->opno;
				break;
			}

			// 单调递增的情况下,创建 <wfunc> <= <value> 或 <value> >= <wfunc> 的条件
			// 单调递减的情况下,创建 <wfunc> >= <value> 或 <value> <= <wfunc> 的条件
			if (res->monotonic & MONOTONICFUNC_INCREASING)
				newstrategy = wfunc_left ? BTLessEqualStrategyNumber : BTGreaterEqualStrategyNumber;
			else
				newstrategy = wfunc_left ? BTGreaterEqualStrategyNumber : BTLessEqualStrategyNumber;

			// 保留原始的等号条件
			*keep_original = true;
			runopexpr = opexpr;

			// 确定用于运行条件的操作符
			runoperator = get_opfamily_member(opinfo->opfamily_id, opinfo->oplefttype,
											  opinfo->oprighttype, newstrategy);
			break;
		}
	}

	// 如果找到了有效的运行条件表达式
	if (runopexpr != NULL)
	{
		Expr	   *newexpr;

		// 构建运行条件表达式,保持窗口函数在原位置
		if (wfunc_left)
			newexpr = make_opclause(runoperator, runopexpr->opresulttype,
									runopexpr->opretset, (Expr *) wfunc,
									otherexpr, runopexpr->opcollid,
									runopexpr->inputcollid);
		else
			newexpr = make_opclause(runoperator, runopexpr->opresulttype,
									runopexpr->opretset, otherexpr, (Expr *) wfunc,
									runopexpr->opcollid, runopexpr->inputcollid);

		// 将新创建的运行条件添加到窗口子句的 runCondition 中
		wclause->runCondition = lappend(wclause->runCondition, newexpr);

		// 记录该属性已经用于运行条件
		*run_cond_attrs = bms_add_member(*run_cond_attrs, attno - FirstLowInvalidHeapAttributeNumber);
		return true;
	}

	// 如果没有找到支持的操作符,返回 false
	return false;
}

该函数的主要执行步骤包括:

  1. 判断窗口函数的单调性属性(递增、递减等)。
  2. 根据单调性属性决定是否可以应用短路条件(比如<<=)。
  3. 生成适当的条件表达式,将其添加到窗口子句的runCondition中。
  4. 更新已处理的属性集,以确保这些条件在查询执行时会被应用。

执行案例

  为了详细解释 find_window_run_conditions 函数的作用,我们可以通过一个具体的例子来说明。假设有一个查询,其中包含一个窗口函数 row_number(),并且该窗口函数用于对行号进行排序。假设有一个窗口函数的查询结构如下:

SELECT 
    row_number() OVER (ORDER BY salary DESC) AS rn, 
    name, salary 
FROM employees
WHERE row_number() <= 10;

  在这个查询中,我们使用了 row_number() 作为窗口函数,并且在外部查询的 WHERE 子句中使用了 row_number() <= 10 来过滤结果。我们的目标是利用 find_window_run_conditions 函数来判断在什么情况下可以通过对窗口函数的运行条件进行优化,避免无谓的计算。

具体执行过程(逐行解释)
  假设我们调用 find_window_run_conditions 函数时,传入的参数如下:

  • subquery: 表示包含窗口函数的子查询,即 SELECT row_number() OVER (ORDER BY salary DESC) AS rn, ...
  • rte: 查询中的关系表条目employees 表)。
  • rti: 该表在查询范围表中的索引(例如,employees 表的索引)。
  • attno: 表示我们感兴趣的属性(例如,row_number() 函数返回的列 rn)。
  • wfunc: 窗口函数结构体,包含了 row_number() 函数的信息(例如,函数ID、窗口参考、分区、排序等)。
  • opexpr: 表示操作符表达式row_number() <= 10 中的 <= 操作符。
  • wfunc_left: 指示窗口函数 row_number() 在操作符的左侧。
  • keep_original: 用于指示是否保留原始的操作符条件
  • run_cond_attrs: 用于记录哪些属性(列)将用于优化条件

逐行解释代码执行过程

  1. 初始化 *keep_original = true
*keep_original = true;

  这行代码初始化了 keep_original 标志为 true。这是因为默认情况下我们保留原始的操作符条件(row_number() <= 10)

  1. 处理窗口函数的类型
while (IsA(wfunc, RelabelType))
    wfunc = (WindowFunc *) ((RelabelType *) wfunc)->arg;

  如果 wfuncRelabelType 类型(这通常发生在类型转换的情况下),我们将递归地访问它的实际参数。假设这里 wfuncWindowFunc 类型,因此这段代码不会执行。

  1. 检查 wfunc 是否是窗口函数
if (!IsA(wfunc, WindowFunc))
    return false;

  这行代码检查 wfunc 是否是窗口函数类型。如果不是窗口函数,直接返回 false。在我们的例子中,row_number() 窗口函数符合条件,因此继续执行。

  1. 检查窗口函数中是否包含子查询
if (contain_subplans((Node *) wfunc))
    return false;

  窗口函数中如果包含子查询(例如嵌套查询),则无法优化。因此,检查 wfunc是否包含子查询。如果包含子查询,返回 false。在本例中,row_number() 不包含子查询,所以继续执行。

  1. 获取窗口函数的支持函数
prosupport = get_func_support(wfunc->winfnoid);

  这行代码获取窗口函数的支持函数。每个窗口函数可能有一个“支持函数”,用来检查该函数的单调性(即是否是递增递减的)。对于 row_number() 函数,它应该是递增的。

注:以 row_number() 窗口函数为例,通常这个函数会在分区内生成递增的行号。因此,窗口函数的支持函数可能会返回一个标志,表示该窗口函数的结果是递增的(单调递增)。

  1. 检查是否存在有效的支持函数
if (!OidIsValid(prosupport))
    return false;

  如果窗口函数没有有效的支持函数,返回 false。如果 row_number() 的支持函数有效,继续执行。

  1. 获取操作符表达式的另一边的表达式
if (wfunc_left)
    otherexpr = lsecond(opexpr->args);
else
    otherexpr = linitial(opexpr->args);

  这行代码根据 wfunc_left 标志来确定操作符表达式 opexpr 的另一边的表达式。如果 wfunc_lefttrue,则窗口函数在操作符的左边otherexpr操作符的右边。如果 wfunc_leftfalse,则窗口函数在右边otherexpr操作符的左边。假设 wfunc_lefttrueotherexpr 将是常数 10,因为 opexpr 中是 row_number() <= 10

  1. 检查 otherexpr 是否是伪常量
if (!is_pseudo_constant_clause((Node *) otherexpr))
    return false;

  这行代码检查 otherexpr 是否是伪常量,即它的值在窗口分区的评估过程中不会发生变化。对于 row_number() <= 10 中的 10,它是一个常量,因此通过此检查。

  1. 获取窗口函数的窗口子句
wclause = (WindowClause *) list_nth(subquery->windowClause, wfunc->winref - 1);

  通过 wfunc->winref 获取与窗口函数 row_number() 相关联的窗口子句。这一行确保我们访问到包含窗口函数定义的正确窗口子句。

  1. 构建支持请求并调用支持函数
req.type = T_SupportRequestWFuncMonotonic;
req.window_func = wfunc;
req.window_clause = wclause;

  创建支持请求结构体指定窗口函数和窗口子句,准备调用支持函数

res = (SupportRequestWFuncMonotonic *)
    DatumGetPointer(OidFunctionCall1(prosupport, PointerGetDatum(&req)));

  调用支持函数检查窗口函数的单调性。如果 row_number() 是递增的,则返回 MONOTONICFUNC_INCREASING

  1. 检查是否支持单调性
if (res == NULL || res->monotonic == MONOTONICFUNC_NONE)
    return false;

  如果支持函数返回为空窗口函数不具有单调性,则返回 false。对于 row_number(),它是递增的,因此通过此检查。

  1. 分析操作符的策略
runopexpr = NULL;
runoperator = InvalidOid;
opinfos = get_op_btree_interpretation(opexpr->opno);

  获取操作符表达式的相关操作符信息,准备分析操作符策略

foreach(lc, opinfos)
{
    OpBtreeInterpretation *opinfo = (OpBtreeInterpretation *) lfirst(lc);
    int strategy = opinfo->strategy;

  遍历操作符的 B-tree 解释,检查操作符的策略类型(例如 <, <=, = 等)。

  1. 检查并处理操作符 <<=
if (strategy == BTLessStrategyNumber || strategy == BTLessEqualStrategyNumber)
{
    if ((wfunc_left && (res->monotonic & MONOTONICFUNC_INCREASING)) ||
        (!wfunc_left && (res->monotonic & MONOTONICFUNC_DECREASING)))
    {
        *keep_original = false;
        runopexpr = opexpr;
        runoperator = opexpr->opno;
    }
    break;
}

  对于 <= 操作符,如果窗口函数是递增的,且窗口函数位于操作符的左侧,则可以利用该条件来优化计算,避免不必要的处理。此时,keep_original 被设置为 false,并且 runopexprrunoperator 被设置为当前的操作符表达式。

  1. 其他操作符处理
      处理 >, >=, = 等操作符的逻辑与上述类似。针对不同的操作符策略,检查是否可以根据窗口函数的单调性来优化。

  2. 构建新的运行条件表达式并添加到窗口子句

if (runopexpr != NULL)
{
    Expr *newexpr;
    if (wfunc_left)
        newexpr = make_opclause(runoperator, runopexpr->opresulttype, runopexpr->opretset, (Expr *) wfunc, otherexpr, runopexpr->opcollid);
    else
        newexpr = make_opclause(runoperator, runopexpr->opresulttype, runopexpr->opretset, otherexpr, (Expr *) wfunc, runopexpr->opcollid);
}

  最后,基于优化后的条件构建新的表达式并将其加入到窗口子句中,供查询执行时使用。

总结

  通过上述详细的例子和逐行分析,find_window_run_conditions 函数的作用是基于窗口函数的单调性和操作符表达式(如 <=)来决定是否可以优化窗口函数的计算,从而避免不必要的计算。通过合理的优化,查询的执行效率得到提升。

计划器模块(set_plan_refs函数)

  这段代码是 PostgreSQL 计划器Planner)中的 WindowAgg 处理逻辑,负责调整窗口函数执行节点的运行条件,确保 WindowFuncs(窗口函数)在 runCondition 计算时,能够正确引用已经计算出的值,而不是重复计算。同时,它还修正了窗口帧的偏移量表达式 (startOffsetendOffset),以及 runConditionrunConditionOrig 的变量索引,使其适应优化后的执行计划,确保查询执行效率和正确性。

case T_WindowAgg:  // 处理 WindowAgg 类型的计划节点
{
    WindowAgg  *wplan = (WindowAgg *) plan;  // 将 plan 转换为 WindowAgg 类型,以便访问其成员变量

    /*
     * 调整 WindowAgg 的运行条件 (`runCondition`):
     * 1. 原始 `runCondition` 可能包含对 WindowFuncs(窗口函数)的直接引用,
     *    但在执行时,WindowFunc 的计算结果会存储在 scan slot(扫描槽)中。
     * 2. 这个调整确保 `runCondition` 直接引用 scan slot 中的结果,而不是重新计算 WindowFunc。
     */
    wplan->runCondition = set_windowagg_runcondition_references(root,
                                                                wplan->runCondition,
                                                                (Plan *) wplan);

    // 处理 WindowAgg 节点的变量引用,确保其与上层查询计划兼容
    set_upper_references(root, plan, rtoffset);

    /*
     * 处理窗口帧的起始 (`startOffset`) 和结束 (`endOffset`) 偏移量:
     * 1. `startOffset` 和 `endOffset` 可能是表达式,如 `RANGE BETWEEN INTERVAL '1 day' PRECEDING AND CURRENT ROW`。
     * 2. 由于它们不会引用子计划中的变量,因此可以直接使用 `fix_scan_expr` 进行调整,确保正确引用优化后的执行计划。
     */
    wplan->startOffset = fix_scan_expr(root, wplan->startOffset, rtoffset, 1);
    wplan->endOffset = fix_scan_expr(root, wplan->endOffset, rtoffset, 1);

    /*
     * 修正 `runCondition` 运行条件中的变量索引:
     * 1. `runCondition` 可能包含对查询目标列表 (target list) 的引用。
     * 2. `fix_scan_list` 通过 `rtoffset` 修正这些引用,以适应优化后的计划。
     */
    wplan->runCondition = fix_scan_list(root,
                                        wplan->runCondition,
                                        rtoffset,
                                        NUM_EXEC_TLIST(plan));

    /*
     * 修正 `runConditionOrig`(原始运行条件)的变量索引:
     * 1. `runConditionOrig` 是未优化前的 `runCondition` 版本,通常用于调试或执行回退。
     * 2. 这里同样调用 `fix_scan_list` 进行调整,使其适应优化后的计划。
     */
    wplan->runConditionOrig = fix_scan_list(root,
                                            wplan->runConditionOrig,
                                            rtoffset,
                                            NUM_EXEC_TLIST(plan));
}

功能描述

  1. 调整 runCondition 引用

    • 确保 runCondition 运行时引用的是 scan slot 中存储的 WindowFunc 计算结果,而不是重新计算 WindowFunc,提高执行效率。
  2. 修正 startOffsetendOffset

    • WindowAgg 可能包含窗口帧偏移量 (frame offset expressions),例如 RANGE BETWEEN 语句中的时间偏移值。
    • 由于这些表达式不会包含子计划中的变量,因此使用 fix_scan_expr 进行修正。
  3. 处理 runConditionrunConditionOrig 变量索引

    • fix_scan_list 确保 runConditionrunConditionOrig 中的变量索引与优化后的查询计划一致,避免执行时变量解析错误。

set_windowagg_runcondition_references 函数

  set_windowagg_runcondition_references 主要用于调整 WindowAgg 计划节点中的 runCondition,确保 runCondition 内部的 WindowFunc(窗口函数)不会被重复计算,而是转换为指向 plan->targetlist(计划目标列表)中相应 WindowFunc 计算结果的 Var(变量引用)。
  这样做的目的是优化执行效率,避免 WindowFuncrunCondition 评估时重复计算,改为直接引用 plan->targetlist 中已经计算出的结果。函数源码如下所示:(路径:postgresql-15.10\src\backend\optimizer\plan\setrefs.c

/*
 * set_windowagg_runcondition_references
 *  将 'runcondition' 运行条件中的 WindowFunc 引用替换为 Var,
 *  使其指向 'plan' 目标列表 (targetlist) 中的相应 WindowFunc 计算结果。
 *
 *  这样可以避免 WindowFunc 在 'runCondition' 评估时重复计算,
 *  改为直接引用 'plan->targetlist',从而优化执行效率。
 */
static List *
set_windowagg_runcondition_references(PlannerInfo *root,
									  List *runcondition,
									  Plan *plan)
{
	List	   *newlist;  // 存储转换后的 runCondition 列表
	indexed_tlist *itlist;  // 存储 'plan->targetlist' 的索引信息

	// 构建目标列表索引,方便后续查找 targetlist 中的变量
	itlist = build_tlist_index(plan->targetlist);

	// 处理 runCondition,将 WindowFunc 引用替换为指向 targetlist 变量的 Var
	newlist = fix_windowagg_condition_expr(root, runcondition, itlist);

	// 释放目标列表索引内存
	pfree(itlist);

	// 返回转换后的 runCondition
	return newlist;
}

执行案例

  我们通过一个具体的 SQL 查询,详细展示 set_windowagg_runcondition_references 的作用,并结合执行计划分析它如何优化 WindowAgg 节点中的 runCondition 计算方式。

  1. SQL 查询示例
      假设我们有一个 sales 表,记录了销售数据:
CREATE TABLE sales (
    id SERIAL PRIMARY KEY,
    region TEXT,
    amount NUMERIC
);

  假设表中有如下数据:

idregionamount
1East100
2West200
3East150
4West250
5East300

  现在我们执行以下查询,计算每个区域内的 row_number,并过滤掉 row_number <= 2 的记录:

SELECT id, region, amount, 
       row_number() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
FROM sales
HAVING row_number() > 2;

  1. PostgreSQL 查询执行计划
      在 HAVING 子句中,我们直接使用 row_number(),这可能导致 PostgreSQL 在执行时多次计算 row_number(),引发性能问题。

初始执行计划(未优化)

WindowAgg  (cost=XX..XX rows=XX width=XX)
  ->  Sort  (cost=XX..XX)
        Sort Key: region, amount DESC
        ->  Seq Scan on sales  (cost=XX..XX)

在此情况下:

  • row_number() 作为 WindowFunc 需要在 HAVING 过滤时重新计算,增加了计算成本。

  1. set_windowagg_runcondition_references 作用
      在 set_windowagg_runcondition_references 运行后,PostgreSQL 会优化 WindowAgg 节点:
  • 原始的 runCondition
    • HAVING row_number() > 2
  • 转换后的 runCondition
    • HAVING rn > 2
    • rn 直接引用 targetlist(plan->targetlist)row_number() 的结果,避免重复计算。

优化后的执行计划

WindowAgg  (cost=XX..XX rows=XX width=XX)
  ->  Sort  (cost=XX..XX)
        Sort Key: region, amount DESC
        ->  Seq Scan on sales  (cost=XX..XX)
  Filter: (rn > 2)
  • WindowAgg 只计算 row_number() 一次,结果存入 targetlist
  • HAVING 直接引用 rn(Var),避免 row_number() 重新执行。

  1. 代码执行流程解析

  当 set_windowagg_runcondition_references 执行时:

  • 构建 targetlist 索引
    build_tlist_index(plan->targetlist) 解析 plan->targetlist,构建一个映射,使 row_number() 结果可以通过 Var 引用。

  • 转换 runCondition
    fix_windowagg_condition_expr(root, runcondition, itlist) 遍历 HAVING row_number() > 2,将 row_number() 引用替换为 Var

  • 释放 itlist 索引
    pfree(itlist) 释放内存。

  • 返回优化后的 runCondition
    返回 HAVING rn > 2,避免 row_number() 额外计算。


  1. 结论

  在未优化的情况下,HAVING row_number() > 2 可能导致 row_number()WindowAgg 计算后再次被评估。
  通过 set_windowagg_runcondition_referencesPostgreSQL 只计算 row_number() 一次,然后在 HAVING 过滤时直接引用 targetlist 结果,提高查询效率。

fix_windowagg_condition_expr 函数

  该函数 fix_windowagg_condition_expr 主要用于优化 WindowAgg 运行条件 (runCondition),避免 WindowFunc 重复计算。
  它将 runcondition 中的 WindowFunc 替换为 Var,以便引用 subplan_itlist(即 plan->targetlist),减少计算开销。
  在 HAVING 或其他涉及 WindowFunc 计算的表达式中,这种优化能够避免重复计算窗口函数,提高查询执行效率

🔹函数参数

参数名类型说明
rootPlannerInfo *查询优化器的全局上下文信息
runconditionList *需要优化的 runCondition(即 HAVING 条件等)
subplan_itlistindexed_tlist *targetlist(plan->targetlist)的索引表

📌 目的
  该函数的目标是优化 WindowAgg 运行条件,使 runCondition 直接引用 plan->targetlist,避免 WindowFunc 重新计算

/*
 * fix_windowagg_condition_expr
 *		转换 `runcondition` 中的 `WindowFunc` 引用,
 *		将其替换为指向 `subplan_itlist` 中对应 `WindowFunc` 结果的 `Var`。
 *		这样可以避免 `WindowFunc` 在 `HAVING` 等条件中被重复计算,提高查询执行效率。
 */
static List *
fix_windowagg_condition_expr(PlannerInfo *root,
                             List *runcondition,
                             indexed_tlist *subplan_itlist)
{
    /* 定义 `fix_windowagg_cond_context` 结构体变量 `context`,用于存储转换过程中的上下文信息 */
    fix_windowagg_cond_context context;

    /* 将 `PlannerInfo` 指针 `root` 传递给 `context`,确保在转换过程中能够访问查询优化器的上下文信息 */
    context.root = root;

    /* 存储 `subplan_itlist`(即 `plan->targetlist` 的索引列表),用于查找 `WindowFunc` 对应的 `Var` */
    context.subplan_itlist = subplan_itlist;

    /* 初始化 `newvarno` 为 `0`(该字段目前未使用,可能用于扩展功能) */
    context.newvarno = 0;

    /* 调用 `fix_windowagg_condition_expr_mutator` 进行转换 */
    /* 该函数会递归遍历 `runCondition`,查找 `WindowFunc`,并替换为 `Var` */
    return (List *) fix_windowagg_condition_expr_mutator((Node *) runcondition,
                                                         &context);
}

fix_windowagg_condition_expr_mutator 函数

  该函数的作用是递归遍历并替换查询树中的 WindowFunc 节点,将其替换为 targetlist 中相应的 Var,以便在 runCondition 中高效引用而不必重新计算 WindowFunc。具体功能如下:

  1. 遍历树结构:递归遍历整个表达式树。
  2. 替换 WindowFunc:当遇到 WindowFunc 节点时,将其替换为指向 targetlist 中对应位置的 Var,避免重新计算 WindowFunc
  3. 查找目标列表中的 Var:通过 search_indexed_tlist_for_non_var 函数,找到对应的 Var
  4. 报错处理:如果没有找到相应的 Var,会抛出错误,说明 WindowFunc 无法在目标列表中找到。
/*
 * fix_windowagg_condition_expr_mutator
 *		变换器函数,用于将 `WindowFunc` 替换为对应的 `Var`,该 `Var` 引用在 `targetlist` 中的 `WindowFunc`。
 *		此操作帮助优化查询,避免在条件中重复计算窗口函数。
 */
static Node *
fix_windowagg_condition_expr_mutator(Node *node,
                                     fix_windowagg_cond_context *context)
{
    /* 如果当前节点为 NULL,直接返回 NULL */
    if (node == NULL)
        return NULL;

    /* 如果当前节点是 WindowFunc 类型的节点 */
    if (IsA(node, WindowFunc))
    {
        /* 定义一个 Var 类型的指针 `newvar`,用于存储替换后的变量 */
        Var *newvar;

        /* 从 `subplan_itlist` 中搜索与 `WindowFunc` 对应的 `Var` */
        newvar = search_indexed_tlist_for_non_var((Expr *) node,
                                                  context->subplan_itlist,
                                                  context->newvarno);
        /* 如果找到了对应的 Var,则返回这个 Var */
        if (newvar)
            return (Node *) newvar;

        /* 如果没有找到对应的 Var,则抛出错误,表示未能在目标列表中找到 WindowFunc */
        elog(ERROR, "WindowFunc not found in subplan target lists");
    }

    /* 对当前节点的子树进行递归处理,遍历树结构 */
    return expression_tree_mutator(node,
                                   fix_windowagg_condition_expr_mutator,
                                   (void *) context);
}

执行案例

  假设我们有如下 SQL 查询:

SELECT id, region, amount, 
       row_number() OVER (PARTITION BY region ORDER BY amount DESC) AS rn
FROM sales
HAVING row_number() > 2;

  在查询优化过程中,WindowFunc(如 row_number())会出现在 HAVING 子句中。为了避免在 HAVING 子句中重复计算窗口函数,fix_windowagg_condition_expr_mutator 函数会将 WindowFunc 替换为 Var,并使得 HAVING 子句直接引用 targetlist 中已经计算好的结果。

关键函数解释

  • search_indexed_tlist_for_non_var:此函数用于从 subplan_itlist 中查找 WindowFunc 对应的 Var。它通过对比表达式的结构和目标列表,找到对应的变量引用。
  • expression_tree_mutator:这是 PostgreSQL 中常用的树遍历函数,用于递归处理表达式树的每个节点,并应用特定的变换操作。

http://www.kler.cn/a/536003.html

相关文章:

  • Linux 命令行指南
  • 2025年Android NDK超全版本下载地址
  • Python因为网络原因安装依赖库报错
  • 【Elasticsearch】nested聚合
  • 基于RTOS的STM32游戏机
  • 基础篇05-直方图操作
  • golang命令大全12--命令速查表
  • Vue学习综合案例(四)
  • Spring的三级缓存如何解决循环依赖问题
  • 202412 青少年软件编程等级考试C/C++ 二级真题答案及解析
  • C++证件识别接口-身份证识别-护照识别-驾驶证识别-户口页识别
  • RabbitMQ 从入门到精通:从工作模式到集群部署实战(三)
  • 【AI大模型】Ubuntu18.04安装deepseek-r1模型+服务器部署+内网访问
  • Spring Boot篇
  • 如何查看linux机器有几个cpu
  • Mono里运行C#脚本41—编译MonoEmbed::gimme()调用的过程
  • Java实现数据库图片上传(包含从数据库拿图片传递前端渲染)-图文详解
  • 3. kafka事务消息
  • 分布式事务实战 ——Seata 与最终一致性方案
  • Cables Finance发布 V1.1 白皮书:开创RWA敞口新范式
  • 第二篇:前端VSCode常用快捷键-以及常用技巧
  • ORACLE 数据库的启动和关闭
  • LLM的Deep Research功能:重构人类认知与创新的新范式
  • SQL Server中RANK()函数:处理并列排名与自然跳号
  • tomcat如何配置保存7天滚动日志
  • NLP知识点