From cc7bf4864d8ee296306bf0295773810061c37d93 Mon Sep 17 00:00:00 2001 From: Alena Rybakina Date: Mon, 12 Oct 2026 21:02:30 +0300 Subject: [PATCH v11 2/3] Pull up EXISTS from outer-join ON clauses referencing both sides An EXISTS in an outer join's ON clause cannot become a semijoin on the nullable input when it also references the preserved input. For strict equality correlations, group the subquery's keys and LEFT JOIN them to the nullable input. Replace EXISTS with a match-guarded CASE testing the preserved-side correlations in the original ON clause. Prepare the EXISTS query once for both strategies. Choose the LEFT JOIN fallback when the correlations exceed available_rels but fit the parent join's two inputs, before renumbering Vars. Keep failures to move quals across an outer join inside the subquery as failures for both strategies. Pass preserved-side context through the existing ON traversal, without a separate expression pass. Require every key to be fixed by the nullable input for NOT EXISTS, and retain SubPlans for unsupported correlations, volatile parent quals, and EXISTS under OR. Cover LEFT and RIGHT joins, duplicates, NULLs, empty inputs, independent keys, mixed semijoin/fallback conjuncts, and comparisons with SubPlans. Validation: make -s -j8 check-world and pgindent --check passed. --- src/backend/optimizer/plan/subselect.c | 312 ++++++++++-- src/backend/optimizer/prep/prepjointree.c | 79 ++- src/include/optimizer/subselect.h | 4 +- src/test/regress/expected/subselect.out | 564 +++++++++++++++++++++- src/test/regress/sql/subselect.sql | 226 ++++++++- 5 files changed, 1108 insertions(+), 77 deletions(-) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index c72eaf2cbd7..7689feed5f0 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -33,7 +33,9 @@ #include "optimizer/planner.h" #include "optimizer/prep.h" #include "optimizer/subselect.h" +#include "optimizer/tlist.h" #include "parser/parse_relation.h" +#include "parser/parse_oper.h" #include "rewrite/rewriteManip.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -95,6 +97,10 @@ static bool sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink static bool simplify_EXISTS_query(PlannerInfo *root, Query *query); static bool collect_correlated_join_quals(Node *jtnode, bool nullable_side, List **hoist_nodes); +static JoinExpr *convert_EXISTS_query_to_left_join(PlannerInfo *root, + Query *subquery, Node *quals, + Relids preserved_rels, Relids nullable_rels, + bool under_not, Node **replacement); static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, Node **testexpr, List **paramIds); static Node *replace_correlation_vars_mutator(Node *node, PlannerInfo *root); @@ -1592,32 +1598,20 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink) } /* - * convert_EXISTS_sublink_to_join: try to convert an EXISTS SubLink to a join - * - * The API of this function is identical to convert_ANY_sublink_to_join's. - * - * References to the parent query are allowed in the WHERE clause of the - * sub-select and in the JOIN/ON clauses that can be moved to the WHERE - * clause without changing the result; see collect_correlated_join_quals(). + * Prepare a private EXISTS query for either semijoin pull-up or the outer-join + * fallback. Keep the original Var numbering until the caller has chosen a + * strategy. Failure to move a qual across an outer join inside the subquery + * is a hard failure for both strategies. */ -JoinExpr * -convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, - bool under_not, Relids available_rels) +static Query * +prepare_EXISTS_query(PlannerInfo *root, SubLink *sublink, Node **where_clause) { - JoinExpr *result; - Query *parse = root->parse; - Query *subselect = (Query *) sublink->subselect; + Query *subselect = castNode(Query, sublink->subselect); Node *whereClause; FromExpr *jointree; List *hoist_nodes = NIL; ListCell *lc; PlannerInfo subroot; - int rtoffset; - int varno; - Relids clause_varnos; - Relids upper_varnos; - - Assert(sublink->subLinkType == EXISTS_SUBLINK); /* * Can't flatten if it contains WITH. (We could arrange to pull up the @@ -1722,6 +1716,51 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, /* Now the rest of the sub-select must not refer to the parent query */ Assert(!contain_vars_of_level((Node *) subselect, 1)); + *where_clause = whereClause; + return subselect; +} + +/* + * convert_EXISTS_sublink_to_join: try to convert an EXISTS SubLink to a join + * + * Normally build a semijoin or antijoin using available_rels. When processing + * an outer join's ON clause, the caller may also supply preserved_rels to + * permit a LEFT JOIN fallback on the nullable input (available_rels). + * *replacement is NULL for a semijoin/antijoin, or the boolean expression + * replacing EXISTS for the fallback. For NOT EXISTS the caller negates it. + */ +JoinExpr * +convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, + bool under_not, Relids available_rels, + Relids preserved_rels, Node **replacement) +{ + JoinExpr *result; + Query *parse = root->parse; + Query *subselect; + Node *whereClause; + int rtoffset; + Relids upper_varnos; + + Assert(sublink->subLinkType == EXISTS_SUBLINK); + *replacement = NULL; + subselect = prepare_EXISTS_query(root, sublink, &whereClause); + if (subselect == NULL) + return NULL; + + /* Decide before renumbering or attaching anything to the parent query. */ + upper_varnos = pull_varnos_of_level(root, whereClause, 1); + Assert(!bms_is_empty(upper_varnos)); + if (!bms_is_subset(upper_varnos, available_rels)) + { + if (preserved_rels == NULL || + !bms_is_subset(upper_varnos, + bms_union(available_rels, preserved_rels))) + return NULL; + return convert_EXISTS_query_to_left_join(root, subselect, whereClause, + preserved_rels, available_rels, + under_not, replacement); + } + /* * The subquery must have a nonempty jointree, but we can make it so. */ @@ -1754,30 +1793,6 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, IncrementVarSublevelsUp((Node *) subselect, -1, 1); IncrementVarSublevelsUp(whereClause, -1, 1); - /* - * Now that the WHERE clause is adjusted to match the parent query - * environment, we can easily identify all the level-zero rels it uses. - * The ones <= rtoffset belong to the upper query; the ones > rtoffset do - * not. - */ - clause_varnos = pull_varnos(root, whereClause); - upper_varnos = NULL; - varno = -1; - while ((varno = bms_next_member(clause_varnos, varno)) >= 0) - { - if (varno <= rtoffset) - upper_varnos = bms_add_member(upper_varnos, varno); - } - bms_free(clause_varnos); - Assert(!bms_is_empty(upper_varnos)); - - /* - * Now that we've got the set of upper-level varnos, we can make the last - * check: only available_rels can be referenced. - */ - if (!bms_is_subset(upper_varnos, available_rels)) - return NULL; - /* * Now we can attach the modified subquery rtable to the parent. This also * adds subquery's RTEPermissionInfos into the upper query. @@ -1806,12 +1821,219 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, return result; } +/* + * convert_EXISTS_query_to_left_join + * + * Handle an EXISTS in an outer join's ON clause which refers to both sides. + * Join distinct correlation keys to the nullable input and test the remaining + * correlations in the original ON clause, guarded by a match marker. + * For example, o LEFT JOIN a ON o.x = a.x AND EXISTS + * (SELECT 1 FROM b JOIN c ON c.y = a.x AND c.x = o.x) becomes + * + * o LEFT JOIN + * (a LEFT JOIN (SELECT DISTINCT c.y, c.x, true AS matched + * FROM b CROSS JOIN c) s ON s.y = a.x) + * ON o.x = a.x AND + * CASE WHEN s.matched THEN (s.x = o.x) IS TRUE ELSE false END. + * + * All projected keys are fixed by correlations to the two parent inputs. + * Thus at most one key group can pass ON for each original pair of rows. + * For NOT EXISTS, every key must be fixed by the nullable input alone: + * otherwise another, nonmatching key group could incorrectly pass NOT. + * + * subquery and quals are private working trees from prepare_EXISTS_query(). + * The caller supplies larg and attaches the returned join to the nullable + * input. On failure the parent query and original SubLink are unchanged. + */ +static JoinExpr * +convert_EXISTS_query_to_left_join(PlannerInfo *root, Query *subquery, Node *quals, + Relids preserved_rels, Relids nullable_rels, + bool under_not, Node **replacement) +{ + List *innerquals = NIL; + List *outerquals = NIL; + List *nullablequals = NIL; + List *correlated = NIL; + List *clauses; + Bitmapset *fixed_keys = NULL; + int i; + ListCell *lc; + ParseState *pstate; + ParseNamespaceItem *nsitem; + RangeTblEntry *joinrte; + RangeTblRef *rtr; + JoinExpr *join; + CaseExpr *expr; + CaseWhen *when; + BooleanTest *test; + Var *flag; + Index rti; + Index joinrti; + + /* Key grouping must not change evaluations of volatile expressions. */ + if (contain_volatile_functions((Node *) subquery) || + contain_volatile_functions(quals)) + return NULL; + + /* Collect the keys and record which are fixed by the nullable input. */ + clauses = list_copy(make_ands_implicit((Expr *) quals)); + for (i = 0; i < list_length(clauses); i++) + { + Node *qual = list_nth(clauses, i); + OpExpr *op; + Var *inner; + Var *outer; + Oid sortop; + Oid eqop; + bool hashable; + TargetEntry *tle; + + /* Preparation can have introduced nested ANDs. */ + if (is_andclause(qual)) + { + clauses = list_concat(clauses, ((BoolExpr *) qual)->args); + continue; + } + if (!contain_vars_of_level(qual, 1)) + { + innerquals = lappend(innerquals, qual); + continue; + } + if (!IsA(qual, OpExpr)) + return NULL; + op = (OpExpr *) qual; + if (list_length(op->args) != 2 || + !IsA(linitial(op->args), Var) || !IsA(lsecond(op->args), Var)) + return NULL; + inner = linitial_node(Var, op->args); + outer = lsecond_node(Var, op->args); + if (inner->varlevelsup == 1) + { + inner = lsecond_node(Var, op->args); + outer = linitial_node(Var, op->args); + } + if (inner->varlevelsup != 0 || outer->varlevelsup != 1 || + inner->varattno <= 0 || inner->vartype != outer->vartype || + op->inputcollid != inner->varcollid || + (!bms_is_member(outer->varno, preserved_rels) && + !bms_is_member(outer->varno, nullable_rels))) + return NULL; + get_sort_group_operators(inner->vartype, false, false, false, + &sortop, &eqop, NULL, &hashable); + if (!OidIsValid(eqop) || op->opno != eqop || + get_commutator(eqop) != eqop || !op_strict(eqop) || + (!OidIsValid(sortop) && !hashable)) + return NULL; + + /* Normalize the equality to inner = outer. */ + op->args = list_make2(inner, outer); + correlated = lappend(correlated, op); + tle = tlist_member((Expr *) inner, subquery->targetList); + if (tle == NULL) + { + SortGroupClause *group = makeNode(SortGroupClause); + + tle = makeTargetEntry((Expr *) copyObject(inner), + list_length(subquery->targetList) + 1, + pstrdup("key"), false); + tle->ressortgroupref = tle->resno; + subquery->targetList = lappend(subquery->targetList, tle); + group->tleSortGroupRef = tle->ressortgroupref; + group->eqop = eqop; + group->sortop = sortop; + group->hashable = hashable; + subquery->groupClause = lappend(subquery->groupClause, group); + } + if (bms_is_member(outer->varno, nullable_rels)) + fixed_keys = bms_add_member(fixed_keys, tle->resno); + } + if (subquery->targetList == NIL) + return NULL; + + /* Negation is safe only when the new join is many-to-one. */ + if (under_not && + bms_num_members(fixed_keys) != list_length(subquery->targetList)) + return NULL; + + rti = list_length(root->parse->rtable) + 1; + joinrti = rti + 1; + foreach(lc, correlated) + { + OpExpr *op = lfirst_node(OpExpr, lc); + Var *inner = linitial_node(Var, op->args); + Var *outer = lsecond_node(Var, op->args); + TargetEntry *tle = tlist_member((Expr *) inner, subquery->targetList); + Var *key; + + Assert(tle != NULL); + key = makeVar(rti, tle->resno, inner->vartype, inner->vartypmod, + inner->varcollid, 0); + outer->varlevelsup = 0; + op->args = list_make2(key, outer); + if (bms_is_member(outer->varno, nullable_rels)) + nullablequals = lappend(nullablequals, op); + else + { + key->varnullingrels = bms_make_singleton(joinrti); + outerquals = lappend(outerquals, op); + } + } + if (nullablequals == NIL || outerquals == NIL) + return NULL; + subquery->jointree->quals = (Node *) make_ands_explicit(innerquals); + if (contain_vars_of_level((Node *) subquery, 1)) + return NULL; + + /* A separate marker distinguishes no match from a nullable key value. */ + subquery->targetList = lappend(subquery->targetList, + makeTargetEntry((Expr *) makeBoolConst(true, false), + list_length(subquery->targetList) + 1, + pstrdup("matched"), false)); + pstate = make_parsestate(NULL); + nsitem = addRangeTableEntryForSubquery(pstate, subquery, + makeAlias("exists_keys", NIL), false, false); + root->parse->rtable = lappend(root->parse->rtable, nsitem->p_rte); + free_parsestate(pstate); + + joinrte = makeNode(RangeTblEntry); + joinrte->rtekind = RTE_JOIN; + joinrte->jointype = JOIN_LEFT; + joinrte->eref = makeAlias("unnamed_join", NIL); + root->parse->rtable = lappend(root->parse->rtable, joinrte); + rtr = makeNode(RangeTblRef); + rtr->rtindex = rti; + join = makeNode(JoinExpr); + join->jointype = JOIN_LEFT; + join->rarg = (Node *) rtr; + join->quals = (Node *) make_ands_explicit(nullablequals); + join->rtindex = joinrti; + + flag = makeVar(rti, list_length(subquery->targetList), BOOLOID, + -1, InvalidOid, 0); + flag->varnullingrels = bms_make_singleton(joinrti); + test = makeNode(BooleanTest); + test->arg = make_ands_explicit(outerquals); + test->booltesttype = IS_TRUE; + test->location = -1; + when = makeNode(CaseWhen); + when->expr = (Expr *) flag; + when->result = (Expr *) test; + when->location = -1; + expr = makeNode(CaseExpr); + expr->casetype = BOOLOID; + expr->args = list_make1(when); + expr->defresult = (Expr *) makeBoolConst(false, false); + expr->location = -1; + *replacement = (Node *) expr; + return join; +} + /* * collect_correlated_join_quals: find the quals of an EXISTS sub-select's * jointree that refer to the parent query * - * convert_EXISTS_sublink_to_join() moves such quals to the WHERE clause of - * the sub-select, which then becomes the semijoin or antijoin qual. That is + * prepare_EXISTS_query() moves such quals to the WHERE clause of the + * sub-select for use by either join conversion strategy. That is * only safe for the quals of inner joins (and of FROM lists) that are not * below the nullable side of an outer join. The ON clause of an outer join * doesn't filter out rows of its non-nullable side, and a qual below the diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index dfe320beccd..a122e3249a9 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -114,7 +114,8 @@ static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, Relids *relids); static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, Node **jtlink1, Relids available_rels1, - Node **jtlink2, Relids available_rels2); + Node **jtlink2, Relids available_rels2, + Relids preserved_rels); static Node *pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode, JoinExpr *lowest_outer_join, AppendRelInfo *containing_appendrel); @@ -664,7 +665,9 @@ replace_empty_jointree(Query *parse) * side of the join). In that case it is legal to push the semijoin * down into the nullable side of the join. If the sublink references any * nonnullable-side variables then it would have to be evaluated as part - * of the outer join, which makes things way too complicated. + * of the outer join, so we cannot use a semijoin. For some such EXISTS + * clauses, we can instead add a LEFT JOIN to the nullable side and keep a + * boolean CASE in the original ON clause. * * Under similar conditions, EXISTS and NOT EXISTS clauses can be handled * by pulling up the sub-SELECT and creating a semijoin or anti-semijoin. @@ -750,7 +753,8 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, /* Now process qual --- all children are available for use */ newf->quals = pull_up_sublinks_qual_recurse(root, f->quals, &jtlink, frelids, - NULL, NULL); + NULL, NULL, + NULL); /* * Note that the result will be either newf, or a stack of JoinExprs @@ -790,7 +794,7 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, * and attach any pulled-up jointree items at the right place. In the * inner-join case we put new JoinExprs above the existing one (much * as for a FromExpr-style join). In outer-join cases the new - * JoinExprs must go into the nullable side of the outer join. The + * semijoins must go into the nullable side of the outer join. The * point of the available_rels machinations is to ensure that we only * pull up quals for which that's okay. * @@ -804,13 +808,16 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, &jtlink, bms_union(leftrelids, rightrelids), - NULL, NULL); + NULL, NULL, + NULL); break; case JOIN_LEFT: + /* The fallback must not multiply volatile ON evaluations. */ j->quals = pull_up_sublinks_qual_recurse(root, j->quals, &j->rarg, rightrelids, - NULL, NULL); + NULL, NULL, + contain_volatile_functions(j->quals) ? NULL : leftrelids); break; case JOIN_FULL: /* can't do anything with full-join quals */ @@ -819,7 +826,8 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, j->quals = pull_up_sublinks_qual_recurse(root, j->quals, &j->larg, leftrelids, - NULL, NULL); + NULL, NULL, + contain_volatile_functions(j->quals) ? NULL : rightrelids); break; default: elog(ERROR, "unrecognized join type: %d", @@ -854,7 +862,12 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, * denotes the relations present underneath jtlink1). Optionally, jtlink2 can * point to a second link where new JoinExprs should be inserted if they * reference available_rels2 (pass NULL for both those arguments if not used). - * Note that SubLinks referencing both sets of variables cannot be optimized. + * SubLinks referencing both sets cannot use a semijoin. For an outer join's + * ON clause, preserved_rels identifies the preserved input, while jtlink1 + * and available_rels1 identify the nullable input. This permits the EXISTS + * converter to choose a LEFT JOIN with a replacement qual. Pass this context + * only through the original ON's AND conjuncts, not into newly pulled-up quals. + * * If we find multiple pull-up-able SubLinks, they'll get stacked onto jtlink1 * and/or jtlink2 in the order we encounter them. We rely on subsequent * optimization to rearrange the stack if appropriate. @@ -864,7 +877,8 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, static Node * pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, Node **jtlink1, Relids available_rels1, - Node **jtlink2, Relids available_rels2) + Node **jtlink2, Relids available_rels2, + Relids preserved_rels) { if (node == NULL) return NULL; @@ -911,7 +925,8 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, &j->larg, available_rels1, &j->rarg, - child_rels); + child_rels, + NULL); /* Return NULL representing constant TRUE */ return NULL; } @@ -937,19 +952,25 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, &j->larg, available_rels2, &j->rarg, - child_rels); + child_rels, + NULL); /* Return NULL representing constant TRUE */ return NULL; } } else if (sublink->subLinkType == EXISTS_SUBLINK) { + Node *replacement; + if ((j = convert_EXISTS_sublink_to_join(root, sublink, false, - available_rels1)) != NULL) + available_rels1, + preserved_rels, &replacement)) != NULL) { /* Yes; insert the new join node into the join tree */ j->larg = *jtlink1; *jtlink1 = (Node *) j; + if (replacement != NULL) + return replacement; /* Recursively process pulled-up jointree nodes */ j->rarg = pull_up_sublinks_jointree_recurse(root, j->rarg, @@ -965,13 +986,15 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, &j->larg, available_rels1, &j->rarg, - child_rels); + child_rels, + NULL); /* Return NULL representing constant TRUE */ return NULL; } if (available_rels2 != NULL && (j = convert_EXISTS_sublink_to_join(root, sublink, false, - available_rels2)) != NULL) + available_rels2, + NULL, &replacement)) != NULL) { /* Yes; insert the new join node into the join tree */ j->larg = *jtlink2; @@ -991,7 +1014,8 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, &j->larg, available_rels2, &j->rarg, - child_rels); + child_rels, + NULL); /* Return NULL representing constant TRUE */ return NULL; } @@ -1031,7 +1055,8 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, j->quals, &j->rarg, child_rels, - NULL, NULL); + NULL, NULL, + NULL); /* Return NULL representing constant TRUE */ return NULL; } @@ -1057,19 +1082,25 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, j->quals, &j->rarg, child_rels, - NULL, NULL); + NULL, NULL, + NULL); /* Return NULL representing constant TRUE */ return NULL; } } else if (sublink->subLinkType == EXISTS_SUBLINK) { + Node *replacement; + if ((j = convert_EXISTS_sublink_to_join(root, sublink, true, - available_rels1)) != NULL) + available_rels1, + preserved_rels, &replacement)) != NULL) { /* Yes; insert the new join node into the join tree */ j->larg = *jtlink1; *jtlink1 = (Node *) j; + if (replacement != NULL) + return (Node *) make_notclause((Expr *) replacement); /* Recursively process pulled-up jointree nodes */ j->rarg = pull_up_sublinks_jointree_recurse(root, j->rarg, @@ -1085,13 +1116,15 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, j->quals, &j->rarg, child_rels, - NULL, NULL); + NULL, NULL, + NULL); /* Return NULL representing constant TRUE */ return NULL; } if (available_rels2 != NULL && (j = convert_EXISTS_sublink_to_join(root, sublink, true, - available_rels2)) != NULL) + available_rels2, + NULL, &replacement)) != NULL) { /* Yes; insert the new join node into the join tree */ j->larg = *jtlink2; @@ -1111,7 +1144,8 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, j->quals, &j->rarg, child_rels, - NULL, NULL); + NULL, NULL, + NULL); /* Return NULL representing constant TRUE */ return NULL; } @@ -1136,7 +1170,8 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, jtlink1, available_rels1, jtlink2, - available_rels2); + available_rels2, + preserved_rels); if (newclause) newclauses = lappend(newclauses, newclause); } diff --git a/src/include/optimizer/subselect.h b/src/include/optimizer/subselect.h index 4ecccf46bd3..56f5f3ffcda 100644 --- a/src/include/optimizer/subselect.h +++ b/src/include/optimizer/subselect.h @@ -27,7 +27,9 @@ extern JoinExpr *convert_ANY_sublink_to_join(PlannerInfo *root, extern JoinExpr *convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, bool under_not, - Relids available_rels); + Relids available_rels, + Relids preserved_rels, + Node **replacement); extern Node *SS_replace_correlation_vars(PlannerInfo *root, Node *expr); extern Node *SS_process_sublinks(PlannerInfo *root, Node *expr, bool isQual); extern void SS_identify_outer_params(PlannerInfo *root); diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index 612634aeb49..ed3aba87554 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -1616,14 +1616,427 @@ ORDER BY 1, 2; | (4 rows) --- No pull-up: the moved qual also references the preserved side of the --- parent LEFT JOIN, which is not included in available_rels +-- Pull-up using a LEFT JOIN and CASE: both sides of the parent join are +-- referenced; the new join is attached to the nullable input EXPLAIN (COSTS OFF) SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a ON o.x = a.x AND EXISTS (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x); - QUERY PLAN ------------------------------------------ + QUERY PLAN +--------------------------------------------------------------------------- + Hash Left Join + Hash Cond: (o.x = a.x) + Join Filter: CASE WHEN (true) THEN ((c.y = o.x) IS TRUE) ELSE false END + -> Seq Scan on ex_o o + -> Hash + -> Hash Left Join + Hash Cond: (a.x = c.y) + -> Seq Scan on ex_a a + -> Hash + -> Group + Group Key: c.y + -> Sort + Sort Key: c.y + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(16 rows) + +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2; + x | ax +---+---- + 1 | 1 + 2 | + 3 | + | +(4 rows) + +-- Duplicate subquery rows must not multiply either side of the parent join. +-- Include an a row which satisfies the original ON qual but fails EXISTS. +INSERT INTO ex_b VALUES (7, 70); +INSERT INTO ex_c VALUES (100, 1), (200, 2), (300, NULL); +INSERT INTO ex_a VALUES (1, 11), (2, 20), (3, 30), (NULL, 99); +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | 1 | 10 + 1 | 1 | 11 + 2 | 2 | 20 + 3 | | + | | +(5 rows) + +-- Compare bags of rows against a SubPlan reference, including duplicates. +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0 OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | ax | y +---+----+--- +(0 rows) + +-- NOT EXISTS must return TRUE, not NULL, when a correlation key is NULL. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop Left Join + Join Filter: (((o.x = a.x) OR ((o.x IS NULL) AND (a.x IS NULL))) AND (NOT CASE WHEN exists_keys.matched THEN ((exists_keys.key = o.x) IS TRUE) ELSE false END)) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop Left Join + Join Filter: (exists_keys.key = a.x) + -> Seq Scan on ex_a a + -> Materialize + -> Subquery Scan on exists_keys + -> Group + Group Key: c.y + -> Sort + Sort Key: c.y + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(16 rows) + +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | | + 2 | | + 3 | 3 | 30 + | | 99 +(4 rows) + +-- Check NOT EXISTS with both matching and null nullable-side keys. +-- NOT EXISTS must still accept such an a row. +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = 1 AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | 2 | 20 + 1 | 3 | 30 + 1 | 5 | 50 + 1 | | 99 + 2 | | + 3 | | + | | +(7 rows) + +-- The same transformation applies to a RIGHT JOIN. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_a a RIGHT JOIN ex_o o + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON o.x = c.y AND a.x = c.y); + QUERY PLAN +--------------------------------------------------------------------------- + Hash Left Join + Hash Cond: (o.x = a.x) + Join Filter: CASE WHEN (true) THEN ((c.y = o.x) IS TRUE) ELSE false END + -> Seq Scan on ex_o o + -> Hash + -> Hash Left Join + Hash Cond: (a.x = c.y) + -> Seq Scan on ex_a a + -> Hash + -> Group + Group Key: c.y + -> Sort + Sort Key: c.y + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(16 rows) + +SELECT o.x, a.x AS ax FROM ex_a a RIGHT JOIN ex_o o + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON o.x = c.y AND a.x = c.y) +ORDER BY 1, 2; + x | ax +---+---- + 1 | 1 + 1 | 1 + 2 | 2 + 3 | + | +(5 rows) + +-- A key in the preserved input can itself have been null-extended earlier. +SELECT o.x, d.x AS dx, a.x AS ax FROM + (ex_o o LEFT JOIN ex_a d ON o.x = d.x AND d.x <> 2) + LEFT JOIN ex_a a ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = d.x) +ORDER BY 1, 2, 3; + x | dx | ax +---+----+---- + 1 | 1 | 1 + 1 | 1 | 1 + 1 | 1 | 1 + 1 | 1 | 1 + 2 | | + 3 | 3 | + | | +(7 rows) + +-- Multiple conjuncts can each add a LEFT JOIN to the nullable input. +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) + AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE c.y = 2) +ORDER BY 1, 2; + x | ax +---+---- + 1 | 1 + 1 | 1 + 2 | + 3 | + | +(5 rows) + +-- Independent correlation keys are grouped together. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Hash Left Join + Hash Cond: (o.x = a.x) + Join Filter: CASE WHEN exists_keys.matched THEN ((exists_keys.key_1 = o.x) IS TRUE) ELSE false END + -> Seq Scan on ex_o o + -> Hash + -> Nested Loop Left Join + Join Filter: (exists_keys.key = a.x) + -> Seq Scan on ex_a a + -> Materialize + -> Subquery Scan on exists_keys + -> Group + Group Key: c.y, c.x + -> Sort + Sort Key: c.y, c.x + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(17 rows) + +-- Off-diagonal keys and duplicates must not create extra parent rows. +INSERT INTO ex_c VALUES (1, 1), (1, 1), (1, 2), (2, 2), (3, 9); +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | 1 | 10 + 1 | 1 | 11 + 2 | 2 | 20 + 3 | | + | | +(5 rows) + +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | | + 2 | | + 3 | 3 | 30 + | | +(4 rows) + +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | ax | y +---+----+--- +(0 rows) + +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | ax | y +---+----+--- +(0 rows) + +-- Several key groups per a are safe for EXISTS, even without o.x = a.x. +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | ax | y +---+----+--- +(0 rows) + +-- NOT EXISTS must retain its SubPlan when a can match several key groups. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +--------------------------------------------------- + Hash Left Join + Hash Cond: (o.x = a.x) + Join Filter: (NOT EXISTS(SubPlan exists_1)) + -> Seq Scan on ex_o o + -> Hash + -> Seq Scan on ex_a a + SubPlan exists_1 + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c + Filter: ((y = a.x) AND (x = o.x)) +(11 rows) + +-- An EXISTS under OR cannot use the remaining correlations to filter groups. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x OR EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +---------------------------------------------------------- + Nested Loop Left Join + Join Filter: ((o.x = a.x) OR EXISTS(SubPlan exists_1)) + -> Seq Scan on ex_o o + -> Materialize + -> Seq Scan on ex_a a + SubPlan exists_1 + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c + Filter: ((y = a.x) AND (x = o.x)) +(10 rows) + +-- Do not multiply evaluations of volatile parent quals. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON random() > 0.5 AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +------------------------------------------------------------------------------------ + Nested Loop Left Join + Join Filter: ((random() > '0.5'::double precision) AND EXISTS(SubPlan exists_1)) + -> Seq Scan on ex_o o + -> Materialize + -> Seq Scan on ex_a a + SubPlan exists_1 + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c + Filter: ((y = a.x) AND (x = o.x)) +(10 rows) + +DELETE FROM ex_c WHERE x < 100; +-- Pull-up does not require an equality in the parent ON. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Nested Loop Left Join + Join Filter: ((o.x >= a.x) AND CASE WHEN exists_keys.matched THEN ((exists_keys.key_1 = o.x) IS TRUE) ELSE false END) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop Left Join + Join Filter: (exists_keys.key = a.x) + -> Seq Scan on ex_a a + -> Materialize + -> Subquery Scan on exists_keys + -> Group + Group Key: c.y, c.x + -> Sort + Sort Key: c.y, c.x + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(16 rows) + +-- Other parent conjuncts can contain OR. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR o.y = 1) AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop Left Join + Join Filter: (CASE WHEN exists_keys.matched THEN ((exists_keys.key_1 = o.x) IS TRUE) ELSE false END AND ((o.x = a.x) OR (o.y = 1))) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop Left Join + Join Filter: (exists_keys.key = a.x) + -> Seq Scan on ex_a a + -> Materialize + -> Subquery Scan on exists_keys + -> Group + Group Key: c.y, c.x + -> Sort + Sort Key: c.y, c.x + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c +(16 rows) + +-- No pull-up for a volatile correlated qual. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c + ON c.y = a.x AND c.y = o.x AND random() > 0.5); + QUERY PLAN +------------------------------------------------------------------- Hash Left Join Hash Cond: (o.x = a.x) Join Filter: EXISTS(SubPlan exists_1) @@ -1634,22 +2047,159 @@ SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a -> Result One-Time Filter: (a.x = o.x) -> Nested Loop + Join Filter: (random() > '0.5'::double precision) -> Seq Scan on ex_b b -> Seq Scan on ex_c c Filter: (y = o.x) -(13 rows) +(14 rows) + +-- Multiple preserved-side keys, with correlations in WHERE rather than ON. +INSERT INTO ex_o VALUES (1, 10); +INSERT INTO ex_c VALUES (1, 10), (1, 10), (1, 11); +SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y) +ORDER BY 1, 2, 3, 4; + x | y | ax | ay +---+----+----+---- + 1 | 1 | | + 1 | 10 | 1 | 10 + 2 | 2 | | + 3 | 3 | | + | | | +(5 rows) + +-- Check NOT EXISTS against a SubPlan for keys fixed by the nullable side. +WITH pulled AS ( + SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON NOT EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y) +), reference AS ( + SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON NOT EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y + OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | y | ax | ay +---+---+----+---- +(0 rows) +-- Ordinary semijoin pull-up and LEFT JOIN fallback share the ON traversal. +-- Put the fallback first so the later semijoin is attached above it. +EXPLAIN (COSTS OFF) SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a ON o.x = a.x AND EXISTS - (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x); + QUERY PLAN +----------------------------------------------------------------------------------------------- + Nested Loop Left Join + Join Filter: (CASE WHEN (true) THEN ((c_1.x = o.x) IS TRUE) ELSE false END AND (o.x = a.x)) + -> Seq Scan on ex_o o + -> Materialize + -> Nested Loop Left Join + Join Filter: (c_1.y = a.x) + -> Nested Loop Semi Join + Join Filter: (a.x = c.y) + -> Seq Scan on ex_a a + -> Materialize + -> Seq Scan on ex_c c + -> Group + Group Key: c_1.y, c_1.x + -> Sort + Sort Key: c_1.y, c_1.x + -> Nested Loop + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c_1 +(18 rows) + +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + x | ax | y +---+----+--- +(0 rows) + +-- A correlation in an inner outer-join ON is a hard failure, even when +-- the parent ON provides both preserved and nullable inputs for a fallback. +-- Since b is nonempty, the subquery has rows regardless of its ON result. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b LEFT JOIN ex_c c ON c.y = a.x AND c.x = o.x); + QUERY PLAN +--------------------------------------------------- + Hash Left Join + Hash Cond: (o.x = a.x) + Join Filter: EXISTS(SubPlan exists_1) + -> Seq Scan on ex_o o + -> Hash + -> Seq Scan on ex_a a + SubPlan exists_1 + -> Nested Loop Left Join + -> Seq Scan on ex_b b + -> Seq Scan on ex_c c + Filter: ((y = a.x) AND (x = o.x)) +(11 rows) + +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b LEFT JOIN ex_c c ON c.y = a.x AND c.x = o.x) ORDER BY 1, 2; x | ax ---+---- 1 | 1 + 1 | 1 + 1 | 1 + 1 | 1 + 2 | 2 + 3 | 3 + | +(7 rows) + +-- Empty inner input must preserve every o row, with a null-extended a. +DELETE FROM ex_b; +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2; + x | ax +---+---- + 1 | + 1 | 2 | 3 | | -(4 rows) +(5 rows) + +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + x | ax | y +---+----+---- + 1 | 1 | 10 + 1 | 1 | 10 + 1 | 1 | 11 + 1 | 1 | 11 + 2 | 2 | 20 + 3 | 3 | 30 + | | 99 +(7 rows) DROP TABLE ex_o, ex_a, ex_b, ex_c; -- Test case for sublinks pushed down into subselects via join alias expansion diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 16e92f51f5e..8f6fbf7d095 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -807,8 +807,8 @@ SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x) ORDER BY 1, 2; --- No pull-up: the moved qual also references the preserved side of the --- parent LEFT JOIN, which is not included in available_rels +-- Pull-up using a LEFT JOIN and CASE: both sides of the parent join are +-- referenced; the new join is attached to the nullable input EXPLAIN (COSTS OFF) SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a ON o.x = a.x AND EXISTS @@ -818,6 +818,228 @@ SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) ORDER BY 1, 2; +-- Duplicate subquery rows must not multiply either side of the parent join. +-- Include an a row which satisfies the original ON qual but fails EXISTS. +INSERT INTO ex_b VALUES (7, 70); +INSERT INTO ex_c VALUES (100, 1), (200, 2), (300, NULL); +INSERT INTO ex_a VALUES (1, 11), (2, 20), (3, 30), (NULL, 99); +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0) +ORDER BY 1, 2, 3; + +-- Compare bags of rows against a SubPlan reference, including duplicates. +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x <= o.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE b.x > 0 OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + +-- NOT EXISTS must return TRUE, not NULL, when a correlation key is NULL. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x); +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + +-- Check NOT EXISTS with both matching and null nullable-side keys. +-- NOT EXISTS must still accept such an a row. +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = 1 AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + +-- The same transformation applies to a RIGHT JOIN. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_a a RIGHT JOIN ex_o o + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON o.x = c.y AND a.x = c.y); +SELECT o.x, a.x AS ax FROM ex_a a RIGHT JOIN ex_o o + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON o.x = c.y AND a.x = c.y) +ORDER BY 1, 2; + +-- A key in the preserved input can itself have been null-extended earlier. +SELECT o.x, d.x AS dx, a.x AS ax FROM + (ex_o o LEFT JOIN ex_a d ON o.x = d.x AND d.x <> 2) + LEFT JOIN ex_a a ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = d.x) +ORDER BY 1, 2, 3; + +-- Multiple conjuncts can each add a LEFT JOIN to the nullable input. +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) + AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x + WHERE c.y = 2) +ORDER BY 1, 2; + +-- Independent correlation keys are grouped together. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +-- Off-diagonal keys and duplicates must not create extra parent rows. +INSERT INTO ex_c VALUES (1, 1), (1, 1), (1, 2), (2, 2), (3, 9); +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +ORDER BY 1, 2, 3; +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +ORDER BY 1, 2, 3; +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON a.x = o.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); +-- Several key groups per a are safe for EXISTS, even without o.x = a.x. +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); +-- NOT EXISTS must retain its SubPlan when a can match several key groups. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +-- An EXISTS under OR cannot use the remaining correlations to filter groups. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x OR EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +-- Do not multiply evaluations of volatile parent quals. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x FROM ex_o o LEFT JOIN ex_a a + ON random() > 0.5 AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +DELETE FROM ex_c WHERE x < 100; +-- Pull-up does not require an equality in the parent ON. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x >= a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +-- Other parent conjuncts can contain OR. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR o.y = 1) AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x); +-- No pull-up for a volatile correlated qual. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c + ON c.y = a.x AND c.y = o.x AND random() > 0.5); + +-- Multiple preserved-side keys, with correlations in WHERE rather than ON. +INSERT INTO ex_o VALUES (1, 10); +INSERT INTO ex_c VALUES (1, 10), (1, 10), (1, 11); +SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y) +ORDER BY 1, 2, 3, 4; +-- Check NOT EXISTS against a SubPlan for keys fixed by the nullable side. +WITH pulled AS ( + SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON NOT EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y) +), reference AS ( + SELECT o.x, o.y, a.x AS ax, a.y AS ay FROM ex_o o LEFT JOIN ex_a a + ON NOT EXISTS (SELECT 1 FROM ex_b b, ex_c c + WHERE c.x = o.x AND c.y = o.y AND c.x = a.x AND c.y = a.y + OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + +-- Ordinary semijoin pull-up and LEFT JOIN fallback share the ON traversal. +-- Put the fallback first so the later semijoin is attached above it. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x); +WITH pulled AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x) +), reference AS ( + SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.x = o.x OFFSET 0) + AND EXISTS (SELECT 1 FROM ex_c c WHERE c.y = a.x OFFSET 0) +) +(SELECT * FROM pulled EXCEPT ALL SELECT * FROM reference) +UNION ALL +(SELECT * FROM reference EXCEPT ALL SELECT * FROM pulled); + +-- A correlation in an inner outer-join ON is a hard failure, even when +-- the parent ON provides both preserved and nullable inputs for a fallback. +-- Since b is nonempty, the subquery has rows regardless of its ON result. +EXPLAIN (COSTS OFF) +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b LEFT JOIN ex_c c ON c.y = a.x AND c.x = o.x); +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b LEFT JOIN ex_c c ON c.y = a.x AND c.x = o.x) +ORDER BY 1, 2; + +-- Empty inner input must preserve every o row, with a null-extended a. +DELETE FROM ex_b; +SELECT o.x, a.x AS ax FROM ex_o o LEFT JOIN ex_a a + ON o.x = a.x AND EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2; +SELECT o.x, a.x AS ax, a.y FROM ex_o o LEFT JOIN ex_a a + ON (o.x = a.x OR (o.x IS NULL AND a.x IS NULL)) AND NOT EXISTS + (SELECT 1 FROM ex_b b JOIN ex_c c ON c.y = a.x AND c.y = o.x) +ORDER BY 1, 2, 3; + DROP TABLE ex_o, ex_a, ex_b, ex_c; -- Test case for sublinks pushed down into subselects via join alias expansion -- 2.50.1 (Apple Git-155)