Fix query pullup issue with WindowClause runCondition

94985c210 added code to detect when WindowFuncs were monotonic and
allowed additional quals to be "pushed down" into the subquery to be
used as WindowClause runConditions in order to short-circuit execution
in nodeWindowAgg.c.

The Node representation of runConditions wasn't well selected and
because we do qual pushdown before planning the subquery, the planning
of the subquery could perform subquery pull-up of nested subqueries.
For WindowFuncs with args, the arguments could be changed after pushing
the qual down to the subquery.

This was made more difficult by the fact that the code duplicated the
WindowFunc inside an OpExpr to include in the WindowClauses runCondition
field.  This could result in duplication of subqueries and a pull-up of
such a subquery could result in another initplan parameter being issued
for the 2nd version of the subplan.  This could result in errors such as:

ERROR:  WindowFunc not found in subplan target lists

To fix this, we change the node representation of these run conditions
and instead of storing an OpExpr containing the WindowFunc in a list
inside WindowClause, we now store a new node type named
WindowFuncRunCondition within a new field in the WindowFunc.  These get
transformed into OpExprs later in planning once subquery pull-up has been
performed.

This problem did exist in v15 and v16, but that was fixed by 9d36b883b
and e5d20bbd.

Cat version bump due to new node type and modifying WindowFunc struct.

Bug: #18305
Reported-by: Zuming Jiang
Discussion: https://postgr.es/m/18305-33c49b4c830b37b3%40postgresql.org
This commit is contained in:
David Rowley 2024-05-05 12:54:46 +12:00
parent 01df147634
commit 7d2c7f08d9
18 changed files with 143 additions and 53 deletions

View File

@ -2163,6 +2163,16 @@ expression_tree_walker_impl(Node *node,
return true; return true;
if (WALK(expr->aggfilter)) if (WALK(expr->aggfilter))
return true; return true;
if (WALK(expr->runCondition))
return true;
}
break;
case T_WindowFuncRunCondition:
{
WindowFuncRunCondition *expr = (WindowFuncRunCondition *) node;
if (WALK(expr->arg))
return true;
} }
break; break;
case T_SubscriptingRef: case T_SubscriptingRef:
@ -2400,8 +2410,6 @@ expression_tree_walker_impl(Node *node,
return true; return true;
if (WALK(wc->endOffset)) if (WALK(wc->endOffset))
return true; return true;
if (WALK(wc->runCondition))
return true;
} }
break; break;
case T_CTECycleClause: case T_CTECycleClause:
@ -2752,8 +2760,6 @@ query_tree_walker_impl(Query *query,
return true; return true;
if (WALK(wc->endOffset)) if (WALK(wc->endOffset))
return true; return true;
if (WALK(wc->runCondition))
return true;
} }
} }
@ -3053,6 +3059,16 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode; return (Node *) newnode;
} }
break; break;
case T_WindowFuncRunCondition:
{
WindowFuncRunCondition *wfuncrc = (WindowFuncRunCondition *) node;
WindowFuncRunCondition *newnode;
FLATCOPY(newnode, wfuncrc, WindowFuncRunCondition);
MUTATE(newnode->arg, wfuncrc->arg, Expr *);
return (Node *) newnode;
}
break;
case T_SubscriptingRef: case T_SubscriptingRef:
{ {
SubscriptingRef *sbsref = (SubscriptingRef *) node; SubscriptingRef *sbsref = (SubscriptingRef *) node;
@ -3466,7 +3482,6 @@ expression_tree_mutator_impl(Node *node,
MUTATE(newnode->orderClause, wc->orderClause, List *); MUTATE(newnode->orderClause, wc->orderClause, List *);
MUTATE(newnode->startOffset, wc->startOffset, Node *); MUTATE(newnode->startOffset, wc->startOffset, Node *);
MUTATE(newnode->endOffset, wc->endOffset, Node *); MUTATE(newnode->endOffset, wc->endOffset, Node *);
MUTATE(newnode->runCondition, wc->runCondition, List *);
return (Node *) newnode; return (Node *) newnode;
} }
break; break;
@ -3799,7 +3814,6 @@ query_tree_mutator_impl(Query *query,
FLATCOPY(newnode, wc, WindowClause); FLATCOPY(newnode, wc, WindowClause);
MUTATE(newnode->startOffset, wc->startOffset, Node *); MUTATE(newnode->startOffset, wc->startOffset, Node *);
MUTATE(newnode->endOffset, wc->endOffset, Node *); MUTATE(newnode->endOffset, wc->endOffset, Node *);
MUTATE(newnode->runCondition, wc->runCondition, List *);
resultlist = lappend(resultlist, (Node *) newnode); resultlist = lappend(resultlist, (Node *) newnode);
} }

