From 848994addce4b913e9e95c56fb9efd9757195ab6 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 13 Oct 2026 00:00:01 +0900 Subject: [PATCH v1 2/2] Don't require outer joins for lateral PHV references outside subqueries When a rel laterally references a PHV, create_lateral_join_info() adds the PHV's whole ph_eval_at to the rel's lateral_relids, including any outer-join relids. That forces those outer joins to be formed before the rel can be joined, which join_is_legal() enforces since commit 55fa1b067. That is necessary for a subquery. A subquery is planned separately and references the PHV through a single Param, so some nestloop must supply the PHV's value as a whole, and that is only possible once the outer joins in its ph_eval_at have been formed. But a function, VALUES or tablesample scan does not need the PHV as a whole. When the outer joins in its ph_eval_at have not been formed yet, replace_nestloop_params() recurses into the PHV's expression and parameterizes the Vars within it, so the scan computes the expression itself. That gives the same answer, because the scan can only be joined below such an outer join via outer-join identity 3, which requires its join clause to be strict for the nullable side. So the rows the outer join would null-extend cannot match it anyway. We already rely on this for PHVs that reference other PHVs laterally, since commit efeb12ef0. For such rels the outer-join relids are therefore not a real requirement, and listing them needlessly rejects join orders that identity 3 permits. To fix, include outer-join relids in lateral_relids only when the referencing rel is a subquery. --- src/backend/optimizer/plan/initsplan.c | 16 ++++++++++-- src/test/regress/expected/join.out | 35 ++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 17 +++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 199e5d0a580..a7032c8514a 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -1223,8 +1223,20 @@ create_lateral_join_info(PlannerInfo *root) PlaceHolderInfo *phinfo = find_placeholder_info(root, phv); found_laterals = true; - lateral_relids = bms_add_members(lateral_relids, - phinfo->ph_eval_at); + + /* + * A subquery needs the PHV as a whole, so any outer joins in + * its ph_eval_at must be formed first. Other rels can + * compute the PHV's expression themselves, so they need only + * the baserels. + */ + if (brel->rtekind == RTE_SUBQUERY) + lateral_relids = bms_add_members(lateral_relids, + phinfo->ph_eval_at); + else + lateral_relids = bms_add_members(lateral_relids, + bms_intersect(phinfo->ph_eval_at, + root->all_baserels)); } else Assert(false); diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index a309732be20..3267aee5d03 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -7030,6 +7030,41 @@ select count(*) from int4_tbl t1 left join 25 (1 row) +-- but a function scan can compute the PHV's expression itself, so the join +-- is allowed +explain (costs off) +select count(*), sum(f) from int8_tbl d left join + (select coalesce(t2.q1, 0) as x, t2.q1 as q + from int8_tbl t1 left join int8_tbl t2 on true) ss + on true + left join lateral generate_series(ss.x, 124) f + on ss.q = 123; + QUERY PLAN +------------------------------------------------------------------------ + Aggregate + -> Nested Loop Left Join + -> Seq Scan on int8_tbl d + -> Materialize + -> Nested Loop Left Join + -> Seq Scan on int8_tbl t1 + -> Materialize + -> Nested Loop Left Join + Join Filter: (t2.q1 = 123) + -> Seq Scan on int8_tbl t2 + -> Function Scan on generate_series f +(11 rows) + +select count(*), sum(f) from int8_tbl d left join + (select coalesce(t2.q1, 0) as x, t2.q1 as q + from int8_tbl t1 left join int8_tbl t2 on true) ss + on true + left join lateral generate_series(ss.x, 124) f + on ss.q = 123; + count | sum +-------+------- + 175 | 12350 +(1 row) + -- -- 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 9c62a418bcf..5f1c487a364 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2513,6 +2513,23 @@ 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; +-- but a function scan can compute the PHV's expression itself, so the join +-- is allowed +explain (costs off) +select count(*), sum(f) from int8_tbl d left join + (select coalesce(t2.q1, 0) as x, t2.q1 as q + from int8_tbl t1 left join int8_tbl t2 on true) ss + on true + left join lateral generate_series(ss.x, 124) f + on ss.q = 123; + +select count(*), sum(f) from int8_tbl d left join + (select coalesce(t2.q1, 0) as x, t2.q1 as q + from int8_tbl t1 left join int8_tbl t2 on true) ss + on true + left join lateral generate_series(ss.x, 124) f + on ss.q = 123; + -- -- 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)