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;