From 15025ed7550589bb2edfba63e1762b52d931f63b Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 8 Sep 2026 15:45:23 +0900 Subject: [PATCH v1] Fix duplicate enforcement of EC-derived conditions A clause connecting the same two EC members can be requested from the EC machinery in different contexts. When building parameterized index paths for a relation, generate_implied_equalities_for_column derives an indexable clause with parent_ec set, to mark it as redundant with other join clauses derived from the same EC. Meanwhile, at a join where the relations of a multi-relation EC member first come together, generate_join_implied_equalities emits a clause equating the same two members with parent_ec unset, since that clause is the sole enforcer of its member there and must not be dropped as redundant. Because create_join_clause treats parent_ec as part of its cache-lookup key, such requests can yield two different RestrictInfos for the same condition, carrying different rinfo_serials. This breaks the assumption that one condition has exactly one serial number, which create_nestloop_path relies on to drop join clauses that are already enforced within a parameterized inner path. As a result, the same condition could be enforced twice at different plan levels, wasting execution effort and applying the clause's selectivity twice, underestimating the join's row count. To fix, make create_join_clause copy the rinfo_serial from an existing clause that connects the same two members with the opposite parent_ec marking, so that the two clauses keep their distinct markings but are recognized as the same condition. --- src/backend/optimizer/path/equivclass.c | 15 ++++++ src/test/regress/expected/join.out | 63 ++++++++++++++++++++----- src/test/regress/sql/join.sql | 24 ++++++++++ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 393a7a69742..14e12861adc 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -2039,6 +2039,21 @@ create_join_clause(PlannerInfo *root, /* If it's a child clause, copy the parent's rinfo_serial */ if (parent_rinfo) rinfo->rinfo_serial = parent_rinfo->rinfo_serial; + else + { + RestrictInfo *counterpart; + + /* + * If a clause comparing the same two EMs already exists with the + * opposite parent_ec marking, adopt its rinfo_serial: the two clauses + * enforce the same condition, and they must share a serial number + * lest we enforce that condition more than once in a plan. + */ + counterpart = ec_search_clause_for_ems(root, ec, leftem, rightem, + parent_ec ? NULL : ec); + if (counterpart) + rinfo->rinfo_serial = counterpart->rinfo_serial; + } /* Mark the clause as redundant, or not */ rinfo->parent_ec = parent_ec; diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index cee3a7b1f22..825bab78b87 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -2337,17 +2337,17 @@ select a.f1, b.f1, t.thousand, t.tenthous from (select sum(f1)+1 as f1 from int4_tbl i4a) a, (select sum(f1) as f1 from int4_tbl i4b) b where b.f1 = t.thousand and a.f1 = b.f1 and (a.f1+b.f1+999) = t.tenthous; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------- Nested Loop + -> Aggregate + -> Seq Scan on int4_tbl i4a -> Nested Loop Join Filter: ((sum(i4b.f1)) = ((sum(i4a.f1) + 1))) - -> Aggregate - -> Seq Scan on int4_tbl i4a -> Aggregate -> Seq Scan on int4_tbl i4b - -> Index Only Scan using tenk1_thous_tenthous on tenk1 t - Index Cond: ((thousand = (sum(i4b.f1))) AND (tenthous = ((((sum(i4a.f1) + 1)) + (sum(i4b.f1))) + 999))) + -> Index Only Scan using tenk1_thous_tenthous on tenk1 t + Index Cond: ((thousand = (sum(i4b.f1))) AND (tenthous = ((((sum(i4a.f1) + 1)) + (sum(i4b.f1))) + 999))) (9 rows) select a.f1, b.f1, t.thousand, t.tenthous from @@ -4860,17 +4860,17 @@ select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from left join tenk1 t2 on (subq1.y1 = t2.unique1) where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------- Nested Loop -> Nested Loop Join Filter: (t1.stringu1 > t2.stringu2) -> Nested Loop + -> Seq Scan on onerow onerow_1 -> Nested Loop -> Seq Scan on onerow - -> Seq Scan on onerow onerow_1 - -> Index Scan using tenk1_unique2 on tenk1 t1 - Index Cond: ((unique2 = (11)) AND (unique2 < 42)) + -> Index Scan using tenk1_unique2 on tenk1 t1 + Index Cond: ((unique2 = (11)) AND (unique2 < 42)) -> Index Scan using tenk1_unique1 on tenk1 t2 Index Cond: (unique1 = (3)) -> Seq Scan on int4_tbl i1 @@ -4915,7 +4915,6 @@ where t1.unique1 < i4.f1; ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Nested Loop Output: (64)::information_schema.cardinal_number - Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer) -> Seq Scan on public.tenk1 t3 Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4 Filter: (t3.fivethous < 0) @@ -4946,7 +4945,7 @@ where t1.unique1 < i4.f1; -> Seq Scan on public.int8_tbl i8 Output: i8.q1, i8.q2 Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer) -(33 rows) +(32 rows) select ss1.d1 from tenk1 as t1 @@ -6738,6 +6737,44 @@ select count(*) from int4_tbl t1 left join 25 (1 row) +-- +-- check that an EC-derived condition is not enforced twice, both within a +-- parameterized path and at the join above it +-- +begin; +set local from_collapse_limit to 1; +explain (costs off) +select count(*) from int4_tbl t1, + lateral (select * from tenk1 t2, + lateral (select t2.ten as x offset 0) s0 + join tenk1 t3 on t3.unique2 = t1.f1 + where t3.unique1 = t2.hundred + s0.x) ss1; + QUERY PLAN +--------------------------------------------------------------------------- + Aggregate + -> Nested Loop + Join Filter: (t3.unique2 = t1.f1) + -> Nested Loop + -> Seq Scan on tenk1 t2 + -> Nested Loop + -> Result + -> Index Scan using tenk1_unique1 on tenk1 t3 + Index Cond: (unique1 = (t2.hundred + (t2.ten))) + -> Materialize + -> Seq Scan on int4_tbl t1 +(11 rows) + +select count(*) from int4_tbl t1, + lateral (select * from tenk1 t2, + lateral (select t2.ten as x offset 0) s0 + join tenk1 t3 on t3.unique2 = t1.f1 + where t3.unique1 = t2.hundred + s0.x) ss1; + count +------- + 0 +(1 row) + +rollback; -- -- test successful handling of full join underneath left join (bug #14105) -- diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index b353073f21e..b4a3a043323 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2424,6 +2424,30 @@ select count(*) from int4_tbl t1 left join join int4_tbl t4 on t3.cnt = t4.f1) on t2.bx = t3.cnt + t4.f1; +-- +-- check that an EC-derived condition is not enforced twice, both within a +-- parameterized path and at the join above it +-- + +begin; + +set local from_collapse_limit to 1; + +explain (costs off) +select count(*) from int4_tbl t1, + lateral (select * from tenk1 t2, + lateral (select t2.ten as x offset 0) s0 + join tenk1 t3 on t3.unique2 = t1.f1 + where t3.unique1 = t2.hundred + s0.x) ss1; + +select count(*) from int4_tbl t1, + lateral (select * from tenk1 t2, + lateral (select t2.ten as x offset 0) s0 + join tenk1 t3 on t3.unique2 = t1.f1 + where t3.unique1 = t2.hundred + s0.x) ss1; + +rollback; + -- -- test successful handling of full join underneath left join (bug #14105) -- -- 2.37.1 (Apple Git-137.1)