From 4e6775bee671cefa06369186bc2a767deddc9d95 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 20 Oct 2026 12:11:12 +0900 Subject: [PATCH v1] Fix detection of unsatisfiable lateral references in join_is_legal() When a proposed join has a nonempty minimum parameterization, join_is_legal() checks that none of the rels supplying the parameters would have to join into this join's own join tree, since such a rel cannot supply a parameter to the joinrel. It looks for rels that would have to be on the inside of an outer join with the joinrel, directly or indirectly, plus input rels that laterally reference any rel found so far. However, it does not consider outer joins that would themselves have to join into this join's own join tree, because the joinrel contains a rel they need. Such an outer join cannot be formed strictly outside this join, so a rel that laterally needs its output cannot supply a parameter to the joinrel either. In particular, a rel that is not on the nullable side of an outer join, but laterally references a rel that is, must be joined above the outer join, since it has to see the referenced values as nulled by the join. If we accept such a join anyway, every path for it requires a parameter that can never be supplied, and the clauseless-join heuristics might think that legality of this join means that some other join rel need not be formed, which leads to "failed to build any N-way joins". To fix, make the search also find the outer joins that must join into this join's own join tree, namely those that need a rel found so far, and then include the input rels that laterally need the output of such an outer join. The RHS of such an outer join must join into the tree as well, unless the joinrel itself can be within that RHS by identity 3. Finding these outer joins also covers minimum parameterizations that contain outer-join relids, so the separate check for those added by commit 55fa1b067 is no longer needed. These rules need, for each outer join, the set of rels that can end up within its RHS by identity 3, and the set of relids that must be joined together before it can be formed, which includes the inputs of any lower outer join that it contains or spans. Both are derived from the complete join_info_list, so compute them once in query_planner() and store them in two new SpecialJoinInfo fields, max_righthand and required_rels. No back-patch, as the fix is too invasive to be safe in the stable branches. The new SpecialJoinInfo fields would also break ABI, though that could be worked around by computing the information into local structures each time it is needed. --- src/backend/optimizer/path/joinrels.c | 284 +++++++++++++++++++++---- src/backend/optimizer/plan/initsplan.c | 3 + src/backend/optimizer/plan/planmain.c | 6 + src/include/nodes/pathnodes.h | 11 + src/include/optimizer/paths.h | 1 + src/test/regress/expected/join.out | 279 ++++++++++++++++++++++++ src/test/regress/sql/join.sql | 92 ++++++++ 7 files changed, 632 insertions(+), 44 deletions(-) diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c index cf95021db75..df0e665d796 100644 --- a/src/backend/optimizer/path/joinrels.c +++ b/src/backend/optimizer/path/joinrels.c @@ -32,6 +32,12 @@ static void make_rels_by_clause_joins(PlannerInfo *root, static void make_rels_by_clauseless_joins(PlannerInfo *root, RelOptInfo *old_rel, List *other_rels); +static bool needs_outer_join_output(PlannerInfo *root, RelOptInfo *rel, + Relids join_plus_rhs); +static Relids outer_join_max_righthand(PlannerInfo *root, + SpecialJoinInfo *ojinfo); +static Relids outer_join_required_rels(PlannerInfo *root, + SpecialJoinInfo *ojinfo); static bool has_join_restriction(PlannerInfo *root, RelOptInfo *rel); static bool has_legal_joinclause(PlannerInfo *root, RelOptInfo *rel); static bool restriction_is_constant_false(List *restrictlist, @@ -601,18 +607,33 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, /* * LATERAL references could also cause problems later on if we accept * this join: if the join's minimum parameterization includes any rels - * that would have to be on the inside of an outer join with this join - * rel, then it's never going to be possible to build the complete - * query using this join. We should reject this join not only because - * it'll save work, but because if we don't, the clauseless-join - * heuristics might think that legality of this join means that some - * other join rel need not be formed, and that could lead to failure - * to find any plan at all. We have to consider not only rels that - * are directly on the inner side of an OJ with the joinrel, but also - * ones that are indirectly so, so search to find all such rels. - * Likewise, input rels of the current join search that laterally - * reference the joinrel or any rel found so far cannot supply a - * parameter to the joinrel, so include those too. + * that would have to join into this join rel's own join tree, then + * it's never going to be possible to build the complete query using + * this join. We should reject this join not only because it'll save + * work, but because if we don't, the clauseless-join heuristics might + * think that legality of this join means that some other join rel + * need not be formed, and that could lead to failure to find any plan + * at all. We have to consider not only rels that are directly forced + * into the joinrel's tree, but also ones that are indirectly so, so + * search to find all such rels, starting from the joinrel's own rels + * and applying these rules until no more rels can be added: + * + * 1. An outer join that cannot be formed without some rel found so + * far cannot be formed strictly outside the joinrel, so its output + * cannot be supplied to the joinrel either: include its relid. + * + * 2. Rels that would have to be on the inside of an outer join with + * the joinrel cannot supply a parameter to it: include the RHS of any + * outer join whose LHS includes a rel found so far. Likewise include + * the RHS of an outer join whose relid was found so far, unless the + * joinrel itself can be within that RHS, in which case the RHS rels + * can still supply a parameter to it. + * + * 3. Input rels of the current join search that laterally reference + * the joinrel or any rel found so far cannot supply a parameter to + * the joinrel, so include those too. The same goes for input rels + * that need the output of an outer join found so far, because they + * laterally reference its RHS from above it. */ join_lateral_rels = min_join_parameterization(root, joinrelids, rel1, rel2); @@ -632,8 +653,29 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, if (sjinfo->jointype == JOIN_FULL) continue; - if (bms_overlap(sjinfo->min_lefthand, join_plus_rhs) && - !bms_is_subset(sjinfo->min_righthand, join_plus_rhs)) + /* + * An outer join that needs any rel found so far cannot be + * formed strictly outside the joinrel, so include its + * relid. + */ + if (!bms_is_member(sjinfo->ojrelid, join_plus_rhs) && + bms_overlap(sjinfo->required_rels, join_plus_rhs)) + { + join_plus_rhs = bms_add_member(join_plus_rhs, + sjinfo->ojrelid); + more = true; + } + + /* + * The RHS of a join whose LHS overlaps the set must be in + * it too. So must the RHS of an outer join whose relid + * is in the set, unless the joinrel itself can be within + * that RHS. + */ + if (!bms_is_subset(sjinfo->min_righthand, join_plus_rhs) && + (bms_overlap(sjinfo->min_lefthand, join_plus_rhs) || + (bms_is_member(sjinfo->ojrelid, join_plus_rhs) && + !bms_overlap(joinrelids, sjinfo->max_righthand)))) { join_plus_rhs = bms_add_members(join_plus_rhs, sjinfo->min_righthand); @@ -643,15 +685,18 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, /* * Add input rels that laterally reference any rel found so - * far. An input rel can be a sub-joinlist's joinrel, which - * must be taken as a whole. + * far, or that need the output of an outer join found so far. + * An input rel can be a sub-joinlist's joinrel, which must be + * taken as a whole. */ foreach(l, root->initial_rels) { RelOptInfo *irel = (RelOptInfo *) lfirst(l); - if (bms_overlap(irel->lateral_relids, join_plus_rhs) && - !bms_is_subset(irel->relids, join_plus_rhs)) + if (bms_is_subset(irel->relids, join_plus_rhs)) + continue; + if (bms_overlap(irel->lateral_relids, join_plus_rhs) || + needs_outer_join_output(root, irel, join_plus_rhs)) { join_plus_rhs = bms_add_members(join_plus_rhs, irel->relids); @@ -661,32 +706,6 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, } while (more); if (bms_overlap(join_plus_rhs, join_lateral_rels)) return false; /* will not be able to join to some RHS rel */ - - /* - * Furthermore, the minimum parameterization can include - * outer-join relids as well as baserel relids. In such a case - * the value laterally needed is an output of that outer join, so - * the outer join must be formed strictly outside this join for - * the value to be supplied to it. That's not possible if any rel - * needed to form the outer join is within join_plus_rhs, since - * all such rels must join into this join's own join tree. If we - * accepted this join anyway, every path for it would require a - * parameter that can never be supplied, so it could never appear - * in a complete plan. - */ - if (bms_overlap(join_lateral_rels, root->outer_join_rels)) - { - foreach(l, root->join_info_list) - { - SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); - - if (!bms_is_member(sjinfo->ojrelid, join_lateral_rels)) - continue; - if (bms_overlap(join_plus_rhs, sjinfo->min_lefthand) || - bms_overlap(join_plus_rhs, sjinfo->min_righthand)) - return false; /* OJ can't be formed outside join */ - } - } } } @@ -696,6 +715,181 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, return true; } +/* + * needs_outer_join_output + * Does rel laterally need the output of an outer join whose relid is in + * join_plus_rhs? + * + * max_righthand is the set of rels that can end up within an outer join's + * RHS. If rel is outside that set but laterally references a rel within it, + * rel must be joined above the outer join: if the referenced rel does end up + * within the RHS, rel sees its values as nulled by the outer join; and if it + * ends up above the outer join instead, rel depends on it and so must be + * above the outer join too. + */ +static bool +needs_outer_join_output(PlannerInfo *root, RelOptInfo *rel, + Relids join_plus_rhs) +{ + ListCell *l; + + if (rel->lateral_relids == NULL) + return false; + + foreach(l, root->join_info_list) + { + SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); + + if (sjinfo->max_righthand == NULL) + continue; /* full join or semijoin */ + if (bms_is_member(sjinfo->ojrelid, join_plus_rhs) && + bms_overlap(rel->lateral_relids, sjinfo->max_righthand) && + !bms_overlap(rel->relids, sjinfo->max_righthand)) + return true; + } + + return false; +} + +/* + * compute_outer_join_extents + * Fill the max_righthand and required_rels fields of the SpecialJoinInfos + * in root->join_info_list. + * + * These are derived from the other SpecialJoinInfo fields, so this must be + * called once join_info_list is complete. join_is_legal() uses them to check + * lateral references, so there is nothing to do if the query has none. Full + * joins and semijoins are left with NULL fields. + */ +void +compute_outer_join_extents(PlannerInfo *root) +{ + ListCell *l; + + if (!root->hasLateralRTEs) + return; + + /* max_righthand must be known for all joins before required_rels */ + foreach(l, root->join_info_list) + { + SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); + + if (sjinfo->jointype == JOIN_FULL || sjinfo->ojrelid == 0) + continue; + sjinfo->max_righthand = outer_join_max_righthand(root, sjinfo); + } + + foreach(l, root->join_info_list) + { + SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); + + if (sjinfo->max_righthand == NULL) + continue; + sjinfo->required_rels = outer_join_required_rels(root, sjinfo); + } +} + +/* + * outer_join_max_righthand + * Compute the rels that can end up within the RHS of the given outer join. + * + * Besides the join's syntactic RHS, this includes the RHS of any outer join + * that identity 3 can move into it, and so on recursively. commute_above_l + * can claim commutability for an upper join whose min_lefthand includes rels + * from above the lower join, but such a join cannot be formed within the + * lower join's RHS, so we only count joins whose min_lefthand lies within + * the rels found so far. + */ +static Relids +outer_join_max_righthand(PlannerInfo *root, SpecialJoinInfo *ojinfo) +{ + Relids result = bms_copy(ojinfo->syn_righthand); + Relids commuters = bms_copy(ojinfo->commute_above_l); + bool more; + + /* fast path for the common case of nothing to move in */ + if (commuters == NULL) + return result; + + do + { + ListCell *l; + + more = false; + foreach(l, root->join_info_list) + { + SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); + + if (!bms_is_member(sjinfo->ojrelid, commuters) || + !bms_is_subset(sjinfo->min_lefthand, result)) + continue; + if (!bms_is_subset(sjinfo->syn_righthand, result)) + { + result = bms_add_members(result, sjinfo->syn_righthand); + more = true; + } + if (!bms_is_subset(sjinfo->commute_above_l, commuters)) + { + commuters = bms_add_members(commuters, + sjinfo->commute_above_l); + more = true; + } + } + } while (more); + + return result; +} + +/* + * outer_join_required_rels + * Compute the relids that must be joined together before the given outer + * join can be formed. + * + * These are the join's min_lefthand and min_righthand, plus the inputs of + * any lower outer join whose relid they include, and of any lower outer + * join they span: one that has a min_righthand rel in the set together with + * a rel that cannot be within its RHS, since those two can only be joined + * once that outer join has been formed. max_righthand must already be set + * for every outer join. + */ +static Relids +outer_join_required_rels(PlannerInfo *root, SpecialJoinInfo *ojinfo) +{ + Relids result; + bool more; + + result = bms_union(ojinfo->min_lefthand, ojinfo->min_righthand); + do + { + ListCell *l; + + more = false; + foreach(l, root->join_info_list) + { + SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(l); + + if (sjinfo == ojinfo || sjinfo->max_righthand == NULL) + continue; + if (bms_is_member(sjinfo->ojrelid, result)) + { + /* included already; make sure its inputs are too */ + if (bms_is_subset(sjinfo->min_lefthand, result) && + bms_is_subset(sjinfo->min_righthand, result)) + continue; + } + else if (!bms_overlap(result, sjinfo->min_righthand) || + bms_is_subset(result, sjinfo->max_righthand)) + continue; /* not spanned */ + result = bms_add_member(result, sjinfo->ojrelid); + result = bms_add_members(result, sjinfo->min_lefthand); + result = bms_add_members(result, sjinfo->min_righthand); + more = true; + } + } while (more); + + return result; +} + /* * init_dummy_sjinfo * Populate the given SpecialJoinInfo for a plain inner join between the @@ -722,6 +916,8 @@ init_dummy_sjinfo(SpecialJoinInfo *sjinfo, Relids left_relids, sjinfo->commute_above_r = NULL; sjinfo->commute_below_l = NULL; sjinfo->commute_below_r = NULL; + sjinfo->max_righthand = NULL; + sjinfo->required_rels = NULL; /* we don't bother trying to make the remaining fields valid */ sjinfo->lhs_strict = false; sjinfo->semi_can_btree = false; diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 517f158d2b2..317018dda52 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -2091,6 +2091,9 @@ make_outerjoininfo(PlannerInfo *root, sjinfo->commute_above_r = NULL; sjinfo->commute_below_l = NULL; sjinfo->commute_below_r = NULL; + /* these fields are filled in by compute_outer_join_extents(): */ + sjinfo->max_righthand = NULL; + sjinfo->required_rels = NULL; compute_semijoin_info(root, sjinfo, clause); diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index 9045c490486..f42ed16cc2f 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -318,6 +318,12 @@ restart: */ create_lateral_join_info(root); + /* + * Now that join_info_list is complete, fill in the SpecialJoinInfo fields + * that join_is_legal() needs to check lateral references. + */ + compute_outer_join_extents(root); + /* * Match foreign keys to equivalence classes and join quals. This must be * done after finalizing equivalence classes, and it's useful to wait till diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 8615db98a55..91743a8acd7 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -3192,6 +3192,15 @@ typedef struct PlaceHolderVar * 3 and are in the RHS of this join (so, this is the upper join in the second * form of the identity). * + * max_righthand is the set of rels that can end up within this join's RHS: + * syn_righthand plus the RHS of any upper outer join that identity 3 can move + * into it. required_rels is the set of base+OJ relids that must be joined + * together before this join can be formed. These are derived from the above + * fields once all SpecialJoinInfos exist (see compute_outer_join_extents), + * and are used by join_is_legal to check lateral references. They are NULL + * for full joins and semijoins, and for all joins if the query has no + * lateral references. They are not translated for child joins. + * * lhs_strict is true if the special join's condition cannot succeed when the * LHS variables are all NULL (this means that an outer join can commute with * upper-level outer joins even if it appears in their RHS). We don't bother @@ -3232,6 +3241,8 @@ typedef struct SpecialJoinInfo Relids commute_above_r; /* commuting OJs above this one, if RHS */ Relids commute_below_l; /* commuting OJs in this one's LHS */ Relids commute_below_r; /* commuting OJs in this one's RHS */ + Relids max_righthand; /* rels that can end up within the RHS */ + Relids required_rels; /* relids needed before it can be formed */ bool lhs_strict; /* joinclause is strict for some LHS rel */ /* Remaining fields are set only for JOIN_SEMI jointype: */ bool semi_can_btree; /* true if semi_operators are all btree */ diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 64f87da1674..536e920c84d 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -112,6 +112,7 @@ extern void add_paths_to_joinrel(PlannerInfo *root, RelOptInfo *joinrel, extern void join_search_one_level(PlannerInfo *root, int level); extern RelOptInfo *make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2); +extern void compute_outer_join_extents(PlannerInfo *root); extern Relids add_outer_joins_to_relids(PlannerInfo *root, Relids input_relids, SpecialJoinInfo *sjinfo, List **pushed_down_joins); diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index ca4058c8d21..41341119578 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -7240,6 +7240,285 @@ where s2.q2 > t1.f1; (1 row) reset from_collapse_limit; +-- +-- check that a join is disallowed when its lateral reference can only be +-- satisfied by way of a rel that needs the output of an outer join that has +-- to be formed above the join +-- +explain (costs off) +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join lateral (select s1.f1 offset 0) s2 on t2.f1 = 1; + QUERY PLAN +------------------------------------------------------------------- + Aggregate + -> Nested Loop Left Join + Join Filter: (t2.f1 = 1) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int4_tbl t1 + -> Materialize + -> Nested Loop + -> Seq Scan on int4_tbl t2 + -> Materialize + -> Seq Scan on int4_tbl t3 + -> Result + -> Memoize + Cache Key: (t3.f1) + Cache Mode: binary + -> Subquery Scan on s2 + -> Result +(17 rows) + +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join lateral (select s1.f1 offset 0) s2 on t2.f1 = 1; + count +------- + 125 +(1 row) + +-- likewise when the rel is reached through an outer join +explain (costs off) +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join int4_tbl t4 on s1.f1 = t4.f1 + left join lateral (select t4.f1 offset 0) s2 on t2.f1 = 1; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> Nested Loop Left Join + Join Filter: (t2.f1 = 1) + -> Hash Left Join + Hash Cond: ((t3.f1) = t4.f1) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int4_tbl t1 + -> Materialize + -> Nested Loop + -> Seq Scan on int4_tbl t2 + -> Materialize + -> Seq Scan on int4_tbl t3 + -> Result + -> Hash + -> Seq Scan on int4_tbl t4 + -> Memoize + Cache Key: t4.f1 + Cache Mode: binary + -> Subquery Scan on s2 + -> Result +(21 rows) + +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join int4_tbl t4 on s1.f1 = t4.f1 + left join lateral (select t4.f1 offset 0) s2 on t2.f1 = 1; + count +------- + 125 +(1 row) + +-- likewise when the rel shares an outer join's RHS with such a rel +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join lateral (select t3.q1 as k offset 0) s1 + left join (int8_tbl t4 cross join lateral (select s1.k offset 0) s2) on t2.q1 = t4.q1 + left join lateral (select t4.q2 offset 0) s3 on t3.q2 = s3.q2; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> Nested Loop Left Join + Join Filter: (t3.q2 = (t4.q2)) + -> Nested Loop Left Join + Join Filter: (t2.q1 = t4.q1) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Materialize + -> Nested Loop + -> Seq Scan on int8_tbl t2 + -> Materialize + -> Seq Scan on int8_tbl t3 + -> Result + -> Nested Loop + -> Result + -> Seq Scan on int8_tbl t4 + -> Result +(18 rows) + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join lateral (select t3.q1 as k offset 0) s1 + left join (int8_tbl t4 cross join lateral (select s1.k offset 0) s2) on t2.q1 = t4.q1 + left join lateral (select t4.q2 offset 0) s3 on t3.q2 = s3.q2; + count +------- + 325 +(1 row) + +-- likewise when the rel is in the RHS of an outer join whose LHS includes +-- the lower outer join +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join (int8_tbl t4 cross join lateral (select coalesce(t1.q1, 0) as k) s1) on t3.q2 = s1.k + left join lateral (select s1.k offset 0) s2 on t2.q2 = s2.k; + QUERY PLAN +------------------------------------------------------------------- + Aggregate + -> Nested Loop Left Join + Join Filter: (t2.q2 = ((COALESCE(t1.q1, '0'::bigint)))) + -> Nested Loop Left Join + Join Filter: (t3.q2 = COALESCE(t1.q1, '0'::bigint)) + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Materialize + -> Nested Loop + -> Seq Scan on int8_tbl t2 + -> Materialize + -> Seq Scan on int8_tbl t3 + -> Memoize + Cache Key: COALESCE(t1.q1, '0'::bigint) + Cache Mode: binary + -> Seq Scan on int8_tbl t4 + -> Result +(17 rows) + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join (int8_tbl t4 cross join lateral (select coalesce(t1.q1, 0) as k) s1) on t3.q2 = s1.k + left join lateral (select s1.k offset 0) s2 on t2.q2 = s2.k; + count +------- + 285 +(1 row) + +-- likewise when the outer join's LHS spans the lower outer join's RHS and a +-- rel outside it, so that it must be formed above the lower outer join +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join int8_tbl t4 + left join int8_tbl t5 on t2.q2 = t5.q1 and t4.q1 = t5.q2 + left join lateral (select t5.q2 offset 0) s2 on t3.q2 = s2.q2; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> Nested Loop Left Join + Join Filter: (t3.q2 = (t5.q2)) + -> Hash Left Join + Hash Cond: ((t2.q2 = t5.q1) AND (t4.q1 = t5.q2)) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Materialize + -> Nested Loop + -> Seq Scan on int8_tbl t2 + -> Materialize + -> Seq Scan on int8_tbl t3 + -> Materialize + -> Seq Scan on int8_tbl t4 + -> Hash + -> Seq Scan on int8_tbl t5 + -> Result +(18 rows) + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join int8_tbl t4 + left join int8_tbl t5 on t2.q2 = t5.q1 and t4.q1 = t5.q2 + left join lateral (select t5.q2 offset 0) s2 on t3.q2 = s2.q2; + count +------- + 625 +(1 row) + +-- likewise when the referenced rel is in the RHS of an outer join that +-- identity 3 can move into the lower outer join's RHS +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t1.q1 as k offset 0) s1 on t3.q1 = 1 + cross join lateral (select s1.k offset 0) s2 + cross join int8_tbl t4 + inner join int8_tbl t5 on s2.k = t5.q2 + left join lateral (select s2.k offset 0) s3 on t2.q1 = 1; + QUERY PLAN +------------------------------------------------------------------- + Aggregate + -> Hash Join + Hash Cond: (((t1.q1)) = t5.q2) + -> Nested Loop Left Join + Join Filter: (t2.q1 = 1) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Nested Loop + -> Nested Loop Left Join + Join Filter: (t3.q1 = 1) + -> Seq Scan on int8_tbl t3 + -> Materialize + -> Result + -> Materialize + -> Seq Scan on int8_tbl t2 + -> Result + -> Memoize + Cache Key: ((t1.q1)) + Cache Mode: binary + -> Subquery Scan on s3 + -> Result + -> Hash + -> Nested Loop + -> Seq Scan on int8_tbl t4 + -> Materialize + -> Seq Scan on int8_tbl t5 +(27 rows) + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t1.q1 as k offset 0) s1 on t3.q1 = 1 + cross join lateral (select s1.k offset 0) s2 + cross join int8_tbl t4 + inner join int8_tbl t5 on s2.k = t5.q2 + left join lateral (select s2.k offset 0) s3 on t2.q1 = 1; + count +------- + 0 +(1 row) + +-- but a rel that identity 3 can move into the RHS can still supply a parameter +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t2.q2 as k offset 0) s1 on t2.q1 = s1.k + left join (lateral (select coalesce(s1.k, 0) as k) s2 left join int8_tbl t4 on s2.k = 1) on t3.q2 = t4.q2 + left join lateral (select t4.q1 offset 0) s3 on s1.k = s3.q1; + QUERY PLAN +------------------------------------------------------------------------------------------------ + Aggregate + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Materialize + -> Nested Loop Left Join + Join Filter: (s1.k = (t4.q1)) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t2 + -> Subquery Scan on s1 + Filter: (t2.q1 = s1.k) + -> Result + -> Hash Right Join + Hash Cond: (t4.q2 = t3.q2) + -> Nested Loop + -> Result + One-Time Filter: (COALESCE(s1.k, '0'::bigint) = 1) + -> Seq Scan on int8_tbl t4 + -> Hash + -> Seq Scan on int8_tbl t3 + -> Result +(21 rows) + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t2.q2 as k offset 0) s1 on t2.q1 = s1.k + left join (lateral (select coalesce(s1.k, 0) as k) s2 left join int8_tbl t4 on s2.k = 1) on t3.q2 = t4.q2 + left join lateral (select t4.q1 offset 0) s3 on s1.k = s3.q1; + count +------- + 125 +(1 row) + -- -- check that an outer join is not taken to commute with a lower outer join -- when its clause references the lower join's RHS only laterally within a diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 037fb7fe093..0125d8fe837 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2587,6 +2587,98 @@ where s2.q2 > t1.f1; reset from_collapse_limit; +-- +-- check that a join is disallowed when its lateral reference can only be +-- satisfied by way of a rel that needs the output of an outer join that has +-- to be formed above the join +-- + +explain (costs off) +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join lateral (select s1.f1 offset 0) s2 on t2.f1 = 1; + +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join lateral (select s1.f1 offset 0) s2 on t2.f1 = 1; + +-- likewise when the rel is reached through an outer join +explain (costs off) +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join int4_tbl t4 on s1.f1 = t4.f1 + left join lateral (select t4.f1 offset 0) s2 on t2.f1 = 1; + +select count(*) from int4_tbl t1 left join (int4_tbl t2 cross join int4_tbl t3) on true + cross join lateral (select t3.f1 offset 0) s1 + left join int4_tbl t4 on s1.f1 = t4.f1 + left join lateral (select t4.f1 offset 0) s2 on t2.f1 = 1; + +-- likewise when the rel shares an outer join's RHS with such a rel +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join lateral (select t3.q1 as k offset 0) s1 + left join (int8_tbl t4 cross join lateral (select s1.k offset 0) s2) on t2.q1 = t4.q1 + left join lateral (select t4.q2 offset 0) s3 on t3.q2 = s3.q2; + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join lateral (select t3.q1 as k offset 0) s1 + left join (int8_tbl t4 cross join lateral (select s1.k offset 0) s2) on t2.q1 = t4.q1 + left join lateral (select t4.q2 offset 0) s3 on t3.q2 = s3.q2; + +-- likewise when the rel is in the RHS of an outer join whose LHS includes +-- the lower outer join +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join (int8_tbl t4 cross join lateral (select coalesce(t1.q1, 0) as k) s1) on t3.q2 = s1.k + left join lateral (select s1.k offset 0) s2 on t2.q2 = s2.k; + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join (int8_tbl t4 cross join lateral (select coalesce(t1.q1, 0) as k) s1) on t3.q2 = s1.k + left join lateral (select s1.k offset 0) s2 on t2.q2 = s2.k; + +-- likewise when the outer join's LHS spans the lower outer join's RHS and a +-- rel outside it, so that it must be formed above the lower outer join +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join int8_tbl t4 + left join int8_tbl t5 on t2.q2 = t5.q1 and t4.q1 = t5.q2 + left join lateral (select t5.q2 offset 0) s2 on t3.q2 = s2.q2; + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + cross join int8_tbl t4 + left join int8_tbl t5 on t2.q2 = t5.q1 and t4.q1 = t5.q2 + left join lateral (select t5.q2 offset 0) s2 on t3.q2 = s2.q2; + +-- likewise when the referenced rel is in the RHS of an outer join that +-- identity 3 can move into the lower outer join's RHS +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t1.q1 as k offset 0) s1 on t3.q1 = 1 + cross join lateral (select s1.k offset 0) s2 + cross join int8_tbl t4 + inner join int8_tbl t5 on s2.k = t5.q2 + left join lateral (select s2.k offset 0) s3 on t2.q1 = 1; + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t1.q1 as k offset 0) s1 on t3.q1 = 1 + cross join lateral (select s1.k offset 0) s2 + cross join int8_tbl t4 + inner join int8_tbl t5 on s2.k = t5.q2 + left join lateral (select s2.k offset 0) s3 on t2.q1 = 1; + +-- but a rel that identity 3 can move into the RHS can still supply a parameter +explain (costs off) +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t2.q2 as k offset 0) s1 on t2.q1 = s1.k + left join (lateral (select coalesce(s1.k, 0) as k) s2 left join int8_tbl t4 on s2.k = 1) on t3.q2 = t4.q2 + left join lateral (select t4.q1 offset 0) s3 on s1.k = s3.q1; + +select count(*) from int8_tbl t1 left join (int8_tbl t2 cross join int8_tbl t3) on true + left join lateral (select t2.q2 as k offset 0) s1 on t2.q1 = s1.k + left join (lateral (select coalesce(s1.k, 0) as k) s2 left join int8_tbl t4 on s2.k = 1) on t3.q2 = t4.q2 + left join lateral (select t4.q1 offset 0) s3 on s1.k = s3.q1; + -- -- check that an outer join is not taken to commute with a lower outer join -- when its clause references the lower join's RHS only laterally within a -- 2.37.1 (Apple Git-137.1)