From 55b9f54dcb467cf6b466dbd96bff4b66fd60065b Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Sat, 17 Oct 2026 10:30:55 +0900 Subject: [PATCH] Fix skipping of join quals for appendrel children Commit 767c59895 made set_append_rel_size() skip join quals that came from above an outer join that can null the parent rel. Such quals are of no use while generating paths for the child, and skipping them also keeps them away from adjust_appendrel_attrs(), which cannot apply nullingrels to a non-Var translation. However, it identified such quals by looking at their clause_relids. That misses a reference to the parent rel that is nulled by such an outer join but appears only within a PlaceHolderVar, because pull_varnos() reports only where the PHV is evaluated. Such a qual could then reach adjust_appendrel_attrs(), which failed with "failed to apply nullingrels to a non-Var" if the referenced output of a UNION ALL member was not a plain Var. To fix, also skip quals whose clause_relids overlap the parent rel's lateral_referencers. A reference to the parent rel within a PHV that is evaluated elsewhere is necessarily a lateral reference, so this catches all such quals. These quals are no use for the child either, since they can never be enforced by a scan of the parent rel; this is the same reasoning join_clause_is_movable_to() uses. Back-patch to v16, where commit 767c59895 was introduced. Author: Richard Guo Reviewed-by: wenhui qiu Discussion: https://postgr.es/m/CAMbWs4_fzrrN9G6-wLwwrdM-n=fwj3Uhr1zkgj3o91EEC-CuEA@mail.gmail.com Backpatch-through: 16 --- src/backend/optimizer/path/allpaths.c | 11 ++++- src/test/regress/expected/join.out | 70 +++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 14 ++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 9c42616f024..e5f6957e5de 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -1043,13 +1043,22 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel, * for the child. This saves some effort while processing the child * rel, and it also avoids an implementation restriction in * adjust_appendrel_attrs (it can't apply nullingrels to a non-Var). + * + * clause_relids does not reflect references to this rel that appear + * within PlaceHolderVars, but those are lateral references, so we + * also skip quals that use any rel laterally referencing this rel. + * This is safe since such quals are useless for the child: enforcing + * one at a scan of this rel would require the scan to be + * parameterized by a rel that laterally references this rel, which is + * impossible since that rel needs this rel's output first. */ childrinfos = NIL; foreach(lc, rel->joininfo) { RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); - if (!bms_overlap(rinfo->clause_relids, rel->nulling_relids)) + if (!bms_overlap(rinfo->clause_relids, rel->nulling_relids) && + !bms_overlap(rinfo->clause_relids, rel->lateral_referencers)) childrinfos = lappend(childrinfos, adjust_appendrel_attrs(root, (Node *) rinfo, diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 26bd25bc460..56e3fdcc6cb 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4371,6 +4371,76 @@ where ss.x is null; Output: 'bar'::text (12 rows) +-- Test a join qual referencing a nulled appendrel output only within a PHV +explain (verbose, costs off) +select 1 from int4_tbl t1 + left join (select 1 as x union all select 2) s on true + left join lateral (select s.x as y from int4_tbl t2) ss on true + left join int4_tbl t3 on ss.y is null; + QUERY PLAN +-------------------------------------------------- + Nested Loop Left Join + Output: 1 + Join Filter: (((1)) IS NULL) + -> Nested Loop Left Join + Output: ((1)) + -> Nested Loop Left Join + Output: (1) + -> Seq Scan on public.int4_tbl t1 + Output: t1.f1 + -> Materialize + Output: (1) + -> Append + -> Result + Output: 1 + -> Result + Output: 2 + -> Seq Scan on public.int4_tbl t2 + Output: (1) + -> Materialize + -> Seq Scan on public.int4_tbl t3 +(20 rows) + +-- Same, but the join qual's required_relids don't include the nulling OJ +explain (verbose, costs off) +select 1 from ((int4_tbl t1 left join (select 1 as x union all select 2) s on t1.f1 < s.x) + join lateral (select coalesce(s.x, t2.f1) as y from int4_tbl t2) ss on true) + full join int4_tbl t3 on t3.f1 = t1.f1 and ss.y = t3.f1 + right join int4_tbl t4 on t3.f1 = t4.f1; + QUERY PLAN +--------------------------------------------------------------------------- + Hash Right Join + Output: 1 + Hash Cond: (t3.f1 = t4.f1) + -> Hash Right Join + Output: t3.f1 + Hash Cond: ((t1.f1 = t3.f1) AND ((COALESCE((1), t2.f1)) = t3.f1)) + -> Nested Loop + Output: t1.f1, (COALESCE((1), t2.f1)) + -> Nested Loop Left Join + Output: t1.f1, (1) + Join Filter: (t1.f1 < (1)) + -> Seq Scan on public.int4_tbl t1 + Output: t1.f1 + -> Materialize + Output: (1) + -> Append + -> Result + Output: 1 + -> Result + Output: 2 + -> Seq Scan on public.int4_tbl t2 + Output: COALESCE((1), t2.f1) + -> Hash + Output: t3.f1 + -> Seq Scan on public.int4_tbl t3 + Output: t3.f1 + -> Hash + Output: t4.f1 + -> Seq Scan on public.int4_tbl t4 + Output: t4.f1 +(30 rows) + -- Test computation of varnullingrels when translating appendrel Var begin; create temp table t_append (a int not null, b int); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 871daf76666..db827c18377 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1457,6 +1457,20 @@ select * from int4_tbl left join ( ) ss(x) on true where ss.x is null; +-- Test a join qual referencing a nulled appendrel output only within a PHV +explain (verbose, costs off) +select 1 from int4_tbl t1 + left join (select 1 as x union all select 2) s on true + left join lateral (select s.x as y from int4_tbl t2) ss on true + left join int4_tbl t3 on ss.y is null; + +-- Same, but the join qual's required_relids don't include the nulling OJ +explain (verbose, costs off) +select 1 from ((int4_tbl t1 left join (select 1 as x union all select 2) s on t1.f1 < s.x) + join lateral (select coalesce(s.x, t2.f1) as y from int4_tbl t2) ss on true) + full join int4_tbl t3 on t3.f1 = t1.f1 and ss.y = t3.f1 + right join int4_tbl t4 on t3.f1 = t4.f1; + -- Test computation of varnullingrels when translating appendrel Var begin;