From 7619d70ba6d652e7881403853563102ff2ac03ba Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Mon, 5 Oct 2026 15:51:19 +0900 Subject: [PATCH v1] Don't build child-join EC members that reference outside rels add_child_join_rel_equivalences() built a child version of every multi-relation EquivalenceClass member that overlapped the child join's topmost parent relids. For a member that also references a relation outside the join, the child version is of no use for what these members exist for, which is matching sort pathkeys of the child join, since such a member cannot be computed there. The only effect of these members was on the clauses generated for parameterized child-join paths, and it was a harmful one. Each child join nested within a larger child join supplies its own version of such a member, as does each rebuild of the same child join by GEQO. These versions are all the same expression, yet generate_join_implied_equalities() would equate them to each other, producing redundant clauses that share one rinfo_serial and so trip the assertion added by commit 8510bcc98. Even a lone such member could yield a join clause that duplicates a condition already enforced by the inner path, as seen in an existing regression test whose plan now loses a redundant join filter. To fix, consider only members that can be computed at the child join's topmost parent joinrel. This matches what add_child_rel_equivalences() has done for base relations since commit 2489d76c4. In passing, simplify how both functions compute the child member's relids. Now that the parent member's relids are known to lie within the parent rel, the child member's relids are simply the child rel's. Back-patch to v19, as with commit 8510bcc98. --- src/backend/optimizer/path/equivclass.c | 33 ++--- src/test/regress/expected/partition_join.out | 122 ++++++++++++++++++- src/test/regress/sql/partition_join.sql | 25 ++++ 3 files changed, 157 insertions(+), 23 deletions(-) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index e2fbf214727..183cc2f1893 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -2859,7 +2859,6 @@ add_child_rel_equivalences(PlannerInfo *root, { /* OK, generate transformed child version */ Expr *child_expr; - Relids new_relids; if (parent_rel->reloptkind == RELOPT_BASEREL) { @@ -2880,20 +2879,17 @@ add_child_rel_equivalences(PlannerInfo *root, } /* - * Transform em_relids to match. Note we do *not* do + * The member's relids are all within the parent, so the child + * version's relids are just the child's. Note we do *not* do * pull_varnos(child_expr) here, as for example the * transformation might have substituted a constant, but we * don't want the child member to be marked as constant. */ - new_relids = bms_difference(cur_em->em_relids, - top_parent_relids); - new_relids = bms_add_members(new_relids, child_relids); - add_child_eq_member(root, cur_ec, i, child_expr, - new_relids, + bms_copy(child_relids), cur_em->em_jdomain, cur_em, cur_em->em_datatype, @@ -2971,12 +2967,14 @@ add_child_join_rel_equivalences(PlannerInfo *root, if (bms_membership(cur_em->em_relids) != BMS_MULTIPLE) continue; - /* Does this member reference child's topmost parent rel? */ - if (bms_overlap(cur_em->em_relids, top_parent_relids)) + /* + * Consider only members that can be computed at child's topmost + * parent joinrel, as in add_child_rel_equivalences. + */ + if (bms_is_subset(cur_em->em_relids, top_parent_relids)) { /* Yes, generate transformed child version */ Expr *child_expr; - Relids new_relids; if (parent_joinrel->reloptkind == RELOPT_JOINREL) { @@ -2997,16 +2995,6 @@ add_child_join_rel_equivalences(PlannerInfo *root, child_joinrel->top_parent); } - /* - * Transform em_relids to match. Note we do *not* do - * pull_varnos(child_expr) here, as for example the - * transformation might have substituted a constant, but we - * don't want the child member to be marked as constant. - */ - new_relids = bms_difference(cur_em->em_relids, - top_parent_relids); - new_relids = bms_add_members(new_relids, child_relids); - /* * Add new child member to the EquivalenceClass. Because this * is a RELOPT_OTHER_JOINREL which has multiple component @@ -3026,12 +3014,15 @@ add_child_join_rel_equivalences(PlannerInfo *root, * for all the component relids, then that would just result * in eclass_member_iterator_next() finding the member * multiple times, which is a waste of effort. + * + * As in add_child_rel_equivalences, the child member's relids + * are just the child's. */ add_child_eq_member(root, cur_ec, -1, child_expr, - new_relids, + bms_copy(child_relids), cur_em->em_jdomain, cur_em, cur_em->em_datatype, diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out index a7e26afa793..ba6ba68ed8c 100644 --- a/src/test/regress/expected/partition_join.out +++ b/src/test/regress/expected/partition_join.out @@ -806,12 +806,11 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN -> Seq Scan on prt1_p3 t4_3 Filter: (b = 0) -> Nested Loop - Join Filter: (((COALESCE(t3_3.c, t4_3.c)))::text = (t1_3.c)::text) -> Index Scan using prt3_p3_a_idx on prt3_p3 t3_3 Index Cond: ((a = t4_3.a) AND (a IS NOT NULL)) -> Index Only Scan using prt4_p3_c_a_idx on prt4_p3 t1_3 Index Cond: ((c = ((COALESCE(t3_3.c, t4_3.c)))::text) AND (a = t3_3.a)) -(28 rows) +(27 rows) SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN (SELECT t3.a, COALESCE(t3.c, t4.c) AS c FROM prt3 t3 JOIN prt1 t4 ON t3.a = t4.a @@ -829,6 +828,125 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN RESET enable_hashjoin; RESET enable_mergejoin; +-- bug with duplicate clauses for a parameterized child join, when an +-- EquivalenceClass member references a rel outside the join +EXPLAIN (COSTS OFF) +SELECT t1.a, t1.c, t3.c, t5.f1 FROM prt4 t1 JOIN prt3 t2 ON t1.a = t2.a + JOIN prt1 t3 ON t2.a = t3.a, int4_tbl t5 + WHERE t1.c = COALESCE(t3.c, t5.f1::text) AND t3.b = 0 ORDER BY t1.a, t5.f1; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: t1.a, t5.f1 + -> Nested Loop + -> Nested Loop + Join Filter: ((t1.c)::text = (COALESCE(t3.c, ((t5.f1)::text)::character varying))::text) + -> Append + -> Hash Join + Hash Cond: (t1_1.a = t3_1.a) + -> Seq Scan on prt4_p1 t1_1 + -> Hash + -> Seq Scan on prt1_p1 t3_1 + Filter: (b = 0) + -> Hash Join + Hash Cond: (t1_2.a = t3_2.a) + -> Seq Scan on prt4_p2 t1_2 + -> Hash + -> Seq Scan on prt1_p2 t3_2 + Filter: (b = 0) + -> Hash Join + Hash Cond: (t1_3.a = t3_3.a) + -> Seq Scan on prt4_p3 t1_3 + -> Hash + -> Seq Scan on prt1_p3 t3_3 + Filter: (b = 0) + -> Materialize + -> Seq Scan on int4_tbl t5 + -> Append + -> Index Only Scan using prt3_p1_a_idx on prt3_p1 t2_1 + Index Cond: (a = t1.a) + -> Index Only Scan using prt3_p2_a_idx on prt3_p2 t2_2 + Index Cond: (a = t1.a) + -> Index Only Scan using prt3_p3_a_idx on prt3_p3 t2_3 + Index Cond: (a = t1.a) +(33 rows) + +SELECT t1.a, t1.c, t3.c, t5.f1 FROM prt4 t1 JOIN prt3 t2 ON t1.a = t2.a + JOIN prt1 t3 ON t2.a = t3.a, int4_tbl t5 + WHERE t1.c = COALESCE(t3.c, t5.f1::text) AND t3.b = 0 ORDER BY t1.a, t5.f1; + a | c | c | f1 +---+------+------+------------- + 0 | 0000 | 0000 | -2147483647 + 0 | 0000 | 0000 | -2147483647 + 0 | 0000 | 0000 | -123456 + 0 | 0000 | 0000 | -123456 + 0 | 0000 | 0000 | 0 + 0 | 0000 | 0000 | 0 + 0 | 0000 | 0000 | 123456 + 0 | 0000 | 0000 | 123456 + 0 | 0000 | 0000 | 2147483647 + 0 | 0000 | 0000 | 2147483647 +(10 rows) + +-- same, with the child joins built repeatedly by GEQO +SET geqo_threshold = 2; +EXPLAIN (COSTS OFF) +SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN + (SELECT t3.a, COALESCE(t3.c, t4.c) AS c FROM prt3 t3 JOIN prt1 t4 ON t3.a = t4.a + WHERE t4.b = 0) t2 ON t1.a = t2.a + WHERE t1.c = t2.c AND t2.a IS NOT NULL ORDER BY t1.a, t1.c; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: t1.a, t1.c + -> Append + -> Nested Loop + Join Filter: ((t3_1.a = t4_1.a) AND ((t1_1.c)::text = (COALESCE(t3_1.c, t4_1.c))::text)) + -> Hash Join + Hash Cond: (t1_1.a = t4_1.a) + -> Seq Scan on prt4_p1 t1_1 + -> Hash + -> Seq Scan on prt1_p1 t4_1 + Filter: (b = 0) + -> Index Scan using prt3_p1_a_idx on prt3_p1 t3_1 + Index Cond: ((a = t1_1.a) AND (a IS NOT NULL)) + -> Nested Loop + Join Filter: ((t3_2.a = t4_2.a) AND ((t1_2.c)::text = (COALESCE(t3_2.c, t4_2.c))::text)) + -> Hash Join + Hash Cond: (t1_2.a = t4_2.a) + -> Seq Scan on prt4_p2 t1_2 + -> Hash + -> Seq Scan on prt1_p2 t4_2 + Filter: (b = 0) + -> Index Scan using prt3_p2_a_idx on prt3_p2 t3_2 + Index Cond: ((a = t1_2.a) AND (a IS NOT NULL)) + -> Nested Loop + Join Filter: ((t3_3.a = t4_3.a) AND ((t1_3.c)::text = (COALESCE(t3_3.c, t4_3.c))::text)) + -> Hash Join + Hash Cond: (t1_3.a = t4_3.a) + -> Seq Scan on prt4_p3 t1_3 + -> Hash + -> Seq Scan on prt1_p3 t4_3 + Filter: (b = 0) + -> Index Scan using prt3_p3_a_idx on prt3_p3 t3_3 + Index Cond: ((a = t1_3.a) AND (a IS NOT NULL)) +(33 rows) + +SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN + (SELECT t3.a, COALESCE(t3.c, t4.c) AS c FROM prt3 t3 JOIN prt1 t4 ON t3.a = t4.a + WHERE t4.b = 0) t2 ON t1.a = t2.a + WHERE t1.c = t2.c AND t2.a IS NOT NULL ORDER BY t1.a, t1.c; + a | c | a | c +-----+------+-----+------ + 0 | 0000 | 0 | 0000 + 0 | 0001 | 0 | 0001 + 250 | 0000 | 250 | 0000 + 250 | 0001 | 250 | 0001 + 500 | 0000 | 500 | 0000 + 500 | 0001 | 500 | 0001 +(6 rows) + +RESET geqo_threshold; -- bug in freeing the SpecialJoinInfo of a child-join EXPLAIN (COSTS OFF) SELECT * FROM prt1 t1 JOIN prt1 t2 ON t1.a = t2.a WHERE t1.a IN (SELECT a FROM prt1 t3); diff --git a/src/test/regress/sql/partition_join.sql b/src/test/regress/sql/partition_join.sql index 3dc3e0bbc93..385849994ea 100644 --- a/src/test/regress/sql/partition_join.sql +++ b/src/test/regress/sql/partition_join.sql @@ -191,6 +191,31 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN RESET enable_hashjoin; RESET enable_mergejoin; +-- bug with duplicate clauses for a parameterized child join, when an +-- EquivalenceClass member references a rel outside the join +EXPLAIN (COSTS OFF) +SELECT t1.a, t1.c, t3.c, t5.f1 FROM prt4 t1 JOIN prt3 t2 ON t1.a = t2.a + JOIN prt1 t3 ON t2.a = t3.a, int4_tbl t5 + WHERE t1.c = COALESCE(t3.c, t5.f1::text) AND t3.b = 0 ORDER BY t1.a, t5.f1; +SELECT t1.a, t1.c, t3.c, t5.f1 FROM prt4 t1 JOIN prt3 t2 ON t1.a = t2.a + JOIN prt1 t3 ON t2.a = t3.a, int4_tbl t5 + WHERE t1.c = COALESCE(t3.c, t5.f1::text) AND t3.b = 0 ORDER BY t1.a, t5.f1; + +-- same, with the child joins built repeatedly by GEQO +SET geqo_threshold = 2; + +EXPLAIN (COSTS OFF) +SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN + (SELECT t3.a, COALESCE(t3.c, t4.c) AS c FROM prt3 t3 JOIN prt1 t4 ON t3.a = t4.a + WHERE t4.b = 0) t2 ON t1.a = t2.a + WHERE t1.c = t2.c AND t2.a IS NOT NULL ORDER BY t1.a, t1.c; +SELECT t1.a, t1.c, t2.a, t2.c FROM prt4 t1 LEFT JOIN + (SELECT t3.a, COALESCE(t3.c, t4.c) AS c FROM prt3 t3 JOIN prt1 t4 ON t3.a = t4.a + WHERE t4.b = 0) t2 ON t1.a = t2.a + WHERE t1.c = t2.c AND t2.a IS NOT NULL ORDER BY t1.a, t1.c; + +RESET geqo_threshold; + -- bug in freeing the SpecialJoinInfo of a child-join EXPLAIN (COSTS OFF) SELECT * FROM prt1 t1 JOIN prt1 t2 ON t1.a = t2.a WHERE t1.a IN (SELECT a FROM prt1 t3); -- 2.37.1 (Apple Git-137.1)