From 20be17ddbe09fbffe0ff10bbbec303e365d210c6 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Mon, 9 Nov 2026 19:28:10 +0800 Subject: [PATCH] Remove redundant constant-propagated outer-join clauses reconsider_outer_join_clauses() can derive an inner-side restriction INNERVAR = CONSTANT when the outer operand of an outer-join clause is known equal to the same constant. The original outer-join clause is then redundant as long as the outer operand has not been null-extended. Clone clauses cannot always be replaced with constant TRUE, because a pushed-down outer join may have already null-extended the outer operand while its OJ relid is still absent from the joinrel. In that case the original strict clause is needed to reject the null-extended row. Commit f4002899 therefore conservatively retained all such clone clauses, which can leave unnecessary join filters in other join orders. Use the existing norm_selec > 1 marker to identify clone clauses made redundant by constant propagation. When selecting clauses for a join or parameterized path, check whether an incompatible outer join that can null the outer operand has both minimum inputs available while its OJ relid is still deferred. Retain the clause in that case; otherwise omit it as redundant. This restores removal of unnecessary join filters without adding additional state to RestrictInfo. --- src/backend/optimizer/util/relnode.c | 90 ++++++++++++++++++++++++++++ src/test/regress/expected/join.out | 41 ++++++------- 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 90a687c32da..f15658aabce 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -66,6 +66,9 @@ static List *build_joinrel_restrictlist(PlannerInfo *root, #ifdef USE_ASSERT_CHECKING static bool no_duplicate_clause_serials(List *clauses); #endif +static bool clone_clause_is_still_needed(PlannerInfo *root, + RestrictInfo *rinfo, + Relids available_relids); static void build_joinrel_joinlist(RelOptInfo *joinrel, RelOptInfo *outer_rel, RelOptInfo *inner_rel); @@ -1453,6 +1456,82 @@ no_duplicate_clause_serials(List *clauses) } #endif +/* + * clone_clause_is_still_needed + * Check whether a clone clause made redundant by constant propagation + * is still needed at this join level. + * + * reconsider_outer_join_clauses() marks such a clause with norm_selec > 1. + * Usually it can be omitted because the derived constant restrictions imply + * it. However, it is still needed if its outer operand has already been + * null-extended by a pushed-down outer join whose OJ relid has not yet been + * added to the joinrel. In that case the strict original clause must reject + * the null-extended row. + */ +static bool +clone_clause_is_still_needed(PlannerInfo *root, RestrictInfo *rinfo, + Relids available_relids) +{ + Relids left_baserels; + Relids right_baserels; + Relids outer_baserels; + + Assert(rinfo->norm_selec > 1.0); + Assert(rinfo->has_clone || rinfo->is_clone); + + /* + * Identify the relids used by the clause's outer operand. Strip OJ + * relids first, since clone variants can carry different nulling bits. + */ + left_baserels = bms_intersect(rinfo->left_relids, root->all_baserels); + right_baserels = bms_intersect(rinfo->right_relids, root->all_baserels); + if (bms_is_subset(left_baserels, rinfo->outer_relids) && + !bms_overlap(right_baserels, rinfo->outer_relids)) + outer_baserels = left_baserels; + else if (bms_is_subset(right_baserels, rinfo->outer_relids) && + !bms_overlap(left_baserels, rinfo->outer_relids)) + outer_baserels = right_baserels; + else + { + bms_free(left_baserels); + bms_free(right_baserels); + return true; /* be conservative */ + } + + foreach_node(SpecialJoinInfo, sjinfo, root->join_info_list) + { + /* + * This OJ is interesting only if it can null the outer operand, is + * incompatible with this clone variant, and can have its relid + * withheld after it has been physically performed. + */ + if (sjinfo->ojrelid == 0 || + !bms_is_member(sjinfo->ojrelid, rinfo->incompatible_relids) || + !bms_overlap(sjinfo->syn_righthand, outer_baserels) || + bms_is_empty(sjinfo->commute_below_l)) + continue; + + /* + * Both minimum inputs are present, so this OJ has physically + * null-extended its RHS. Its relid is nevertheless absent because + * a commuting lower outer join has not been completed yet. + */ + if (!bms_is_member(sjinfo->ojrelid, available_relids) && + bms_is_subset(sjinfo->min_lefthand, available_relids) && + bms_is_subset(sjinfo->min_righthand, available_relids) && + !bms_is_subset(sjinfo->commute_below_l, available_relids)) + { + bms_free(left_baserels); + bms_free(right_baserels); + return true; + } + } + + bms_free(left_baserels); + bms_free(right_baserels); + return false; +} + /* * build_joinrel_restrictlist * build_joinrel_joinlist @@ -1585,6 +1664,11 @@ subbuild_joinrel_restrictlist(PlannerInfo *root, continue; if (bms_overlap(rinfo->incompatible_relids, both_input_relids)) continue; + /* Drop a redundant clone unless a deferred OJ still needs it. */ + if (rinfo->norm_selec > 1.0 && + !clone_clause_is_still_needed(root, rinfo, + both_input_relids)) + continue; } else { @@ -1827,6 +1911,9 @@ get_baserel_parampathinfo(PlannerInfo *root, RelOptInfo *baserel, continue; if (bms_is_member(rinfo->rinfo_serial, pserials)) continue; + if (rinfo->norm_selec > 1.0 && + !clone_clause_is_still_needed(root, rinfo, joinrelids)) + continue; } pclauses = lappend(pclauses, rinfo); @@ -1973,6 +2060,9 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel, continue; if (bms_is_member(rinfo->rinfo_serial, pserials)) continue; + if (rinfo->norm_selec > 1.0 && + !clone_clause_is_still_needed(root, rinfo, join_and_req)) + continue; } pclauses = lappend(pclauses, rinfo); diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 7773852e61b..a7c0c80db2f 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4303,22 +4303,20 @@ select * from left join zt3 on (f2 = f3) left join zt1 zt1b on (f3 = zt1b.f1) where zt1.f1 = 53; - QUERY PLAN ---------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------- Nested Loop Left Join - Join Filter: (zt3.f3 = zt1b.f1) + -> Index Only Scan using zt1_pkey on zt1 + Index Cond: (f1 = 53) -> Nested Loop Left Join - Join Filter: (zt2.f2 = zt3.f3) + -> Index Only Scan using zt2_pkey on zt2 + Index Cond: (f2 = 53) -> Nested Loop Left Join - -> Index Only Scan using zt1_pkey on zt1 + -> Index Only Scan using zt3_pkey on zt3 + Index Cond: (f3 = 53) + -> Index Only Scan using zt1_pkey on zt1 zt1b Index Cond: (f1 = 53) - -> Index Only Scan using zt2_pkey on zt2 - Index Cond: (f2 = 53) - -> Index Only Scan using zt3_pkey on zt3 - Index Cond: (f3 = 53) - -> Index Only Scan using zt1_pkey on zt1 zt1b - Index Cond: (f1 = 53) -(13 rows) +(11 rows) select * from zt1 left join zt2 on (zt1.f1 = f2) @@ -4335,23 +4333,21 @@ explain (costs off) select * from zt1 left join zt1 zt1b on (zt1.f1 = zt1b.f1 and zt1b.f1 = 53) left join (zt3 left join zt2 on (f3 = f2)) on (zt1b.f1 = f3); - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------- Hash Left Join Hash Cond: (zt1.f1 = zt1b.f1) -> Seq Scan on zt1 -> Hash -> Nested Loop Left Join - Join Filter: (zt3.f3 = zt2.f2) + -> Index Only Scan using zt1_pkey on zt1 zt1b + Index Cond: (f1 = 53) -> Nested Loop Left Join - Join Filter: (zt1b.f1 = zt3.f3) - -> Index Only Scan using zt1_pkey on zt1 zt1b - Index Cond: (f1 = 53) -> Index Only Scan using zt3_pkey on zt3 Index Cond: (f3 = 53) - -> Index Only Scan using zt2_pkey on zt2 - Index Cond: (f2 = 53) -(14 rows) + -> Index Only Scan using zt2_pkey on zt2 + Index Cond: (f2 = 53) +(12 rows) select * from zt1 left join zt1 zt1b on (zt1.f1 = zt1b.f1 and zt1b.f1 = 53) @@ -7080,7 +7076,6 @@ where tt1.f1 = ss1.c0; Output: tt4.f1 -> Nested Loop Left Join Output: tt4.f1 - Join Filter: (tt3.f1 = tt4.f1) -> Seq Scan on public.text_tbl tt3 Output: tt3.f1 Filter: (tt3.f1 = 'foo'::text) @@ -7098,7 +7093,7 @@ where tt1.f1 = ss1.c0; Output: (tt4.f1) -> Seq Scan on public.text_tbl tt5 Output: tt4.f1 -(33 rows) +(32 rows) select 1 from text_tbl as tt1 -- 2.43.0