From 66e12239a9ad0e3bd020bcd53269bea395f34a71 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Tue, 25 Aug 2026 18:28:15 +0300 Subject: [PATCH] Preserve empty-input values when pulling up aggregate subqueries With the Postgres planner, pulling up a correlated scalar aggregate subquery into an INNER join can discard outer rows whose subquery has empty input, even when the comparison against its empty-input value would pass. Use a LEFT join when such rows can pass. Add a match flag and evaluate the comparison above the join, choosing the actual expression for a matched group and its empty-input value otherwise. Retain an INNER join when the empty-input comparison folds to FALSE or NULL. Keep only grouping keys, the match flag, and aggregate values in the grouped subquery. Evaluate expressions around aggregates in the matched CASE branch above the join, so an unused inner group cannot cause a division-by-zero error. Mark their inputs with the LEFT join's nulling relid and keep nested subqueries at their original query level. Derive aggregate empty-input constants from pg_aggregate, including initial transition values and foldable final functions. For cases that need executor evaluation, construct empty-input aggregate sublinks that become InitPlans. Within compound expressions, avoid introducing non-NULL constants, or NULL constants under nonstrict expressions, that could trigger errors such as division by zero during planning. Do not fold user-defined final functions or invoke them speculatively through empty-input InitPlans. Retain correlated execution unless the aggregate's final function is safe on empty input. Expose these checks for the ORCA metadata translator as well. Keep unsupported window functions, correlated targetlists, and clauses whose Vars are unavailable at the join attachment point as SubPlans. Allow nested subqueries inside aggregate arguments and FILTER: these remain at their original query level in the grouped subquery. Retain correlated execution for SubLinks in the surrounding expression. Recognize NULL results of the built-in numeric and int8 AVG/SUM final functions on a NULL internal state. Retain INNER joins for strict comparisons with these aggregates, without an empty-input InitPlan. Use an int8 constant of 1 for the empty-input value of built-in hypothetical rank when all direct arguments are constants. Avoid an empty-input InitPlan, retaining the LEFT join and the CASE that selects the value for unmatched outer rows. Require constant direct arguments so that replacing the aggregate does not suppress their evaluation or errors. Keep the existing safeguards for expressions around aggregates. Add PG regression coverage for empty and matching outer inputs, multiple aggregates, NULL and non-NULL defaults, and unsafe final functions, including parallel execution. Refresh affected PG plans in subselect_gp, eagerfree and the PAX answer files. Updated from open-gpdb/cloudberry commit ad62f729c20ee00447bd926cfafd65310ecf066e (PR #89), originally adapted from open-gpdb/gpdb#397 and GreengageDB/greengage#546. Co-Authored-By: excaliiibur Backpatch-through: REL_2_STABLE --- .../src/test/regress/expected/eagerfree.out | 19 +- src/backend/cdb/cdbsubselect.c | 610 ++++++++++++- src/backend/optimizer/prep/prepjointree.c | 28 + src/include/cdb/cdbsubselect.h | 7 + src/test/regress/expected/eagerfree.out | 25 +- src/test/regress/expected/subselect_gp.out | 809 ++++++++++++++++++ .../expected/subselect_gp_optimizer.out | 809 ++++++++++++++++++ src/test/regress/sql/subselect_gp.sql | 378 ++++++++ 8 files changed, 2649 insertions(+), 36 deletions(-) diff --git a/contrib/pax_storage/src/test/regress/expected/eagerfree.out b/contrib/pax_storage/src/test/regress/expected/eagerfree.out index 5d9b67caff7..64c504ff08f 100644 --- a/contrib/pax_storage/src/test/regress/expected/eagerfree.out +++ b/contrib/pax_storage/src/test/regress/expected/eagerfree.out @@ -1398,21 +1398,20 @@ where i < (select count(*) from smallt where smallt.i = smallt2.i) order by 1,2, explain select smallt2.* from smallt2 where i < (select count(*) from smallt where smallt.i = smallt2.i); - QUERY PLAN ---------------------------------------------------------------------------------------- - Gather Motion 3:1 (slice1; segments: 3) (cost=1.57..3.56 rows=11 width=15) - -> Hash Join (cost=1.57..2.82 rows=4 width=15) + QUERY PLAN +----------------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) (cost=1.59..3.97 rows=17 width=15) + -> Hash Left Join (cost=1.59..2.86 rows=6 width=15) Hash Cond: (smallt2.i = "Expr_SUBQUERY".csq_c0) - Join Filter: (smallt2.i < "Expr_SUBQUERY".csq_c1) + Filter: (smallt2.i < CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) -> Seq Scan on smallt2 (cost=0.00..1.17 rows=17 width=15) - -> Hash (cost=1.55..1.55 rows=1 width=12) - -> Subquery Scan on "Expr_SUBQUERY" (cost=1.50..1.55 rows=1 width=12) - -> HashAggregate (cost=1.50..1.54 rows=1 width=12) + -> Hash (cost=1.54..1.54 rows=3 width=13) + -> Subquery Scan on "Expr_SUBQUERY" (cost=1.50..1.54 rows=3 width=13) + -> HashAggregate (cost=1.50..1.53 rows=3 width=13) Group Key: smallt.i - Filter: (smallt.i < count(*)) -> Seq Scan on smallt (cost=0.00..1.33 rows=33 width=4) Optimizer: Postgres query optimizer -(12 rows) +(11 rows) -- Sort in MergeJoin -- start_ignore diff --git a/src/backend/cdb/cdbsubselect.c b/src/backend/cdb/cdbsubselect.c index 83b908c9ce5..99963c9f306 100644 --- a/src/backend/cdb/cdbsubselect.c +++ b/src/backend/cdb/cdbsubselect.c @@ -16,7 +16,10 @@ #include "access/htup_details.h" #include "access/skey.h" +#include "access/transam.h" +#include "catalog/pg_aggregate.h" #include "catalog/pg_operator.h" +#include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "nodes/makefuncs.h" #include "optimizer/clauses.h" @@ -29,6 +32,8 @@ #include "parser/parse_relation.h" /* addRangeTableEntryForSubquery() */ #include "parser/parsetree.h" /* rt_fetch() */ #include "rewrite/rewriteManip.h" +#include "utils/builtins.h" /* TextDatumGetCString() */ +#include "utils/fmgroids.h" #include "utils/lsyscache.h" /* get_op_btree_interpretation() */ #include "utils/syscache.h" #include "cdb/cdbsubselect.h" /* me */ @@ -42,6 +47,28 @@ static JoinExpr *make_join_expr(Node *larg, int r_rtindex, int join_type); static Node *make_lasj_quals(PlannerInfo *root, SubLink *sublink, int subquery_indx); static Node *add_null_match_clause(Node *clause); +static Expr *build_match_flag_case_expr(Var *flagVar, Expr *matchedExpr, + Expr *defaultExpr); + +/* + * State of replace_agg_with_empty_input_mutator(). + */ +typedef struct EmptyInputContext +{ + Query *subselect; /* the original, correlated subquery */ + bool bareAggref; /* is the whole expression a single Aggref? */ + bool allowNullConst; /* may NULL be folded inside the expression? */ + bool sawNullConst; /* has an Aggref been replaced by NULL? */ + bool allConst; /* has every Aggref been replaced by a Const? */ +} EmptyInputContext; + +static Const *make_init_value_const(Oid type, Oid collation, Datum textInitVal); +static Node *make_empty_input_agg_sublink(Query *subselect, Aggref *aggref); +static Node *replace_agg_with_empty_input_mutator(Node *node, + EmptyInputContext *ctx); +static bool contain_moved_sublinks(Node *node, void *context); +static bool no_match_row_survives(PlannerInfo *root, OpExpr *opexp, + Expr *defaultExpr); typedef struct NonNullableVarsContext { @@ -545,6 +572,18 @@ safe_to_convert_EXPR(SubLink *sublink, ConvertSubqueryToJoinContext *ctx1) if (!subselect->hasAggs) return false; + /* + * A window function cannot survive the pull-up. Without it the subquery + * has a plain aggregate and so produces exactly one row per outer row, and + * the window runs over that single row. The pulled-up subquery is grouped + * by the correlation columns, so the same window would run over every + * group at once and compute a different value. (The rewrite of the + * comparison below would also copy the WindowFunc into a qual above the + * join, where there is no WindowAgg node to evaluate it.) + */ + if (subselect->hasWindowFuncs) + return false; + /** * A LIMIT or OFFSET could interfere with the transformation of the * correlated qual to GROUP BY. (LIMIT >0 in a subquery that contains a @@ -561,12 +600,21 @@ safe_to_convert_EXPR(SubLink *sublink, ConvertSubqueryToJoinContext *ctx1) if (subselect->groupClause) return false; - /** - * If targetlist of the subquery does not contain exactly one element, don't bother. + /* + * Require exactly one target entry, including any resjunk entries + * added for ORDER BY. The pull-up does not handle those extra entries. */ if (list_length(subselect->targetList) != 1) return false; + /** + * Correlation in the targetlist cannot be handled: the pulled-up + * expression (and the empty-input default derived from it) would carry + * upper-level Vars out of the subquery. + */ + if (contain_vars_of_level_or_above((Node *) subselect->targetList, 1)) + return false; + /** * Walk the quals of the subquery to do a more fine grained check as to whether this subquery @@ -630,6 +678,122 @@ convert_EXPR_to_join(PlannerInfo *root, OpExpr *opexp) subselect->jointree->quals = ctx1.innerQual; + /* + * An INNER join drops outer rows that have no matching inner + * rows. Without the pull-up they are kept: the subquery computes + * its expression over empty input, and the comparison may pass. + * COUNT is 0 there, rank() of a hypothetical row is 1, an + * aggregate with a non-NULL initial state returns that state. + * + * So build the expression's value over empty input, see + * replace_agg_with_empty_input_mutator(). If it is known at plan + * time, plug it into the comparison and constant-fold: FALSE or + * NULL means no-match rows cannot pass and the INNER join is + * correct. Otherwise use a LEFT join to keep them, and compute + * the comparison for them against that value. + */ + Expr *defaultExpr; + TargetEntry *flagTLE = NULL; + bool use_left_join; + EmptyInputContext emptyCtx; + + emptyCtx.subselect = (Query *) sublink->subselect; + emptyCtx.bareAggref = IsA(origSubqueryTLE->expr, Aggref); + emptyCtx.allowNullConst = true; + emptyCtx.sawNullConst = false; + emptyCtx.allConst = true; + defaultExpr = (Expr *) + replace_agg_with_empty_input_mutator((Node *) origSubqueryTLE->expr, + &emptyCtx); + + /* + * An aggregate of the subquery used inside a sub-select of its + * targetlist is out of the mutator's reach. The mutator also leaves + * aggregates whose final functions cannot safely run on empty input. + * Leave such a sublink to be planned as a SubPlan. + */ + if (contain_aggs_of_level((Node *) defaultExpr, 0)) + return NULL; + + /* + * NULL can be folded safely through strict functions, as in + * "10 * avg(x)". A nonstrict expression can turn it into a value + * that makes another function fail, e.g. "10 / coalesce(sum(x), 0)". + * Rebuild such defaults with InitPlans instead of NULL constants, + * before any constant folding. Do this even when some aggregates + * already became InitPlans: a NULL-dependent subexpression elsewhere + * in the default could still be folded by the planner. + */ + if (!emptyCtx.bareAggref && emptyCtx.sawNullConst && + contain_nonstrict_functions((Node *) defaultExpr)) + { + emptyCtx.allowNullConst = false; + emptyCtx.sawNullConst = false; + emptyCtx.allConst = true; + defaultExpr = (Expr *) + replace_agg_with_empty_input_mutator((Node *) origSubqueryTLE->expr, + &emptyCtx); + } + + use_left_join = !emptyCtx.allConst || + no_match_row_survives(root, opexp, defaultExpr); + + if (use_left_join) + { + List *aggInputs; + + /* + * SubLinks inside aggregate arguments stay in the grouped + * subquery. Only those in the surrounding expression would + * move to a different query level. + */ + if (contain_moved_sublinks((Node *) origSubqueryTLE->expr, NULL)) + return NULL; + + /* + * After the LEFT join the expression column is NULL both for a + * no-match row and for a matched group whose expression is + * genuinely NULL. To tell them apart, add a constant-TRUE + * match-flag column to the subquery: it can be NULL only when + * the LEFT join found no match and filled the subquery's + * columns with NULLs. + * + * For example: + * + * SELECT a.x FROM a + * WHERE a.x > (SELECT count(*) FROM b WHERE b.y = a.x); + * + * becomes: + * + * SELECT a.x FROM a + * LEFT JOIN ( + * SELECT y, TRUE AS matched, count(*) AS value + * FROM b GROUP BY y + * ) s ON s.y = a.x + * WHERE a.x > CASE WHEN s.matched THEN s.value + * ELSE 0 END; + * + * Project only the aggregates in the grouped subquery. The + * expression around them must be evaluated after the join, for + * matched outer rows only. For example, computing + * 100 / coalesce(sum(x), 0) in the subquery can divide by zero + * for an inner group that no outer row uses. A parallel hash + * join builds its inner side even when the outer side is empty. + * Put the match flag before the aggregate columns. + */ + subselect->targetList = (List *) copyObject(ctx1.targetList); + + flagTLE = makeTargetEntry((Expr *) makeBoolConst(true, false), + list_length(subselect->targetList) + 1, + pstrdup("csq_match_flag"), + false); + subselect->targetList = lappend(subselect->targetList, flagTLE); + aggInputs = pull_var_clause((Node *) origSubqueryTLE->expr, + PVC_INCLUDE_AGGREGATES); + subselect->targetList = add_to_flat_tlist(subselect->targetList, + aggInputs); + } + /** * Construct a new range table entry for the new pulled up subquery. */ @@ -651,19 +815,51 @@ convert_EXPR_to_join(PlannerInfo *root, OpExpr *opexp) join_expr->quals = joinQual; - TargetEntry *subselectAggTLE = (TargetEntry *) list_nth(subselect->targetList, list_length(subselect->targetList) - 1); + /* Refer to the aggregate columns when evaluating the comparison. */ + Expr *matchedExpr = cdbpullup_expr(origSubqueryTLE->expr, + subselect->targetList, NULL, rteIndex); - /** - * modify the op expr to involve the column that has the computed aggregate that needs to compared. - */ - Var *aggVar = (Var *) makeVar(rteIndex, - subselectAggTLE->resno, - exprType((Node *) subselectAggTLE->expr), - exprTypmod((Node *) subselectAggTLE->expr), - exprCollation((Node *) subselectAggTLE->expr), - 0); + Assert(matchedExpr != NULL); + + if (use_left_join) + { + Var *flagVar; + RangeTblEntry *joinRTE; + int joinRTIndex; - list_nth_replace(opexp->args, 1, aggVar); + join_expr->jointype = JOIN_LEFT; + + /* + * Give the outer join a range table entry and mark the Vars the + * comparison uses as nulled by it. Since the removal of + * outerjoin_delayed the planner keeps a clause above an outer + * join only when the clause's Vars carry the join's relid in + * varnullingrels; without this the comparison would be pushed + * down to the subquery rel (or the join removed as useless) and + * the no-match default would never apply. + */ + joinRTE = makeNode(RangeTblEntry); + joinRTE->rtekind = RTE_JOIN; + joinRTE->jointype = JOIN_LEFT; + joinRTE->joinmergedcols = 0; + joinRTE->eref = makeAlias("unnamed_join", NIL); + joinRTE->inFromCl = false; + root->parse->rtable = lappend(root->parse->rtable, joinRTE); + joinRTIndex = list_length(root->parse->rtable); + join_expr->rtindex = joinRTIndex; + + flagVar = (Var *) makeVar(rteIndex, flagTLE->resno, BOOLOID, -1, + InvalidOid, 0); + flagVar->varnullingrels = bms_make_singleton(joinRTIndex); + matchedExpr = (Expr *) add_nulling_relids((Node *) matchedExpr, + NULL, flagVar->varnullingrels); + list_nth_replace(opexp->args, 1, + build_match_flag_case_expr(flagVar, matchedExpr, defaultExpr)); + } + else + { + list_nth_replace(opexp->args, 1, matchedExpr); + } return join_expr; } @@ -671,6 +867,394 @@ convert_EXPR_to_join(PlannerInfo *root, OpExpr *opexp) return NULL; } +/* Check the part of the target expression that moves above the join. */ +static bool +contain_moved_sublinks(Node *node, void *context) +{ + if (node == NULL || IsA(node, Aggref)) + return false; + if (IsA(node, SubLink)) + return true; + return expression_tree_walker(node, contain_moved_sublinks, context); +} + +/* + * Build "CASE WHEN flagVar THEN matchedExpr ELSE defaultExpr END". + * + * flagVar is the subquery's match-flag column: TRUE for a matched group, + * NULL for a null-extended no-match row. + */ +static Expr * +build_match_flag_case_expr(Var *flagVar, Expr *matchedExpr, Expr *defaultExpr) +{ + CaseWhen *casewhen; + CaseExpr *caseexpr; + + Assert(flagVar != NULL); + Assert(matchedExpr != NULL); + Assert(defaultExpr != NULL); + + casewhen = makeNode(CaseWhen); + casewhen->expr = (Expr *) flagVar; + casewhen->result = matchedExpr; + casewhen->location = -1; + + caseexpr = makeNode(CaseExpr); + caseexpr->casetype = exprType((Node *) matchedExpr); + caseexpr->casecollid = exprCollation((Node *) matchedExpr); + caseexpr->arg = NULL; + caseexpr->args = list_make1(casewhen); + caseexpr->defresult = defaultExpr; + caseexpr->location = -1; + + return (Expr *) caseexpr; +} + +/* + * Build a Const of the given type from an aggregate's initial value text, + * as GetAggInitVal() does for the executor. + */ +static Const * +make_init_value_const(Oid type, Oid collation, Datum textInitVal) +{ + Oid typinput; + Oid typioparam; + int16 typlen; + bool typbyval; + char *strInitVal; + Datum value; + + getTypeInputInfo(type, &typinput, &typioparam); + get_typlenbyval(type, &typlen, &typbyval); + strInitVal = TextDatumGetCString(textInitVal); + value = OidInputFunctionCall(typinput, strInitVal, typioparam, -1); + pfree(strInitVal); + + return makeConst(type, -1, collation, typlen, value, false, typbyval); +} + +/* + * get_agg_empty_input_const + * + * If the value the aggregate returns over empty input follows from its + * pg_aggregate entry or a known built-in final function, store it in *result + * and return true. aggtype is the aggregate's result type and argtypes the + * types of its aggregated arguments, as resolved for the call. Direct + * arguments must be constants: the executor evaluates them even over empty + * input, so replacing the aggregate must not suppress their evaluation. + * + * Over empty input the result is the initial transition value, passed + * through the final function if there is one (see finalize_aggregate()). + * Without a final function that is the initial value itself, like 0 for + * count(); with a strict one and no initial value it is NULL, like for + * sum() and min(). Otherwise it is the final function applied to the + * initial value, e.g. NULL for avg() and its initial state {0,0}: a call of + * an immutable function on constants, which constant folding evaluates like + * any other. Only built-in aggregates are folded this way: a user-defined + * final function can fail on empty input even when the actual input is never + * empty, or require an aggregate execution context. + * A final function over an internal state expects to run inside + * an aggregate, and one of an ordered-set aggregate also takes the direct + * arguments; those are not called here. + */ +bool +get_agg_empty_input_const(Oid aggfnoid, Oid aggtype, Oid aggcollid, + Oid inputcollid, List *argtypes, List *directargs, + Const **result) +{ + HeapTuple aggTuple; + Form_pg_aggregate aggform; + Datum textInitVal; + bool initValueIsNull; + bool derived = false; + ListCell *lc; + + foreach(lc, directargs) + { + if (!IsA(lfirst(lc), Const)) + return false; + } + + aggTuple = SearchSysCache1(AGGFNOID, ObjectIdGetDatum(aggfnoid)); + if (!HeapTupleIsValid(aggTuple)) + elog(ERROR, "cache lookup failed for aggregate %u", aggfnoid); + aggform = (Form_pg_aggregate) GETSTRUCT(aggTuple); + + textInitVal = SysCacheGetAttr(AGGFNOID, aggTuple, + Anum_pg_aggregate_agginitval, + &initValueIsNull); + + if (initValueIsNull && + (!OidIsValid(aggform->aggfinalfn) || func_strict(aggform->aggfinalfn))) + { + *result = makeNullConst(aggtype, -1, aggcollid); + derived = true; + } + else if (initValueIsNull && aggfnoid < FirstNormalObjectId && + aggform->aggtranstype == INTERNALOID && + (aggform->aggfinalfn == F_NUMERIC_AVG || + aggform->aggfinalfn == F_NUMERIC_SUM || + aggform->aggfinalfn == F_NUMERIC_POLY_AVG || + aggform->aggfinalfn == F_NUMERIC_POLY_SUM)) + { + /* + * AVG and SUM over numeric and int8 return NULL for a NULL internal + * state. Their final functions are not marked strict, so the catalog + * check above cannot establish this. Use the known result without + * calling an internal-state final function during planning. + */ + *result = makeNullConst(aggtype, -1, aggcollid); + derived = true; + } + else if (initValueIsNull && aggfnoid == F_RANK_ANY) + { + /* A hypothetical row has rank 1 when there are no input rows. */ + *result = makeConst(INT8OID, -1, InvalidOid, sizeof(int64), + Int64GetDatum(1), false, FLOAT8PASSBYVAL); + derived = true; + } + else if (IsPolymorphicType(aggtype)) + { + /* cannot build a value of an unresolved type */ + } + else if (!OidIsValid(aggform->aggfinalfn)) + { + /* The transition type is the result type then. */ + *result = make_init_value_const(aggtype, aggcollid, textInitVal); + derived = true; + } + else if (aggfnoid < FirstNormalObjectId && + aggform->aggkind == AGGKIND_NORMAL && + aggform->aggtranstype != INTERNALOID && + !IsPolymorphicType(aggform->aggtranstype)) + { + List *args; + bool argsResolved = true; + Node *folded; + + if (initValueIsNull) + args = list_make1(makeNullConst(aggform->aggtranstype, -1, + InvalidOid)); + else + args = list_make1(make_init_value_const(aggform->aggtranstype, + InvalidOid, textInitVal)); + + /* the extra arguments of the final function are NULLs */ + if (aggform->aggfinalextra) + { + foreach(lc, argtypes) + { + Oid argtype = lfirst_oid(lc); + + if (IsPolymorphicType(argtype) || argtype == ANYOID) + argsResolved = false; + args = lappend(args, makeNullConst(argtype, -1, InvalidOid)); + } + } + + if (argsResolved) + { + folded = eval_const_expressions(NULL, (Node *) + makeFuncExpr(aggform->aggfinalfn, + aggtype, args, + aggcollid, + inputcollid, + COERCE_EXPLICIT_CALL)); + if (IsA(folded, Const)) + { + *result = (Const *) folded; + derived = true; + } + } + } + + ReleaseSysCache(aggTuple); + + return derived; +} + +/* + * agg_empty_input_is_null + * Is the aggregate known to return NULL over empty input, whatever + * the types it is called with? + */ +bool +agg_empty_input_is_null(Oid aggfnoid) +{ + HeapTuple procTuple; + Form_pg_proc procform; + List *argtypes = NIL; + Const *emptyConst; + int i; + + procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(aggfnoid)); + if (!HeapTupleIsValid(procTuple)) + elog(ERROR, "cache lookup failed for function %u", aggfnoid); + procform = (Form_pg_proc) GETSTRUCT(procTuple); + + for (i = 0; i < procform->pronargs; i++) + argtypes = lappend_oid(argtypes, procform->proargtypes.values[i]); + + /* The empty-input value does not depend on the collation. */ + if (!get_agg_empty_input_const(aggfnoid, procform->prorettype, + InvalidOid, InvalidOid, argtypes, NIL, + &emptyConst)) + emptyConst = NULL; + + ReleaseSysCache(procTuple); + + return emptyConst != NULL && emptyConst->constisnull; +} + +/* + * Is it safe to evaluate the aggregate over empty input speculatively? + * Built-in aggregates support this. For user-defined aggregates, a final + * function may throw on the initial state or have observable side effects; + * allow a final function only when strictness guarantees it will not be + * called. An aggregate without a final function returns its initial state. + */ +bool +agg_empty_input_is_safe(Oid aggfnoid) +{ + HeapTuple aggTuple; + Form_pg_aggregate aggform; + bool initValueIsNull; + bool safe; + + if (aggfnoid < FirstNormalObjectId) + return true; + + aggTuple = SearchSysCache1(AGGFNOID, ObjectIdGetDatum(aggfnoid)); + if (!HeapTupleIsValid(aggTuple)) + elog(ERROR, "cache lookup failed for aggregate %u", aggfnoid); + aggform = (Form_pg_aggregate) GETSTRUCT(aggTuple); + (void) SysCacheGetAttr(AGGFNOID, aggTuple, + Anum_pg_aggregate_agginitval, &initValueIsNull); + safe = !OidIsValid(aggform->aggfinalfn) || + (initValueIsNull && func_strict(aggform->aggfinalfn)); + ReleaseSysCache(aggTuple); + return safe; +} + +/* + * make_empty_input_agg_sublink + * + * Build "(SELECT aggref FROM WHERE false)": the + * aggregate computed by the executor over empty input. It is not + * correlated through the WHERE clause any more, so it becomes an InitPlan + * and runs once. + */ +static Node * +make_empty_input_agg_sublink(Query *subselect, Aggref *aggref) +{ + Query *emptyQuery = copyObject(subselect); + SubLink *emptySublink; + + emptyQuery->targetList = + list_make1(makeTargetEntry((Expr *) copyObject(aggref), 1, + pstrdup("csq_empty_input"), false)); + emptyQuery->jointree->quals = (Node *) makeBoolConst(false, false); + emptyQuery->sortClause = NIL; + emptyQuery->distinctClause = NIL; + + emptySublink = makeNode(SubLink); + emptySublink->subLinkType = EXPR_SUBLINK; + emptySublink->subLinkId = 0; + emptySublink->testexpr = NULL; + emptySublink->operName = NIL; + emptySublink->subselect = (Node *) emptyQuery; + emptySublink->location = -1; + + return (Node *) emptySublink; +} + +/* + * Build the value the subquery's expression takes over empty input, by + * replacing every aggregate of the subquery with its own empty-input value. + * + * Use a known Const when the aggregate is the whole expression. Within a + * larger expression, only NULL is a candidate: a non-NULL Const could make + * "10 / count(*)" become "10 / 0" and fail during planning, even when every + * outer row has a match or the outer table is empty. The caller checks the + * resulting expression for nonstrict constructs before folding it. If any + * are present, it repeats the mutation with allowNullConst = false, because + * "10 / coalesce(sum(x), 0)" must not become "10 / coalesce(NULL, 0)" either. + * + * Everywhere else the aggregate becomes a sublink computing it over empty + * input. Its value is a Param at plan time, so the expression around it + * is evaluated at run time, and only for the outer rows without a match. + */ +static Node * +replace_agg_with_empty_input_mutator(Node *node, EmptyInputContext *ctx) +{ + if (node == NULL) + return NULL; + + if (IsA(node, Aggref) && ((Aggref *) node)->agglevelsup == 0) + { + Aggref *aggref = (Aggref *) node; + Const *emptyConst; + + /* + * An empty-input InitPlan may be executed eagerly before dispatch, + * even when the CASE never needs its result. Keep this Aggref so + * convert_EXPR_to_join() declines the pull-up instead of calling a + * user-defined final function on an input it would never see. + */ + if (!agg_empty_input_is_safe(aggref->aggfnoid)) + return copyObject(node); + + if ((ctx->bareAggref || ctx->allowNullConst) && + get_agg_empty_input_const(aggref->aggfnoid, aggref->aggtype, + aggref->aggcollid, aggref->inputcollid, + aggref->aggargtypes, aggref->aggdirectargs, + &emptyConst) && + (ctx->bareAggref || emptyConst->constisnull)) + { + ctx->sawNullConst |= emptyConst->constisnull; + return (Node *) emptyConst; + } + + ctx->allConst = false; + return make_empty_input_agg_sublink(ctx->subselect, aggref); + } + + return expression_tree_mutator(node, replace_agg_with_empty_input_mutator, + (void *) ctx); +} + +/* + * no_match_row_survives + * + * Check whether an outer row with no matching inner rows can pass the + * comparison. Replace the subquery with defaultExpr, its value over empty + * input, and try to evaluate the comparison at plan time. + * + * WHERE keeps a row only when the comparison is TRUE. If the result is + * FALSE or NULL, return false: an INNER join can discard such rows. + * Otherwise return true, including when the result cannot be determined + * at plan time, so the caller uses a LEFT join to preserve them. + */ +static bool +no_match_row_survives(PlannerInfo *root, OpExpr *opexp, Expr *defaultExpr) +{ + OpExpr *testexpr = (OpExpr *) copyObject(opexp); + Node *folded; + + list_nth_replace(testexpr->args, 1, copyObject(defaultExpr)); + folded = eval_const_expressions(root, (Node *) testexpr); + + if (IsA(folded, Const)) + { + Const *c = (Const *) folded; + + if (c->constisnull || !DatumGetBool(c->constvalue)) + return false; + } + + return true; +} + /* NOTIN subquery transformation -start */ /* check if NOT IN conversion to antijoin is possible */ diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index dde7402c572..fee519a673d 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -858,11 +858,39 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, if (IsA(rarg, SubLink)) { + /* + * The pulled-up join is spliced in at *jtlink1, and in the + * LEFT-join case the comparison itself moves there too, so + * every Var of this query level used by the clause must be + * available at that attach point. Otherwise (e.g. an outer + * join's ON clause referencing the non-nullable side) leave + * the sublink to be planned as a SubPlan. + */ + if (!bms_is_subset(pull_varnos(root, node), available_rels1)) + return node; + j = convert_EXPR_to_join(root, opexp); if (j) { /* Yes, insert the new join node into the join tree */ j->larg = *jtlink1; + + if (j->jointype == JOIN_LEFT) + { + /* + * No-match-preserving pull-up (see convert_EXPR_to_join). + * opexp must run ABOVE the LEFT JOIN, not as its join + * condition: as a join qual a matched row that fails it + * would be treated as unmatched, null-extended, and let + * back in by the no-match default of the CASE built by + * convert_EXPR_to_join. Wrap the join in a FromExpr so + * opexp stays a post-join filter. + */ + *jtlink1 = (Node *) makeFromExpr(list_make1(j), node); + return NULL; + } + + /* Inner-join case: opexp stays as an ordinary qual. */ *jtlink1 = (Node *) j; } return node; diff --git a/src/include/cdb/cdbsubselect.h b/src/include/cdb/cdbsubselect.h index 41df4f539c0..96e270d0be6 100644 --- a/src/include/cdb/cdbsubselect.h +++ b/src/include/cdb/cdbsubselect.h @@ -27,4 +27,11 @@ extern JoinExpr *convert_IN_to_antijoin(PlannerInfo *root, SubLink *sublink, Rel extern void cdbsubselect_drop_orderby(Query *subselect); extern void cdbsubselect_drop_distinct(Query *subselect); +extern bool get_agg_empty_input_const(Oid aggfnoid, Oid aggtype, Oid aggcollid, + Oid inputcollid, List *argtypes, List *directargs, + Const **result); + +extern bool agg_empty_input_is_null(Oid aggfnoid); +extern bool agg_empty_input_is_safe(Oid aggfnoid); + #endif /* CDBSUBSELECT_H */ diff --git a/src/test/regress/expected/eagerfree.out b/src/test/regress/expected/eagerfree.out index 9658e9a8dd6..c5a0dd5adfd 100644 --- a/src/test/regress/expected/eagerfree.out +++ b/src/test/regress/expected/eagerfree.out @@ -1379,21 +1379,20 @@ where i < (select count(*) from smallt where smallt.i = smallt2.i) order by 1,2, explain select smallt2.* from smallt2 where i < (select count(*) from smallt where smallt.i = smallt2.i); - QUERY PLAN ---------------------------------------------------------------------------------------- - Gather Motion 3:1 (slice1; segments: 3) (cost=5.10..8.08 rows=17 width=15) - -> Hash Join (cost=5.10..8.08 rows=6 width=15) - Hash Cond: smallt2.i = "Expr_SUBQUERY".csq_c0 - Join Filter: smallt2.i < "Expr_SUBQUERY".csq_c1 - -> Seq Scan on smallt2 (cost=0.00..2.50 rows=17 width=15) - -> Hash (cost=4.97..4.97 rows=4 width=12) - -> Subquery Scan on "Expr_SUBQUERY" (cost=4.75..4.97 rows=4 width=12) - -> HashAggregate (cost=4.75..4.88 rows=4 width=12) - Filter: smallt.i < count(*) + QUERY PLAN +----------------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) (cost=1.59..3.97 rows=17 width=15) + -> Hash Left Join (cost=1.59..2.86 rows=6 width=15) + Hash Cond: (smallt2.i = "Expr_SUBQUERY".csq_c0) + Filter: (smallt2.i < CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Seq Scan on smallt2 (cost=0.00..1.17 rows=17 width=15) + -> Hash (cost=1.54..1.54 rows=3 width=13) + -> Subquery Scan on "Expr_SUBQUERY" (cost=1.50..1.54 rows=3 width=13) + -> HashAggregate (cost=1.50..1.53 rows=3 width=13) Group Key: smallt.i - -> Seq Scan on smallt (cost=0.00..4.00 rows=34 width=4) + -> Seq Scan on smallt (cost=0.00..1.33 rows=33 width=4) Optimizer: Postgres query optimizer -(12 rows) +(11 rows) -- Sort in MergeJoin -- start_ignore diff --git a/src/test/regress/expected/subselect_gp.out b/src/test/regress/expected/subselect_gp.out index 3e4ea0d16c9..65606dd2210 100644 --- a/src/test/regress/expected/subselect_gp.out +++ b/src/test/regress/expected/subselect_gp.out @@ -4015,3 +4015,812 @@ reset optimizer; drop table outer_foo; drop table inner_bar; drop table t; +-- A window function in a correlated aggregate subquery: the pull-up must +-- bail out, because no join row carries the value a no-match row would see +-- and a WindowFunc cannot be evaluated outside the subquery. The sublink +-- runs as a SubPlan. +create table t_csq_win_out(a int, b int) distributed by (a); +create table t_csq_win_in(a int) distributed by (a); +insert into t_csq_win_out values (1, 5); +set optimizer=off; +-- count()=0 over empty input, then count() over () = 1 over the single +-- aggregate row: "1 > 0 + 1" is false and no row may be returned +select * from t_csq_win_out +where a > (select count(*) + count(*) over () from t_csq_win_in where t_csq_win_in.a = t_csq_win_out.b); + a | b +---+--- +(0 rows) + +reset optimizer; +drop table t_csq_win_out, t_csq_win_in; +-- A compound COUNT expression must not become 1/0 during planning when +-- the outer table is empty and the subquery is never evaluated. +create table t_csq_div_out(a int) distributed by (a); +create table t_csq_div_in(a int) distributed by (a); +set optimizer=off; +select * from t_csq_div_out +where a > (select 1 / count(*) from t_csq_div_in where t_csq_div_in.a = t_csq_div_out.a); + a +--- +(0 rows) + +reset optimizer; +drop table t_csq_div_out, t_csq_div_in; +-- Scalar aggregate pull-up must preserve empty-input values in WHERE. +set optimizer to off; +create table pullup_agg_outer (a int) distributed by (a); +create table pullup_agg_inner (a int, b int) distributed by (a); +insert into pullup_agg_outer values (1), (2), (3); +insert into pullup_agg_inner values (1, 10), (1, 20), (2, NULL); +analyze pullup_agg_outer; +analyze pullup_agg_inner; +-- Only the missing group passes. A matched group that fails the comparison +-- must not be turned into an unmatched row by putting the comparison in ON. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + Filter: (0 = CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- FALSE and NULL empty-input comparisons can still use an INNER join. +explain (costs off) select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +-------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: (1 = count(*)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres-based planner +(11 rows) + +select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 +(1 row) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +-------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: (40 > sum(i.b)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres-based planner +(11 rows) + +select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- COUNT(expr) ignores NULL inputs, so both the all-NULL and missing groups pass. +select a from pullup_agg_outer o + where 0 = (select count(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- A matched group may return NULL while the empty-input value is 0. +-- The match flag must preserve that NULL instead of substituting the default. +select a from pullup_agg_outer o + where 0 = (select case when count(*) = 0 then 0 else sum(i.b) end + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- Evaluate the whole expression over empty input, not just the bare aggregate. +select a from pullup_agg_outer o + where 1 = (select count(*) + 1 from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- Non-NULL defaults also occur in aggregates other than COUNT. +select a from pullup_agg_outer o + where 0 = (select regr_count(i.b, i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +create aggregate pullup_sum_from_five(int) + (sfunc = int4pl, stype = int4, initcond = '5'); +select a from pullup_agg_outer o + where 5 = (select pullup_sum_from_five(i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Hypothetical rank returns 1 on empty input, regardless of the sort values. +select a from pullup_agg_outer o + where 1 = (select rank(15) within group (order by i.b) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Do not evaluate a hypothetical zero divisor during planning, even when +-- NULL is turned into 0 by COALESCE or another aggregate already uses an InitPlan. +create table pullup_agg_div_outer (a int) distributed by (a); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- +(0 rows) + +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- +(0 rows) + +select a from pullup_agg_div_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +-- All outer rows have a match, so no empty-input division is needed. +insert into pullup_agg_div_outer values (1); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A missing group must still raise the original error at execution time. +insert into pullup_agg_div_outer values (3); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; +ERROR: division by zero +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; +ERROR: division by zero +-- Parallel hash joins build the inner side even when no outer row uses it. +-- Keep scalar expressions above the join: the all-NULL group for a = 2 +-- must not make COALESCE produce a zero divisor for an unused inner group. +begin; +set local enable_parallel = on; +set local min_parallel_table_scan_size = 0; +set local min_parallel_index_scan_size = 0; +set local parallel_setup_cost = 0; +create table pullup_agg_parallel_outer (a int) with (parallel_workers = 2) + distributed by (a); +alter table pullup_agg_inner set (parallel_workers = 2); +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +insert into pullup_agg_parallel_outer values (1); +select a from pullup_agg_parallel_outer o + where a < (select 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +rollback; +-- AVG and SUM over numeric and bigint are NULL on empty input, even though +-- their internal-state final functions are not declared strict. Keep INNER joins. +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::numeric) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > avg((i.b)::numeric)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::bigint) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > avg((i.b)::bigint)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::numeric) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > sum((i.b)::numeric)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::bigint) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > sum((i.b)::bigint)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +select a from pullup_agg_outer o + where 100 > (select avg(i.b::numeric) + avg(i.b::bigint) + + sum(i.b::numeric) + sum(i.b::bigint) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- COALESCE still needs the empty-input value for the missing group. +select a from pullup_agg_outer o + where 0 = (select coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Do not fold or evaluate division by zero for an unused group. +select a from pullup_agg_outer o + where a = 1 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A missing group must still raise the error when its value is used. +select a from pullup_agg_outer o + where a = 3 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +ERROR: division by zero +-- Nested subqueries in aggregate arguments and FILTER keep their query level. +-- Pull up the scalar aggregate while leaving its nested SubPlan in place. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + Filter: (0 = CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on pullup_agg_inner i + SubPlan 1 + -> Limit + -> Result + Filter: (j.a = i.a) + -> Materialize + -> Broadcast Motion 3:3 (slice2; segments: 3) + -> Seq Scan on pullup_agg_inner j + Optimizer: Postgres query optimizer +(18 rows) + +select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +select a from pullup_agg_outer o + where 1 = (select count(*) filter + (where exists (select 1 from pullup_agg_inner j + where j.a = i.a and j.b > 15)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- A CTE defined by the aggregate query remains visible to its argument. +select a from pullup_agg_outer o + where 1 = (with c as materialized (select 7 as v) + select count((select v from c)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- A SubLink outside the aggregate would move; retain correlated execution. +select a from pullup_agg_outer o + where 7 = (select count(*) + (select 7) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +drop table pullup_agg_div_outer, pullup_agg_outer, pullup_agg_inner; +drop aggregate pullup_sum_from_five(int); +reset optimizer; +-- +-- Test the COUNT bug for aggregates other than count() that return a non-NULL +-- value on empty input: regr_count(), hypothetical-set aggregates and +-- aggregates with a non-NULL initial state, and for expressions over +-- aggregates. A correlated subquery must return its value over empty input +-- for an outer row without matching inner rows, not NULL, and must not lose +-- such a row in WHERE. The planner decorrelates these subqueries into an +-- outer join and computes that value for the rows without a match. +-- +create table empty_agg_outer (a int, b int, d int) distributed by (a); +create table empty_agg_inner (a int, b int) distributed by (a); +insert into empty_agg_outer values (3,1,1), (1,9,5), (0,2,7), (5,5,1), (2,4,9); +insert into empty_agg_inner values (1,10), (2,20), (1,30); +analyze empty_agg_outer; +analyze empty_agg_inner; +create aggregate sum_from_zero(int) (sfunc = int4pl, stype = int4, initcond = '0'); +-- values on empty input +select regr_count(a, b), rank(5) within group (order by b), + cume_dist(5) within group (order by b), sum_from_zero(a), avg(a), + coalesce(sum(b), 0), sum(b) is null + from empty_agg_inner where false; + regr_count | rank | cume_dist | sum_from_zero | avg | coalesce | ?column? +------------+------+-----------+---------------+-----+----------+---------- + 0 | 1 | 1 | 0 | | 0 | t +(1 row) + +set optimizer to off; +select a, d, + (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) as regr, + (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) as rnk, + (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) as sfz, + (select coalesce(sum(i.b), 0) from empty_agg_inner i where i.a = o.d) as cs, + (select sum(i.b) is null from empty_agg_inner i where i.a = o.d) as sn, + (select avg(i.b) from empty_agg_inner i where i.a = o.d) as avg + from empty_agg_outer o order by 1, 2; + a | d | regr | rnk | sfz | cs | sn | avg +---+---+------+-----+-----+----+----+--------------------- + 0 | 7 | 0 | 1 | 0 | 0 | t | + 1 | 5 | 0 | 1 | 0 | 0 | t | + 2 | 9 | 0 | 1 | 0 | 0 | t | + 3 | 1 | 2 | 1 | 2 | 40 | f | 20.0000000000000000 + 5 | 1 | 2 | 1 | 2 | 40 | f | 20.0000000000000000 +(5 rows) + +-- the same in WHERE: the rows without a match must be kept +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d); + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Filter: (o.a > CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(13 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 1 | 9 | 5 + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(4 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select count(*) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 1 | 9 | 5 + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(4 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select count(*) + 1 from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 5 | 5 | 1 +(2 rows) + +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Filter: (o.a > CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '1'::bigint END) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> GroupAggregate + Group Key: i.a + -> Sort + Sort Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(15 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +select a, d from empty_agg_outer o + where o.a in (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) + order by 1, 2; + a | d +---+--- + 0 | 7 +(1 row) + +-- Constant direct arguments let rank use 1 without an empty-input aggregate. +-- NULL and multiple direct arguments have the same empty-input rank. +create function empty_rank_arg(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select rank(NULL::int) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 5 | 5 | 1 +(2 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(5, 5) within group (order by i.b, i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(empty_rank_arg(2)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +-- A nonconstant direct argument must still be evaluated on empty input. +select a from empty_agg_outer o where d = 9 + and a > (select rank(empty_rank_arg(0)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +CONTEXT: PL/pgSQL function empty_rank_arg(integer) line 3 at RETURN +-- The surrounding expression must not fold its empty-input zero divisor. +select a from empty_agg_outer o where d = 1 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d) order by 1; + a +--- + 3 + 5 +(2 rows) + +select a from empty_agg_outer o where d = 9 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +drop function empty_rank_arg(int); +-- The value over empty input is computed for the rows without a match only: +-- "10 / count(*)" divides by zero there, as the correlated subquery does, but +-- must not fail when every outer row has a match. +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d); +ERROR: division by zero (seg0 slice1 127.0.0.1:6102 pid=72089) +select a, b, d from empty_agg_outer o + where o.d = 1 + and o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d) + order by 1; + a | b | d +---+---+--- +(0 rows) + +-- A NULL empty-input value is not safe to fold inside an expression either: +-- COALESCE can turn it into a zero divisor. Do not evaluate the expression +-- when the outer table is empty or every outer row has matching inner rows. +create table empty_agg_null_outer (a int) distributed by (a); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +-- An existing InitPlan for count() must not hide an unsafe NULL substitution. +select a from empty_agg_null_outer o + where a < (select count(*) + 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +insert into empty_agg_null_outer values (1); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A row without a match must still raise the error at execution time. +insert into empty_agg_null_outer values (9); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +ERROR: division by zero +drop table empty_agg_null_outer; +-- avg() and sum() return NULL on empty input, and a comparison with NULL is +-- not true, so the rows without a match can still be dropped by an inner join +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select avg(i.b) from empty_agg_inner i where i.a = o.d); + QUERY PLAN +---------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Join Filter: ((o.a)::numeric > "Expr_SUBQUERY".csq_c1) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(13 rows) + +-- A final function may fail on empty input even when the actual input is +-- nonempty. Neither metadata lookup nor decorrelation may call it early. +create function empty_agg_divide_final(int) returns int + language sql immutable strict as 'select 10 / $1'; +create aggregate empty_agg_divide(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_final); +-- Keep all rows in this table matched, independently of qual evaluation order. +create table empty_agg_matched as + select * from empty_agg_outer where d = 1 distributed by (a); +set optimizer to off; +select empty_agg_divide(a) from empty_agg_inner; + empty_agg_divide +------------------ + 2 +(1 row) + +select a, (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + from empty_agg_matched o order by 1; + a | empty_agg_divide +---+------------------ + 3 | 5 + 5 | 5 +(2 rows) + +select a from empty_agg_matched o + where a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; + a +--- + 3 +(1 row) + +select a from empty_agg_matched o + where a in (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; + a +--- + 5 +(1 row) + +-- Empty input must still raise the original error at execution time. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +CONTEXT: SQL function "empty_agg_divide_final" statement 1 +-- A STABLE PL/pgSQL final function is not folded while loading metadata. +-- It still must not run on empty input unless the correlated input is empty. +create function empty_agg_divide_stable_final(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +create aggregate empty_agg_divide_stable(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_stable_final); +set optimizer to off; +select empty_agg_divide_stable(a) from empty_agg_inner; + empty_agg_divide_stable +------------------------- + 2 +(1 row) + +select a, (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) + from empty_agg_matched o order by 1; + a | empty_agg_divide_stable +---+------------------------- + 3 | 5 + 5 | 5 +(2 rows) + +select a from empty_agg_matched o + where a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; + a +--- + 3 +(1 row) + +select a from empty_agg_matched o + where a in (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; + a +--- + 5 +(1 row) + +-- A genuinely empty input still invokes the final function and fails. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d); +ERROR: division by zero +CONTEXT: PL/pgSQL function empty_agg_divide_stable_final(integer) line 3 at RETURN +drop aggregate empty_agg_divide_stable(int); +drop function empty_agg_divide_stable_final(int); +drop table empty_agg_matched; +drop aggregate empty_agg_divide(int); +drop function empty_agg_divide_final(int); +reset optimizer; +drop aggregate sum_from_zero(int); +drop table empty_agg_outer, empty_agg_inner; diff --git a/src/test/regress/expected/subselect_gp_optimizer.out b/src/test/regress/expected/subselect_gp_optimizer.out index c847505320f..766cbf52ed4 100644 --- a/src/test/regress/expected/subselect_gp_optimizer.out +++ b/src/test/regress/expected/subselect_gp_optimizer.out @@ -4153,3 +4153,812 @@ reset optimizer; drop table outer_foo; drop table inner_bar; drop table t; +-- A window function in a correlated aggregate subquery: the pull-up must +-- bail out, because no join row carries the value a no-match row would see +-- and a WindowFunc cannot be evaluated outside the subquery. The sublink +-- runs as a SubPlan. +create table t_csq_win_out(a int, b int) distributed by (a); +create table t_csq_win_in(a int) distributed by (a); +insert into t_csq_win_out values (1, 5); +set optimizer=off; +-- count()=0 over empty input, then count() over () = 1 over the single +-- aggregate row: "1 > 0 + 1" is false and no row may be returned +select * from t_csq_win_out +where a > (select count(*) + count(*) over () from t_csq_win_in where t_csq_win_in.a = t_csq_win_out.b); + a | b +---+--- +(0 rows) + +reset optimizer; +drop table t_csq_win_out, t_csq_win_in; +-- A compound COUNT expression must not become 1/0 during planning when +-- the outer table is empty and the subquery is never evaluated. +create table t_csq_div_out(a int) distributed by (a); +create table t_csq_div_in(a int) distributed by (a); +set optimizer=off; +select * from t_csq_div_out +where a > (select 1 / count(*) from t_csq_div_in where t_csq_div_in.a = t_csq_div_out.a); + a +--- +(0 rows) + +reset optimizer; +drop table t_csq_div_out, t_csq_div_in; +-- Scalar aggregate pull-up must preserve empty-input values in WHERE. +set optimizer to off; +create table pullup_agg_outer (a int) distributed by (a); +create table pullup_agg_inner (a int, b int) distributed by (a); +insert into pullup_agg_outer values (1), (2), (3); +insert into pullup_agg_inner values (1, 10), (1, 20), (2, NULL); +analyze pullup_agg_outer; +analyze pullup_agg_inner; +-- Only the missing group passes. A matched group that fails the comparison +-- must not be turned into an unmatched row by putting the comparison in ON. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + Filter: (0 = CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- FALSE and NULL empty-input comparisons can still use an INNER join. +explain (costs off) select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +-------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: (1 = count(*)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres-based planner +(11 rows) + +select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 +(1 row) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +-------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: (40 > sum(i.b)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres-based planner +(11 rows) + +select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- COUNT(expr) ignores NULL inputs, so both the all-NULL and missing groups pass. +select a from pullup_agg_outer o + where 0 = (select count(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- A matched group may return NULL while the empty-input value is 0. +-- The match flag must preserve that NULL instead of substituting the default. +select a from pullup_agg_outer o + where 0 = (select case when count(*) = 0 then 0 else sum(i.b) end + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- Evaluate the whole expression over empty input, not just the bare aggregate. +select a from pullup_agg_outer o + where 1 = (select count(*) + 1 from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- Non-NULL defaults also occur in aggregates other than COUNT. +select a from pullup_agg_outer o + where 0 = (select regr_count(i.b, i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +create aggregate pullup_sum_from_five(int) + (sfunc = int4pl, stype = int4, initcond = '5'); +select a from pullup_agg_outer o + where 5 = (select pullup_sum_from_five(i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Hypothetical rank returns 1 on empty input, regardless of the sort values. +select a from pullup_agg_outer o + where 1 = (select rank(15) within group (order by i.b) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Do not evaluate a hypothetical zero divisor during planning, even when +-- NULL is turned into 0 by COALESCE or another aggregate already uses an InitPlan. +create table pullup_agg_div_outer (a int) distributed by (a); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- +(0 rows) + +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- +(0 rows) + +select a from pullup_agg_div_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +-- All outer rows have a match, so no empty-input division is needed. +insert into pullup_agg_div_outer values (1); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A missing group must still raise the original error at execution time. +insert into pullup_agg_div_outer values (3); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; +ERROR: division by zero +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; +ERROR: division by zero +-- Parallel hash joins build the inner side even when no outer row uses it. +-- Keep scalar expressions above the join: the all-NULL group for a = 2 +-- must not make COALESCE produce a zero divisor for an unused inner group. +begin; +set local enable_parallel = on; +set local min_parallel_table_scan_size = 0; +set local min_parallel_index_scan_size = 0; +set local parallel_setup_cost = 0; +create table pullup_agg_parallel_outer (a int) with (parallel_workers = 2) + distributed by (a); +alter table pullup_agg_inner set (parallel_workers = 2); +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +insert into pullup_agg_parallel_outer values (1); +select a from pullup_agg_parallel_outer o + where a < (select 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +rollback; +-- AVG and SUM over numeric and bigint are NULL on empty input, even though +-- their internal-state final functions are not declared strict. Keep INNER joins. +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::numeric) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > avg((i.b)::numeric)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::bigint) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > avg((i.b)::bigint)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::numeric) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > sum((i.b)::numeric)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::bigint) from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +------------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + Filter: ('40'::numeric > sum((i.b)::bigint)) + -> Seq Scan on pullup_agg_inner i + Optimizer: Postgres query optimizer +(11 rows) + +select a from pullup_agg_outer o + where 100 > (select avg(i.b::numeric) + avg(i.b::bigint) + + sum(i.b::numeric) + sum(i.b::bigint) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- COALESCE still needs the empty-input value for the missing group. +select a from pullup_agg_outer o + where 0 = (select coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- Do not fold or evaluate division by zero for an unused group. +select a from pullup_agg_outer o + where a = 1 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A missing group must still raise the error when its value is used. +select a from pullup_agg_outer o + where a = 3 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +ERROR: division by zero +-- Nested subqueries in aggregate arguments and FILTER keep their query level. +-- Pull up the scalar aggregate while leaving its nested SubPlan in place. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.a = "Expr_SUBQUERY".csq_c0) + Filter: (0 = CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Seq Scan on pullup_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on pullup_agg_inner i + SubPlan 1 + -> Limit + -> Result + Filter: (j.a = i.a) + -> Materialize + -> Broadcast Motion 3:3 (slice2; segments: 3) + -> Seq Scan on pullup_agg_inner j + Optimizer: Postgres query optimizer +(18 rows) + +select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +select a from pullup_agg_outer o + where 1 = (select count(*) filter + (where exists (select 1 from pullup_agg_inner j + where j.a = i.a and j.b > 15)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 2 + 3 +(2 rows) + +-- A CTE defined by the aggregate query remains visible to its argument. +select a from pullup_agg_outer o + where 1 = (with c as materialized (select 7 as v) + select count((select v from c)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +-- A SubLink outside the aggregate would move; retain correlated execution. +select a from pullup_agg_outer o + where 7 = (select count(*) + (select 7) + from pullup_agg_inner i where i.a = o.a) order by 1; + a +--- + 3 +(1 row) + +drop table pullup_agg_div_outer, pullup_agg_outer, pullup_agg_inner; +drop aggregate pullup_sum_from_five(int); +reset optimizer; +-- +-- Test the COUNT bug for aggregates other than count() that return a non-NULL +-- value on empty input: regr_count(), hypothetical-set aggregates and +-- aggregates with a non-NULL initial state, and for expressions over +-- aggregates. A correlated subquery must return its value over empty input +-- for an outer row without matching inner rows, not NULL, and must not lose +-- such a row in WHERE. The planner decorrelates these subqueries into an +-- outer join and computes that value for the rows without a match. +-- +create table empty_agg_outer (a int, b int, d int) distributed by (a); +create table empty_agg_inner (a int, b int) distributed by (a); +insert into empty_agg_outer values (3,1,1), (1,9,5), (0,2,7), (5,5,1), (2,4,9); +insert into empty_agg_inner values (1,10), (2,20), (1,30); +analyze empty_agg_outer; +analyze empty_agg_inner; +create aggregate sum_from_zero(int) (sfunc = int4pl, stype = int4, initcond = '0'); +-- values on empty input +select regr_count(a, b), rank(5) within group (order by b), + cume_dist(5) within group (order by b), sum_from_zero(a), avg(a), + coalesce(sum(b), 0), sum(b) is null + from empty_agg_inner where false; + regr_count | rank | cume_dist | sum_from_zero | avg | coalesce | ?column? +------------+------+-----------+---------------+-----+----------+---------- + 0 | 1 | 1 | 0 | | 0 | t +(1 row) + +set optimizer to off; +select a, d, + (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) as regr, + (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) as rnk, + (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) as sfz, + (select coalesce(sum(i.b), 0) from empty_agg_inner i where i.a = o.d) as cs, + (select sum(i.b) is null from empty_agg_inner i where i.a = o.d) as sn, + (select avg(i.b) from empty_agg_inner i where i.a = o.d) as avg + from empty_agg_outer o order by 1, 2; + a | d | regr | rnk | sfz | cs | sn | avg +---+---+------+-----+-----+----+----+--------------------- + 0 | 7 | 0 | 1 | 0 | 0 | t | + 1 | 5 | 0 | 1 | 0 | 0 | t | + 2 | 9 | 0 | 1 | 0 | 0 | t | + 3 | 1 | 2 | 1 | 2 | 40 | f | 20.0000000000000000 + 5 | 1 | 2 | 1 | 2 | 40 | f | 20.0000000000000000 +(5 rows) + +-- the same in WHERE: the rows without a match must be kept +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d); + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Filter: (o.a > CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '0'::bigint END) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(13 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 1 | 9 | 5 + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(4 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select count(*) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 1 | 9 | 5 + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(4 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select count(*) + 1 from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 5 | 5 | 1 +(2 rows) + +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Left Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Filter: (o.a > CASE WHEN "Expr_SUBQUERY".csq_c1 THEN "Expr_SUBQUERY".csq_c2 ELSE '1'::bigint END) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> GroupAggregate + Group Key: i.a + -> Sort + Sort Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(15 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +select a, d from empty_agg_outer o + where o.a in (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) + order by 1, 2; + a | d +---+--- + 0 | 7 +(1 row) + +-- Constant direct arguments let rank use 1 without an empty-input aggregate. +-- NULL and multiple direct arguments have the same empty-input rank. +create function empty_rank_arg(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select rank(NULL::int) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 5 | 5 | 1 +(2 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(5, 5) within group (order by i.b, i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +select a, b, d from empty_agg_outer o + where o.a > (select rank(empty_rank_arg(2)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; + a | b | d +---+---+--- + 2 | 4 | 9 + 3 | 1 | 1 + 5 | 5 | 1 +(3 rows) + +-- A nonconstant direct argument must still be evaluated on empty input. +select a from empty_agg_outer o where d = 9 + and a > (select rank(empty_rank_arg(0)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +CONTEXT: PL/pgSQL function empty_rank_arg(integer) line 3 at RETURN +-- The surrounding expression must not fold its empty-input zero divisor. +select a from empty_agg_outer o where d = 1 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d) order by 1; + a +--- + 3 + 5 +(2 rows) + +select a from empty_agg_outer o where d = 9 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +drop function empty_rank_arg(int); +-- The value over empty input is computed for the rows without a match only: +-- "10 / count(*)" divides by zero there, as the correlated subquery does, but +-- must not fail when every outer row has a match. +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d); +ERROR: division by zero (seg0 slice1 127.0.0.1:6102 pid=71438) +select a, b, d from empty_agg_outer o + where o.d = 1 + and o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d) + order by 1; + a | b | d +---+---+--- +(0 rows) + +-- A NULL empty-input value is not safe to fold inside an expression either: +-- COALESCE can turn it into a zero divisor. Do not evaluate the expression +-- when the outer table is empty or every outer row has matching inner rows. +create table empty_agg_null_outer (a int) distributed by (a); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +-- An existing InitPlan for count() must not hide an unsafe NULL substitution. +select a from empty_agg_null_outer o + where a < (select count(*) + 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- +(0 rows) + +insert into empty_agg_null_outer values (1); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; + a +--- + 1 +(1 row) + +-- A row without a match must still raise the error at execution time. +insert into empty_agg_null_outer values (9); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +ERROR: division by zero +drop table empty_agg_null_outer; +-- avg() and sum() return NULL on empty input, and a comparison with NULL is +-- not true, so the rows without a match can still be dropped by an inner join +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select avg(i.b) from empty_agg_inner i where i.a = o.d); + QUERY PLAN +---------------------------------------------------------------- + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Join + Hash Cond: (o.d = "Expr_SUBQUERY".csq_c0) + Join Filter: ((o.a)::numeric > "Expr_SUBQUERY".csq_c1) + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: o.d + -> Seq Scan on empty_agg_outer o + -> Hash + -> Subquery Scan on "Expr_SUBQUERY" + -> HashAggregate + Group Key: i.a + -> Seq Scan on empty_agg_inner i + Optimizer: Postgres query optimizer +(13 rows) + +-- A final function may fail on empty input even when the actual input is +-- nonempty. Neither metadata lookup nor decorrelation may call it early. +create function empty_agg_divide_final(int) returns int + language sql immutable strict as 'select 10 / $1'; +create aggregate empty_agg_divide(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_final); +-- Keep all rows in this table matched, independently of qual evaluation order. +create table empty_agg_matched as + select * from empty_agg_outer where d = 1 distributed by (a); +set optimizer to off; +select empty_agg_divide(a) from empty_agg_inner; + empty_agg_divide +------------------ + 2 +(1 row) + +select a, (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + from empty_agg_matched o order by 1; + a | empty_agg_divide +---+------------------ + 3 | 5 + 5 | 5 +(2 rows) + +select a from empty_agg_matched o + where a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; + a +--- + 3 +(1 row) + +select a from empty_agg_matched o + where a in (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; + a +--- + 5 +(1 row) + +-- Empty input must still raise the original error at execution time. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d); +ERROR: division by zero +CONTEXT: SQL function "empty_agg_divide_final" statement 1 +-- A STABLE PL/pgSQL final function is not folded while loading metadata. +-- It still must not run on empty input unless the correlated input is empty. +create function empty_agg_divide_stable_final(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +create aggregate empty_agg_divide_stable(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_stable_final); +set optimizer to off; +select empty_agg_divide_stable(a) from empty_agg_inner; + empty_agg_divide_stable +------------------------- + 2 +(1 row) + +select a, (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) + from empty_agg_matched o order by 1; + a | empty_agg_divide_stable +---+------------------------- + 3 | 5 + 5 | 5 +(2 rows) + +select a from empty_agg_matched o + where a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; + a +--- + 3 +(1 row) + +select a from empty_agg_matched o + where a in (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; + a +--- + 5 +(1 row) + +-- A genuinely empty input still invokes the final function and fails. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d); +ERROR: division by zero +CONTEXT: PL/pgSQL function empty_agg_divide_stable_final(integer) line 3 at RETURN +drop aggregate empty_agg_divide_stable(int); +drop function empty_agg_divide_stable_final(int); +drop table empty_agg_matched; +drop aggregate empty_agg_divide(int); +drop function empty_agg_divide_final(int); +reset optimizer; +drop aggregate sum_from_zero(int); +drop table empty_agg_outer, empty_agg_inner; diff --git a/src/test/regress/sql/subselect_gp.sql b/src/test/regress/sql/subselect_gp.sql index d8edea7d4e0..a08d4722a8c 100644 --- a/src/test/regress/sql/subselect_gp.sql +++ b/src/test/regress/sql/subselect_gp.sql @@ -1515,3 +1515,381 @@ reset optimizer; drop table outer_foo; drop table inner_bar; drop table t; + +-- A window function in a correlated aggregate subquery: the pull-up must +-- bail out, because no join row carries the value a no-match row would see +-- and a WindowFunc cannot be evaluated outside the subquery. The sublink +-- runs as a SubPlan. +create table t_csq_win_out(a int, b int) distributed by (a); +create table t_csq_win_in(a int) distributed by (a); +insert into t_csq_win_out values (1, 5); +set optimizer=off; +-- count()=0 over empty input, then count() over () = 1 over the single +-- aggregate row: "1 > 0 + 1" is false and no row may be returned +select * from t_csq_win_out +where a > (select count(*) + count(*) over () from t_csq_win_in where t_csq_win_in.a = t_csq_win_out.b); +reset optimizer; +drop table t_csq_win_out, t_csq_win_in; + +-- A compound COUNT expression must not become 1/0 during planning when +-- the outer table is empty and the subquery is never evaluated. +create table t_csq_div_out(a int) distributed by (a); +create table t_csq_div_in(a int) distributed by (a); +set optimizer=off; +select * from t_csq_div_out +where a > (select 1 / count(*) from t_csq_div_in where t_csq_div_in.a = t_csq_div_out.a); +reset optimizer; +drop table t_csq_div_out, t_csq_div_in; + +-- Scalar aggregate pull-up must preserve empty-input values in WHERE. +set optimizer to off; +create table pullup_agg_outer (a int) distributed by (a); +create table pullup_agg_inner (a int, b int) distributed by (a); +insert into pullup_agg_outer values (1), (2), (3); +insert into pullup_agg_inner values (1, 10), (1, 20), (2, NULL); +analyze pullup_agg_outer; +analyze pullup_agg_inner; + +-- Only the missing group passes. A matched group that fails the comparison +-- must not be turned into an unmatched row by putting the comparison in ON. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a); +select a from pullup_agg_outer o + where 0 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; + +-- FALSE and NULL empty-input comparisons can still use an INNER join. +explain (costs off) select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a); +select a from pullup_agg_outer o + where 1 = (select count(*) from pullup_agg_inner i where i.a = o.a) order by 1; +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a); +select a from pullup_agg_outer o + where 40 > (select sum(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + +-- COUNT(expr) ignores NULL inputs, so both the all-NULL and missing groups pass. +select a from pullup_agg_outer o + where 0 = (select count(i.b) from pullup_agg_inner i where i.a = o.a) order by 1; + +-- A matched group may return NULL while the empty-input value is 0. +-- The match flag must preserve that NULL instead of substituting the default. +select a from pullup_agg_outer o + where 0 = (select case when count(*) = 0 then 0 else sum(i.b) end + from pullup_agg_inner i where i.a = o.a) order by 1; + +-- Evaluate the whole expression over empty input, not just the bare aggregate. +select a from pullup_agg_outer o + where 1 = (select count(*) + 1 from pullup_agg_inner i where i.a = o.a) order by 1; + +-- Non-NULL defaults also occur in aggregates other than COUNT. +select a from pullup_agg_outer o + where 0 = (select regr_count(i.b, i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; +create aggregate pullup_sum_from_five(int) + (sfunc = int4pl, stype = int4, initcond = '5'); +select a from pullup_agg_outer o + where 5 = (select pullup_sum_from_five(i.b) from pullup_agg_inner i + where i.a = o.a) order by 1; +-- Hypothetical rank returns 1 on empty input, regardless of the sort values. +select a from pullup_agg_outer o + where 1 = (select rank(15) within group (order by i.b) + from pullup_agg_inner i where i.a = o.a) order by 1; + +-- Do not evaluate a hypothetical zero divisor during planning, even when +-- NULL is turned into 0 by COALESCE or another aggregate already uses an InitPlan. +create table pullup_agg_div_outer (a int) distributed by (a); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; +select a from pullup_agg_div_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + +-- All outer rows have a match, so no empty-input division is needed. +insert into pullup_agg_div_outer values (1); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + +-- A missing group must still raise the original error at execution time. +insert into pullup_agg_div_outer values (3); +select a from pullup_agg_div_outer o + where a < (select 10 / count(*) from pullup_agg_inner i + where i.a = o.a) order by 1; +select a from pullup_agg_div_outer o + where a < (select 100 / coalesce(sum(i.b), 0) from pullup_agg_inner i + where i.a = o.a) order by 1; + +-- Parallel hash joins build the inner side even when no outer row uses it. +-- Keep scalar expressions above the join: the all-NULL group for a = 2 +-- must not make COALESCE produce a zero divisor for an unused inner group. +begin; +set local enable_parallel = on; +set local min_parallel_table_scan_size = 0; +set local min_parallel_index_scan_size = 0; +set local parallel_setup_cost = 0; +create table pullup_agg_parallel_outer (a int) with (parallel_workers = 2) + distributed by (a); +alter table pullup_agg_inner set (parallel_workers = 2); +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +insert into pullup_agg_parallel_outer values (1); +select a from pullup_agg_parallel_outer o + where a < (select 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +select a from pullup_agg_parallel_outer o + where a < (select count(*) + 100 / coalesce(sum(i.b), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +rollback; + +-- AVG and SUM over numeric and bigint are NULL on empty input, even though +-- their internal-state final functions are not declared strict. Keep INNER joins. +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::numeric) from pullup_agg_inner i where i.a = o.a); +explain (costs off) select a from pullup_agg_outer o + where 40 > (select avg(i.b::bigint) from pullup_agg_inner i where i.a = o.a); +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::numeric) from pullup_agg_inner i where i.a = o.a); +explain (costs off) select a from pullup_agg_outer o + where 40 > (select sum(i.b::bigint) from pullup_agg_inner i where i.a = o.a); +select a from pullup_agg_outer o + where 100 > (select avg(i.b::numeric) + avg(i.b::bigint) + + sum(i.b::numeric) + sum(i.b::bigint) + from pullup_agg_inner i where i.a = o.a) order by 1; +-- COALESCE still needs the empty-input value for the missing group. +select a from pullup_agg_outer o + where 0 = (select coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +-- Do not fold or evaluate division by zero for an unused group. +select a from pullup_agg_outer o + where a = 1 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; +-- A missing group must still raise the error when its value is used. +select a from pullup_agg_outer o + where a = 3 and a < (select 100 / coalesce(avg(i.b::numeric), 0) + from pullup_agg_inner i where i.a = o.a) order by 1; + +-- Nested subqueries in aggregate arguments and FILTER keep their query level. +-- Pull up the scalar aggregate while leaving its nested SubPlan in place. +explain (costs off) select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a); +select a from pullup_agg_outer o + where 0 = (select count((select j.b from pullup_agg_inner j + where j.a = i.a limit 1)) + from pullup_agg_inner i where i.a = o.a) order by 1; +select a from pullup_agg_outer o + where 1 = (select count(*) filter + (where exists (select 1 from pullup_agg_inner j + where j.a = i.a and j.b > 15)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; +-- A CTE defined by the aggregate query remains visible to its argument. +select a from pullup_agg_outer o + where 1 = (with c as materialized (select 7 as v) + select count((select v from c)) + 1 + from pullup_agg_inner i where i.a = o.a) order by 1; +-- A SubLink outside the aggregate would move; retain correlated execution. +select a from pullup_agg_outer o + where 7 = (select count(*) + (select 7) + from pullup_agg_inner i where i.a = o.a) order by 1; + +drop table pullup_agg_div_outer, pullup_agg_outer, pullup_agg_inner; +drop aggregate pullup_sum_from_five(int); +reset optimizer; + +-- +-- Test the COUNT bug for aggregates other than count() that return a non-NULL +-- value on empty input: regr_count(), hypothetical-set aggregates and +-- aggregates with a non-NULL initial state, and for expressions over +-- aggregates. A correlated subquery must return its value over empty input +-- for an outer row without matching inner rows, not NULL, and must not lose +-- such a row in WHERE. The planner decorrelates these subqueries into an +-- outer join and computes that value for the rows without a match. +-- +create table empty_agg_outer (a int, b int, d int) distributed by (a); +create table empty_agg_inner (a int, b int) distributed by (a); +insert into empty_agg_outer values (3,1,1), (1,9,5), (0,2,7), (5,5,1), (2,4,9); +insert into empty_agg_inner values (1,10), (2,20), (1,30); +analyze empty_agg_outer; +analyze empty_agg_inner; +create aggregate sum_from_zero(int) (sfunc = int4pl, stype = int4, initcond = '0'); + +-- values on empty input +select regr_count(a, b), rank(5) within group (order by b), + cume_dist(5) within group (order by b), sum_from_zero(a), avg(a), + coalesce(sum(b), 0), sum(b) is null + from empty_agg_inner where false; + +set optimizer to off; +select a, d, + (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) as regr, + (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) as rnk, + (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) as sfz, + (select coalesce(sum(i.b), 0) from empty_agg_inner i where i.a = o.d) as cs, + (select sum(i.b) is null from empty_agg_inner i where i.a = o.d) as sn, + (select avg(i.b) from empty_agg_inner i where i.a = o.d) as avg + from empty_agg_outer o order by 1, 2; +-- the same in WHERE: the rows without a match must be kept +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d); +select a, b, d from empty_agg_outer o + where o.a > (select regr_count(i.a, i.b) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; +select a, b, d from empty_agg_outer o + where o.a > (select count(*) from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; +select a, b, d from empty_agg_outer o + where o.a > (select count(*) + 1 from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); +select a, b, d from empty_agg_outer o + where o.a > (select rank(5) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) + order by 1, 2, 3; +select a, d from empty_agg_outer o + where o.a in (select sum_from_zero(i.a) from empty_agg_inner i where i.a = o.d) + order by 1, 2; +-- Constant direct arguments let rank use 1 without an empty-input aggregate. +-- NULL and multiple direct arguments have the same empty-input rank. +create function empty_rank_arg(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select rank(NULL::int) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; +select a, b, d from empty_agg_outer o + where o.a > (select rank(5, 5) within group (order by i.b, i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; +select a, b, d from empty_agg_outer o + where o.a > (select rank(empty_rank_arg(2)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d) order by 1, 2, 3; +-- A nonconstant direct argument must still be evaluated on empty input. +select a from empty_agg_outer o where d = 9 + and a > (select rank(empty_rank_arg(0)) within group (order by i.b) + from empty_agg_inner i where i.a = o.d); +-- The surrounding expression must not fold its empty-input zero divisor. +select a from empty_agg_outer o where d = 1 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d) order by 1; +select a from empty_agg_outer o where d = 9 + and a < (select 10 / (rank(25) within group (order by i.b) - 1) + from empty_agg_inner i where i.a = o.d); +drop function empty_rank_arg(int); + +-- The value over empty input is computed for the rows without a match only: +-- "10 / count(*)" divides by zero there, as the correlated subquery does, but +-- must not fail when every outer row has a match. +set optimizer to off; +select a, b, d from empty_agg_outer o + where o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d); +select a, b, d from empty_agg_outer o + where o.d = 1 + and o.a > (select 10 / count(*) from empty_agg_inner i where i.a = o.d) + order by 1; +-- A NULL empty-input value is not safe to fold inside an expression either: +-- COALESCE can turn it into a zero divisor. Do not evaluate the expression +-- when the outer table is empty or every outer row has matching inner rows. +create table empty_agg_null_outer (a int) distributed by (a); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +-- An existing InitPlan for count() must not hide an unsafe NULL substitution. +select a from empty_agg_null_outer o + where a < (select count(*) + 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +insert into empty_agg_null_outer values (1); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +-- A row without a match must still raise the error at execution time. +insert into empty_agg_null_outer values (9); +set optimizer to off; +select a from empty_agg_null_outer o + where a < (select 10 / coalesce(sum(i.a), 0) + from empty_agg_inner i where i.a = o.a) order by 1; +drop table empty_agg_null_outer; + +-- avg() and sum() return NULL on empty input, and a comparison with NULL is +-- not true, so the rows without a match can still be dropped by an inner join +set optimizer to off; +explain (costs off) select a, b, d from empty_agg_outer o + where o.a > (select avg(i.b) from empty_agg_inner i where i.a = o.d); +-- A final function may fail on empty input even when the actual input is +-- nonempty. Neither metadata lookup nor decorrelation may call it early. +create function empty_agg_divide_final(int) returns int + language sql immutable strict as 'select 10 / $1'; +create aggregate empty_agg_divide(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_final); + +-- Keep all rows in this table matched, independently of qual evaluation order. +create table empty_agg_matched as + select * from empty_agg_outer where d = 1 distributed by (a); + +set optimizer to off; +select empty_agg_divide(a) from empty_agg_inner; +select a, (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + from empty_agg_matched o order by 1; +select a from empty_agg_matched o + where a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; +select a from empty_agg_matched o + where a in (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d) + order by 1; +-- Empty input must still raise the original error at execution time. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide(i.a) from empty_agg_inner i where i.a = o.d); +-- A STABLE PL/pgSQL final function is not folded while loading metadata. +-- It still must not run on empty input unless the correlated input is empty. +create function empty_agg_divide_stable_final(int) returns int + language plpgsql stable strict as $$ +begin + return 10 / $1; +end; +$$; +create aggregate empty_agg_divide_stable(int) + (sfunc = int4pl, stype = int4, initcond = '0', + finalfunc = empty_agg_divide_stable_final); + +set optimizer to off; +select empty_agg_divide_stable(a) from empty_agg_inner; +select a, (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) + from empty_agg_matched o order by 1; +select a from empty_agg_matched o + where a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; +select a from empty_agg_matched o + where a in (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d) order by 1; +-- A genuinely empty input still invokes the final function and fails. +select a from empty_agg_outer o where d = 9 + and a < (select empty_agg_divide_stable(i.a) from empty_agg_inner i + where i.a = o.d); +drop aggregate empty_agg_divide_stable(int); +drop function empty_agg_divide_stable_final(int); +drop table empty_agg_matched; +drop aggregate empty_agg_divide(int); +drop function empty_agg_divide_final(int); + +reset optimizer; +drop aggregate sum_from_zero(int); +drop table empty_agg_outer, empty_agg_inner;