View File

@ -2205,7 +2205,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
* the run condition will handle all of the required filtering. * the run condition will handle all of the required filtering.
* *
* Returns true if 'opexpr' was found to be useful and was added to the * Returns true if 'opexpr' was found to be useful and was added to the
* WindowClauses runCondition. We also set *keep_original accordingly and add * WindowFunc's runCondition. We also set *keep_original accordingly and add
* 'attno' to *run_cond_attrs offset by FirstLowInvalidHeapAttributeNumber. * 'attno' to *run_cond_attrs offset by FirstLowInvalidHeapAttributeNumber.
* If the 'opexpr' cannot be used then we set *keep_original to true and * If the 'opexpr' cannot be used then we set *keep_original to true and
* return false. * return false.
@ -2358,7 +2358,7 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
*keep_original = true; *keep_original = true;
runopexpr = opexpr; runopexpr = opexpr;
/* determine the operator to use for the runCondition qual */ /* determine the operator to use for the WindowFuncRunCondition */
runoperator = get_opfamily_member(opinfo->opfamily_id, runoperator = get_opfamily_member(opinfo->opfamily_id,
opinfo->oplefttype, opinfo->oplefttype,
opinfo->oprighttype, opinfo->oprighttype,
@ -2369,27 +2369,15 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
if (runopexpr != NULL) if (runopexpr != NULL)
{ {
Expr *newexpr; WindowFuncRunCondition *wfuncrc;
/* wfuncrc = makeNode(WindowFuncRunCondition);
* Build the qual required for the run condition keeping the wfuncrc->opno = runoperator;
* WindowFunc on the same side as it was originally. wfuncrc->inputcollid = runopexpr->inputcollid;
*/ wfuncrc->wfunc_left = wfunc_left;
if (wfunc_left) wfuncrc->arg = copyObject(otherexpr);
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);
wclause->runCondition = lappend(wclause->runCondition, newexpr); wfunc->runCondition = lappend(wfunc->runCondition, wfuncrc);
/* record that this attno was used in a run condition */ /* record that this attno was used in a run condition */
*run_cond_attrs = bms_add_member(*run_cond_attrs, *run_cond_attrs = bms_add_member(*run_cond_attrs,
@ -2403,9 +2391,9 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
/* /*
* check_and_push_window_quals * check_and_push_window_quals
* Check if 'clause' is a qual that can be pushed into a WindowFunc's * Check if 'clause' is a qual that can be pushed into a WindowFunc
* WindowClause as a 'runCondition' qual. These, when present, allow * as a 'runCondition' qual. These, when present, allow some unnecessary
* some unnecessary work to be skipped during execution. * work to be skipped during execution.
* *
* 'run_cond_attrs' will be populated with all targetlist resnos of subquery * 'run_cond_attrs' will be populated with all targetlist resnos of subquery
* targets (offset by FirstLowInvalidHeapAttributeNumber) that we pushed * targets (offset by FirstLowInvalidHeapAttributeNumber) that we pushed

View File

@ -2699,7 +2699,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
wc->inRangeColl, wc->inRangeColl,
wc->inRangeAsc, wc->inRangeAsc,
wc->inRangeNullsFirst, wc->inRangeNullsFirst,
wc->runCondition, best_path->runCondition,
best_path->qual, best_path->qual,
best_path->topwindow, best_path->topwindow,
subplan); subplan);

View File

@ -870,9 +870,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
EXPRKIND_LIMIT); EXPRKIND_LIMIT);
wc->endOffset = preprocess_expression(root, wc->endOffset, wc->endOffset = preprocess_expression(root, wc->endOffset,
EXPRKIND_LIMIT); EXPRKIND_LIMIT);
wc->runCondition = (List *) preprocess_expression(root,
(Node *) wc->runCondition,
EXPRKIND_TARGET);
} }
parse->limitOffset = preprocess_expression(root, parse->limitOffset, parse->limitOffset = preprocess_expression(root, parse->limitOffset,
@ -4527,9 +4524,11 @@ create_one_window_path(PlannerInfo *root,
{ {
WindowClause *wc = lfirst_node(WindowClause, l); WindowClause *wc = lfirst_node(WindowClause, l);
List *window_pathkeys; List *window_pathkeys;
List *runcondition = NIL;
int presorted_keys; int presorted_keys;
bool is_sorted; bool is_sorted;
bool topwindow; bool topwindow;
ListCell *lc2;
window_pathkeys = make_pathkeys_for_window(root, window_pathkeys = make_pathkeys_for_window(root,
wc, wc,
@ -4577,7 +4576,6 @@ create_one_window_path(PlannerInfo *root,
* we do need to account for the increase in tlist width. * we do need to account for the increase in tlist width.
*/ */
int64 tuple_width = window_target->width; int64 tuple_width = window_target->width;
ListCell *lc2;
window_target = copy_pathtarget(window_target); window_target = copy_pathtarget(window_target);
foreach(lc2, wflists->windowFuncs[wc->winref]) foreach(lc2, wflists->windowFuncs[wc->winref])
@ -4599,17 +4597,53 @@ create_one_window_path(PlannerInfo *root,
topwindow = foreach_current_index(l) == list_length(activeWindows) - 1; topwindow = foreach_current_index(l) == list_length(activeWindows) - 1;
/* /*
* Accumulate all of the runConditions from each intermediate * Collect the WindowFuncRunConditions from each WindowFunc and
* WindowClause. The top-level WindowAgg must pass these as a qual so * convert them into OpExprs
* that it filters out unwanted tuples correctly.
*/ */
if (!topwindow) foreach(lc2, wflists->windowFuncs[wc->winref])
topqual = list_concat(topqual, wc->runCondition); {
ListCell *lc3;
WindowFunc *wfunc = lfirst_node(WindowFunc, lc2);
foreach(lc3, wfunc->runCondition)
{
WindowFuncRunCondition *wfuncrc =
lfirst_node(WindowFuncRunCondition, lc3);
Expr *opexpr;
Expr *leftop;
Expr *rightop;
if (wfuncrc->wfunc_left)
{
leftop = (Expr *) copyObject(wfunc);
rightop = copyObject(wfuncrc->arg);
}
else
{
leftop = copyObject(wfuncrc->arg);
rightop = (Expr *) copyObject(wfunc);
}
opexpr = make_opclause(wfuncrc->opno,
BOOLOID,
false,
leftop,
rightop,
InvalidOid,
wfuncrc->inputcollid);
runcondition = lappend(runcondition, opexpr);
if (!topwindow)
topqual = lappend(topqual, opexpr);
}
}
path = (Path *) path = (Path *)
create_windowagg_path(root, window_rel, path, window_target, create_windowagg_path(root, window_rel, path, window_target,
wflists->windowFuncs[wc->winref], wflists->windowFuncs[wc->winref],
wc, topwindow ? topqual : NIL, topwindow); runcondition, wc,
topwindow ? topqual : NIL, topwindow);
} }
add_path(window_rel, path); add_path(window_rel, path);

View File

@ -2175,14 +2175,6 @@ perform_pullup_replace_vars(PlannerInfo *root,
parse->returningList = (List *) parse->returningList = (List *)
pullup_replace_vars((Node *) parse->returningList, rvcontext); pullup_replace_vars((Node *) parse->returningList, rvcontext);
foreach(lc, parse->windowClause)
{
WindowClause *wc = lfirst_node(WindowClause, lc);
if (wc->runCondition != NIL)
wc->runCondition = (List *)
pullup_replace_vars((Node *) wc->runCondition, rvcontext);
}
if (parse->onConflict) if (parse->onConflict)
{ {
parse->onConflict->onConflictSet = (List *) parse->onConflict->onConflictSet = (List *)

View File

@ -2566,6 +2566,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid; newexpr->inputcollid = expr->inputcollid;
newexpr->args = args; newexpr->args = args;
newexpr->aggfilter = aggfilter; newexpr->aggfilter = aggfilter;
newexpr->runCondition = expr->runCondition;
newexpr->winref = expr->winref; newexpr->winref = expr->winref;
newexpr->winstar = expr->winstar; newexpr->winstar = expr->winstar;
newexpr->winagg = expr->winagg; newexpr->winagg = expr->winagg;

View File

@ -3471,6 +3471,7 @@ create_minmaxagg_path(PlannerInfo *root,
* 'subpath' is the path representing the source of data * 'subpath' is the path representing the source of data
* 'target' is the PathTarget to be computed * 'target' is the PathTarget to be computed
* 'windowFuncs' is a list of WindowFunc structs * 'windowFuncs' is a list of WindowFunc structs
* 'runCondition' is a list of OpExprs to short-circuit WindowAgg execution
* 'winclause' is a WindowClause that is common to all the WindowFuncs * 'winclause' is a WindowClause that is common to all the WindowFuncs
* 'qual' WindowClause.runconditions from lower-level WindowAggPaths. * 'qual' WindowClause.runconditions from lower-level WindowAggPaths.
* Must always be NIL when topwindow == false * Must always be NIL when topwindow == false
@ -3486,6 +3487,7 @@ create_windowagg_path(PlannerInfo *root,
Path *subpath, Path *subpath,
PathTarget *target, PathTarget *target,
List *windowFuncs, List *windowFuncs,
List *runCondition,
WindowClause *winclause, WindowClause *winclause,
List *qual, List *qual,
bool topwindow) bool topwindow)
@ -3510,6 +3512,7 @@ create_windowagg_path(PlannerInfo *root,
pathnode->subpath = subpath; pathnode->subpath = subpath;
pathnode->winclause = winclause; pathnode->winclause = winclause;
pathnode->qual = qual; pathnode->qual = qual;
pathnode->runCondition = runCondition;
pathnode->topwindow = topwindow; pathnode->topwindow = topwindow;
/* /*

View File

@ -2956,7 +2956,6 @@ transformWindowDefinitions(ParseState *pstate,
rangeopfamily, rangeopcintype, rangeopfamily, rangeopcintype,
&wc->endInRangeFunc, &wc->endInRangeFunc,
windef->endOffset); windef->endOffset);
wc->runCondition = NIL;
wc->winref = winref; wc->winref = winref;
result = lappend(result, wc); result = lappend(result, wc);

View File

@ -3826,6 +3826,7 @@ transformJsonAggConstructor(ParseState *pstate, JsonAggConstructor *agg_ctor,
/* wincollid and inputcollid will be set by parse_collate.c */ /* wincollid and inputcollid will be set by parse_collate.c */
wfunc->args = args; wfunc->args = args;
wfunc->aggfilter = aggfilter; wfunc->aggfilter = aggfilter;
wfunc->runCondition = NIL;
/* winref will be set by transformWindowFuncCall */ /* winref will be set by transformWindowFuncCall */
wfunc->winstar = false; wfunc->winstar = false;
wfunc->winagg = true; wfunc->winagg = true;

View File

@ -834,6 +834,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
wfunc->winstar = agg_star; wfunc->winstar = agg_star;
wfunc->winagg = (fdresult == FUNCDETAIL_AGGREGATE); wfunc->winagg = (fdresult == FUNCDETAIL_AGGREGATE);
wfunc->aggfilter = agg_filter; wfunc->aggfilter = agg_filter;
wfunc->runCondition = NIL;
wfunc->location = location; wfunc->location = location;
/* /*

View File

@ -57,6 +57,6 @@
*/ */
/* yyyymmddN */ /* yyyymmddN */
#define CATALOG_VERSION_NO 202404291 #define CATALOG_VERSION_NO 202405051
#endif #endif

View File

@ -1549,8 +1549,6 @@ typedef struct WindowClause
int frameOptions; /* frame_clause options, see WindowDef */ int frameOptions; /* frame_clause options, see WindowDef */
Node *startOffset; /* expression for starting bound, if any */ Node *startOffset; /* expression for starting bound, if any */
Node *endOffset; /* expression for ending bound, if any */ Node *endOffset; /* expression for ending bound, if any */
/* qual to help short-circuit execution */
List *runCondition pg_node_attr(query_jumble_ignore);
/* in_range function for startOffset */ /* in_range function for startOffset */
Oid startInRangeFunc pg_node_attr(query_jumble_ignore); Oid startInRangeFunc pg_node_attr(query_jumble_ignore);
/* in_range function for endOffset */ /* in_range function for endOffset */

View File

@ -2308,6 +2308,7 @@ typedef struct WindowAggPath
Path *subpath; /* path representing input source */ Path *subpath; /* path representing input source */
WindowClause *winclause; /* WindowClause we'll be using */ WindowClause *winclause; /* WindowClause we'll be using */
List *qual; /* lower-level WindowAgg runconditions */ List *qual; /* lower-level WindowAgg runconditions */
List *runCondition; /* OpExpr List to short-circuit execution */
bool topwindow; /* false for all apart from the WindowAgg bool topwindow; /* false for all apart from the WindowAgg
* that's closest to the root of the plan */ * that's closest to the root of the plan */
} WindowAggPath; } WindowAggPath;

View File

@ -575,6 +575,8 @@ typedef struct WindowFunc
List *args; List *args;
/* FILTER expression, if any */ /* FILTER expression, if any */
Expr *aggfilter; Expr *aggfilter;
/* List of WindowFuncRunConditions to help short-circuit execution */
List *runCondition pg_node_attr(query_jumble_ignore);
/* index of associated WindowClause */ /* index of associated WindowClause */
Index winref; Index winref;
/* true if argument list was really '*' */ /* true if argument list was really '*' */
@ -585,6 +587,34 @@ typedef struct WindowFunc
ParseLoc location; ParseLoc location;
} WindowFunc; } WindowFunc;
/*
* WindowFuncRunCondition
*
* Represents intermediate OpExprs which will be used by WindowAgg to
* short-circuit execution.
*/
typedef struct WindowFuncRunCondition
{
Expr xpr;
/* PG_OPERATOR OID of the operator */
Oid opno;
/* OID of collation that operator should use */
Oid inputcollid pg_node_attr(query_jumble_ignore);
/*
* true of WindowFunc belongs on the left of the resulting OpExpr or false
* if the WindowFunc is on the right.
*/
bool wfunc_left;
/*
* The Expr being compared to the WindowFunc to use in the OpExpr in the
* WindowAgg's runCondition
*/
Expr *arg;
} WindowFuncRunCondition;
/* /*
* MergeSupportFunc * MergeSupportFunc
* *

View File

@ -250,6 +250,7 @@ extern WindowAggPath *create_windowagg_path(PlannerInfo *root,
Path *subpath, Path *subpath,
PathTarget *target, PathTarget *target,
List *windowFuncs, List *windowFuncs,
List *runCondition,
WindowClause *winclause, WindowClause *winclause,
List *qual, List *qual,
bool topwindow); bool topwindow);

View File

@ -4207,6 +4207,24 @@ WHERE s.c = 1;
-> Seq Scan on empsalary e2 -> Seq Scan on empsalary e2
(14 rows) (14 rows)
-- Ensure the run condition optimization is used in cases where the WindowFunc
-- has a Var from another query level
EXPLAIN (COSTS OFF)
SELECT 1 FROM
(SELECT ntile(s1.x) OVER () AS c
FROM (SELECT (SELECT 1) AS x) AS s1) s
WHERE s.c = 1;
QUERY PLAN
-----------------------------------------------------------------
Subquery Scan on s
Filter: (s.c = 1)
-> WindowAgg
Run Condition: (ntile((InitPlan 1).col1) OVER (?) <= 1)
InitPlan 1
-> Result
-> Result
(7 rows)
-- Tests to ensure we don't push down the run condition when it's not valid to -- Tests to ensure we don't push down the run condition when it's not valid to
-- do so. -- do so.
-- Ensure we don't push down when the frame options show that the window -- Ensure we don't push down when the frame options show that the window

View File

@ -1377,6 +1377,14 @@ SELECT 1 FROM
WHERE e1.empno = e2.empno) s WHERE e1.empno = e2.empno) s
WHERE s.c = 1; WHERE s.c = 1;
-- Ensure the run condition optimization is used in cases where the WindowFunc
-- has a Var from another query level
EXPLAIN (COSTS OFF)
SELECT 1 FROM
(SELECT ntile(s1.x) OVER () AS c
FROM (SELECT (SELECT 1) AS x) AS s1) s
WHERE s.c = 1;
-- Tests to ensure we don't push down the run condition when it's not valid to -- Tests to ensure we don't push down the run condition when it's not valid to
-- do so. -- do so.

View File

@ -3120,6 +3120,7 @@ WindowDef
WindowFunc WindowFunc
WindowFuncExprState WindowFuncExprState
WindowFuncLists WindowFuncLists
WindowFuncRunCondition
WindowObject WindowObject
WindowObjectData WindowObjectData
WindowStatePerAgg WindowStatePerAgg