From 173c33d7502673989fd40cdd81bd4a67e96c865a Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 8 Oct 2026 10:29:43 +0900 Subject: [PATCH v1] Disallow joins whose lateral references can never be satisfied If a proposed join has lateral references to rels outside it, join_is_legal() checks that those rels can still be joined to from outside. To do that it collects the rels that must end up on the inner side of an outer join with the proposed join, directly or indirectly, and rejects the join if any of its lateral references point to one of them. That search followed only outer joins, from min_lefthand to min_righthand. It failed to notice that a rel that laterally references the proposed join, or any rel found by the search, cannot supply a parameter to the proposed join either, and that the same holds for rels on the inner side of an outer join with such a rel. As a result, we could approve a join whose lateral references could never be satisfied. Such a join can never appear in a complete plan. Worse, as the existing comment warns, its apparent legality could mislead the clauseless-join heuristics into thinking that some other join rel need not be formed. In particular, have_join_order_restriction() would not force a clauseless outer join if one of its input rels appeared to have a legal clause join elsewhere, and if that outer join was required, we would fail with "failed to build any N-way joins". To fix, also include in the search any rel that laterally references a rel already found. Back-patch to all supported branches. Author: Richard Guo Discussion: https://postgr.es/m/XXX Backpatch-through: 14 --- src/backend/optimizer/path/joinrels.c | 20 +++++++++++++++- src/test/regress/expected/join.out | 34 +++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 16 +++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c index 10fb3e39d28..1278d958fb7 100644 --- a/src/backend/optimizer/path/joinrels.c +++ b/src/backend/optimizer/path/joinrels.c @@ -609,7 +609,9 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, * 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. + * ones that are indirectly so, so search to find all such rels. Any + * rel that laterally references one of those rels must end up inside + * the same outer join too, so include such rels in the search. */ join_lateral_rels = min_join_parameterization(root, joinrelids, rel1, rel2); @@ -637,6 +639,22 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, more = true; } } + + /* add rels that laterally reference any rel found so far */ + for (int rti = 1; rti < root->simple_rel_array_size; rti++) + { + RelOptInfo *brel = root->simple_rel_array[rti]; + + if (brel == NULL || brel->reloptkind != RELOPT_BASEREL) + continue; + + if (!bms_is_member(rti, join_plus_rhs) && + bms_overlap(brel->lateral_relids, join_plus_rhs)) + { + join_plus_rhs = bms_add_member(join_plus_rhs, rti); + more = true; + } + } } while (more); if (bms_overlap(join_plus_rhs, join_lateral_rels)) return false; /* will not be able to join to some RHS rel */ diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 4ee94d00ade..aa9f20ad957 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6977,6 +6977,40 @@ select count(*) from int4_tbl t1 left join 25 (1 row) +-- +-- check that a join is disallowed when its lateral reference can only be +-- satisfied by way of a rel that laterally references the join itself +-- +explain (costs off) +select 1 from int4_tbl t1 left join int4_tbl t2 on true, + lateral (select t2.f1 offset 0) s1 left join int8_tbl t3 on s1.f1 = t3.q1, + lateral (select t3.q2 offset 0) s2 +where s2.q2 > t1.f1; + QUERY PLAN +------------------------------------------------------- + Nested Loop + Join Filter: ((t3.q2) > t1.f1) + -> Hash Left Join + Hash Cond: ((t2.f1) = t3.q1) + -> Nested Loop + -> Nested Loop Left Join + -> Seq Scan on int4_tbl t1 + -> Materialize + -> Seq Scan on int4_tbl t2 + -> Result + -> Hash + -> Seq Scan on int8_tbl t3 + -> Result +(13 rows) + +select 1 from int4_tbl t1 left join int4_tbl t2 on true, + lateral (select t2.f1 offset 0) s1 left join int8_tbl t3 on s1.f1 = t3.q1, + lateral (select t3.q2 offset 0) s2 +where s2.q2 > t1.f1; + ?column? +---------- +(0 rows) + -- -- check that a cloned outer-join qual is not enforced multiple times when -- it is moved into a parameterized join diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index bf8153afbcd..e43fae822dc 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2495,6 +2495,22 @@ select count(*) from int4_tbl t1 left join (select c.f1 as cnt from int4_tbl c where c.f1 = t2.one offset 0) t3 on t2.bx = t3.cnt; +-- +-- check that a join is disallowed when its lateral reference can only be +-- satisfied by way of a rel that laterally references the join itself +-- + +explain (costs off) +select 1 from int4_tbl t1 left join int4_tbl t2 on true, + lateral (select t2.f1 offset 0) s1 left join int8_tbl t3 on s1.f1 = t3.q1, + lateral (select t3.q2 offset 0) s2 +where s2.q2 > t1.f1; + +select 1 from int4_tbl t1 left join int4_tbl t2 on true, + lateral (select t2.f1 offset 0) s1 left join int8_tbl t3 on s1.f1 = t3.q1, + lateral (select t3.q2 offset 0) s2 +where s2.q2 > t1.f1; + -- -- check that a cloned outer-join qual is not enforced multiple times when -- it is moved into a parameterized join -- 2.37.1 (Apple Git-137.1)