From 23e7ca78a003fb6ae5d983f3a093faaecf0778b8 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Sat, 17 Oct 2026 10:28:53 +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 | 78 +++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 14 +++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 85a3b56def0..295e3509c74 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -1126,13 +1126,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 b9c791f3821..738d4b71c31 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -5631,6 +5631,84 @@ 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 + -> Memoize + Output: ((1)) + Cache Key: (1) + Cache Mode: binary + -> Seq Scan on public.int4_tbl t2 + Output: (1) + -> Materialize + -> Seq Scan on public.int4_tbl t3 +(24 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 + -> Memoize + Output: (COALESCE((1), t2.f1)) + Cache Key: (1) + Cache Mode: binary + -> 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 +(34 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 d51e702c3f6..9b8ccaadcfe 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1937,6 +1937,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;