From ba1f1c56cace43757f57420dbdf15b93474e16be Mon Sep 17 00:00:00 2001 From: Narayanan Venkateswaran Date: Thu, 12 Nov 2026 21:28:18 +0000 Subject: [PATCH v3] Skip self-join elimination for relations in grouping sets. Self-join elimination (SJE) removes one side of a self-join on a unique key and rewrites references to the removed relation to point to the kept relation via ChangeVarNodes(). When grouping sets (GROUPING SETS, CUBE, or ROLLUP) are present, each grouping set can independently nullify grouping items that are omitted from that set. When both sides of a self-join appear in the grouping items, SJE's rewrite causes two distinct grouping items to become the exact same Var expression. This causes make_pathkeys_for_sortclauses() to drop the second ORDER BY or DISTINCT key as redundant, and causes setrefs.c to bind HAVING clauses and upper targetlist expressions referencing the second column to the first column. To prevent this while still allowing SJE on other relations in the query, collect the base relids referenced by groupClause when grouping sets are present, and skip SJE for any candidate relation pair that overlaps those relids. --- src/backend/optimizer/plan/analyzejoins.c | 58 +++- src/test/regress/expected/join.out | 325 ++++++++++++++++++++++ src/test/regress/sql/join.sql | 99 +++++++ 3 files changed, 477 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 25a863efbde..521569174f7 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -1775,7 +1775,8 @@ match_unique_clauses(RelOptInfo *outer, List *uclauses, Index relid) * cannot make one that isn't justified. */ static bool -remove_self_joins_one_group(PlannerInfo *root, Relids relids) +remove_self_joins_one_group(PlannerInfo *root, Relids relids, + Relids grouping_relids) { bool removed = false; int k; /* Index of kept relation */ @@ -1785,6 +1786,10 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) { RelOptInfo *rrel = root->simple_rel_array[r]; + /* SJE must not rewrite expressions used as grouping items. */ + if (bms_is_member(r, grouping_relids)) + continue; + /* k iterates over the relids after r */ k = r; while ((k = bms_next_member(relids, k)) > 0) @@ -1804,6 +1809,10 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) Assert(root->simple_rte_array[k]->relid == root->simple_rte_array[r]->relid); + /* SJE must not rewrite expressions used as grouping items. */ + if (bms_is_member(k, grouping_relids)) + continue; + /* * It is impossible to eliminate the join of two relations if they * are not on the same side of every outer join. Otherwise, the @@ -1943,7 +1952,8 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) * Return true if we removed any joins. */ static bool -remove_self_joins_recurse(PlannerInfo *root, List *joinlist) +remove_self_joins_recurse(PlannerInfo *root, List *joinlist, + Relids grouping_relids) { bool removed = false; ListCell *jl; @@ -2000,7 +2010,8 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist) else if (IsA(jlnode, List)) { /* Recursively perform SJE within the sub-joinlist */ - removed |= remove_self_joins_recurse(root, (List *) jlnode); + removed |= remove_self_joins_recurse(root, (List *) jlnode, + grouping_relids); } else elog(ERROR, "unrecognized joinlist node type: %d", @@ -2068,7 +2079,8 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist) relids = bms_del_members(relids, group); /* Try to remove self-joins from the group */ - removed |= remove_self_joins_one_group(root, group); + removed |= remove_self_joins_one_group(root, group, + grouping_relids); bms_free(group); } else @@ -2144,11 +2156,47 @@ self_join_candidates_cmp(const void *a, const void *b) bool remove_useless_self_joins(PlannerInfo *root, List *joinlist) { + Relids grouping_relids = NULL; + bool removed; + /* Skip if SJE is disabled, or if the joinlist has less than 2 members. */ if (!enable_self_join_elimination || joinlist == NIL || (list_length(joinlist) == 1 && !IsA(linitial(joinlist), List))) return false; + /* + * With grouping sets, grouping items that become the same expression + * after SJE are not necessarily interchangeable: either item may be + * replaced with NULL independently. Collect the base relids referenced + * by grouping expressions, so that only SJE candidates that could affect + * those expressions are rejected. + * + * GroupingSet nodes contain sortgrouprefs rather than expressions, so use + * groupClause to locate the corresponding target-list expressions. Pass + * NULL as the root to pull_varnos() so that it recurses into any + * PlaceHolderVars rather than using their ph_eval_at relids. + */ + if (root->parse->groupingSets != NIL) + { + ListCell *lc; + + foreach(lc, root->parse->groupClause) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); + TargetEntry *tle; + Relids expr_relids; + + tle = get_sortgroupclause_tle(sgc, root->parse->targetList); + expr_relids = pull_varnos(NULL, (Node *) tle->expr); + grouping_relids = bms_add_members(grouping_relids, expr_relids); + bms_free(expr_relids); + } + } + /* Try to merge pairs of self-joined relations. */ - return remove_self_joins_recurse(root, joinlist); + removed = remove_self_joins_recurse(root, joinlist, grouping_relids); + + bms_free(grouping_relids); + + return removed; } diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 526430a5ea0..932f3afa6c8 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -9597,6 +9597,331 @@ where t1.a = s.a; Filter: (a IS NOT NULL) (2 rows) +-- Ensure that SJE is not performed on relations referenced in grouping sets, +-- since grouping sets can independently nullify columns that would otherwise +-- be considered equivalent. +-- 1. CUBE with aggregates +explain (costs off) +select t1.a, t2.a, count(*), sum(t1.b) from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) order by 1, 2; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: t1.a, t2.a + -> MixedAggregate + Hash Key: t1.a, t2.a + Hash Key: t1.a + Hash Key: t2.a + Group Key: () + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(12 rows) + +select t1.a, t2.a, count(*), sum(t1.b) from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) order by 1, 2; + a | a | count | sum +---+---+-------+----- + 1 | 1 | 1 | + 1 | | 1 | + 2 | 2 | 1 | 1 + 2 | | 1 | 1 + | 1 | 1 | + | 2 | 1 | 1 + | | 2 | 1 +(7 rows) + +-- 2. ROLLUP +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by rollup (t1.a, t2.a) order by 1, 2; + QUERY PLAN +------------------------------------------------- + Sort + Sort Key: t1.a, t2.a + -> GroupAggregate + Group Key: t1.a, t2.a + Group Key: t1.a + Group Key: () + -> Sort + Sort Key: t1.a + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(13 rows) + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by rollup (t1.a, t2.a) order by 1, 2; + a | a | count +---+---+------- + 1 | 1 | 1 + 1 | | 1 + 2 | 2 | 1 + 2 | | 1 + | | 2 +(5 rows) + +-- 3. Explicit GROUPING SETS +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a, t2.a), (t1.a), (t2.a), ()) order by 1, 2; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: t1.a, t2.a + -> MixedAggregate + Hash Key: t1.a, t2.a + Hash Key: t1.a + Hash Key: t2.a + Group Key: () + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(12 rows) + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a, t2.a), (t1.a), (t2.a), ()) order by 1, 2; + a | a | count +---+---+------- + 1 | 1 | 1 + 1 | | 1 + 2 | 2 | 1 + 2 | | 1 + | 1 | 1 + | 2 | 1 + | | 2 +(7 rows) + +-- 4. Asymmetric GROUPING SETS +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a), (t2.a)) order by 1, 2; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: t1.a, t2.a + -> HashAggregate + Hash Key: t1.a + Hash Key: t2.a + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(10 rows) + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a), (t2.a)) order by 1, 2; + a | a | count +---+---+------- + 1 | | 1 + 2 | | 1 + | 1 | 1 + | 2 | 1 +(4 rows) + +-- 5. Multiple unique keys with CUBE +explain (costs off) +select t1.a, t2.c, count(*) from sj t1 join sj t2 on t1.a = t2.a and t1.c = t2.c +group by cube (t1.a, t2.c) order by 1, 2; + QUERY PLAN +-------------------------------------------------------------- + Sort + Sort Key: t1.a, t2.c + -> MixedAggregate + Hash Key: t1.a, t2.c + Hash Key: t1.a + Hash Key: t2.c + Group Key: () + -> Nested Loop + Join Filter: ((t2.a = t1.a) AND (t1.c = t2.c)) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(12 rows) + +select t1.a, t2.c, count(*) from sj t1 join sj t2 on t1.a = t2.a and t1.c = t2.c +group by cube (t1.a, t2.c) order by 1, 2; + a | c | count +---+---+------- + 1 | 2 | 1 + 1 | | 1 + 2 | 1 | 1 + 2 | | 1 + | 1 | 1 + | 2 | 1 + | | 2 +(7 rows) + +-- 6. Pulled-up subquery with self-join under grouping sets +explain (costs off) +select x, y, count(*) +from (select t1.a as x, t2.a as y from sj t1 join sj t2 on t1.a = t2.a) s +group by cube (x, y) order by 1, 2; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: t1.a, t2.a + -> MixedAggregate + Hash Key: t1.a, t2.a + Hash Key: t1.a + Hash Key: t2.a + Group Key: () + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(12 rows) + +select x, y, count(*) +from (select t1.a as x, t2.a as y from sj t1 join sj t2 on t1.a = t2.a) s +group by cube (x, y) order by 1, 2; + x | y | count +---+---+------- + 1 | 1 | 1 + 1 | | 1 + 2 | 2 | 1 + 2 | | 1 + | 1 | 1 + | 2 | 1 + | | 2 +(7 rows) + +-- 7. CUBE with HAVING and non-Var targetlist expression +select t1.a, t2.a, coalesce(t2.a, -1) +from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) +having t2.a is null +order by 1, 2; + a | a | coalesce +---+---+---------- + 1 | | -1 + 2 | | -1 + | | -1 +(3 rows) + +-- 8. 3-way self-join where t1 and t3 are in GROUP BY (SJE must not +-- transitively merge t1 and t3 via t2) +explain (costs off) +select t1.a, t3.a, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t2.a = t3.a +group by cube (t1.a, t3.a) order by 1, 2; + QUERY PLAN +------------------------------------------------------- + Sort + Sort Key: t1.a, t3.a + -> MixedAggregate + Hash Key: t1.a, t3.a + Hash Key: t1.a + Hash Key: t3.a + Group Key: () + -> Nested Loop + Join Filter: (t1.a = t3.a) + -> Seq Scan on sj t3 + -> Materialize + -> Nested Loop + Join Filter: (t1.a = t2.a) + -> Seq Scan on sj t1 + -> Materialize + -> Seq Scan on sj t2 +(16 rows) + +select t1.a, t3.a, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t2.a = t3.a +group by cube (t1.a, t3.a) order by 1, 2; + a | a | count +---+---+------- + 1 | 1 | 1 + 1 | | 1 + 2 | 2 | 1 + 2 | | 1 + | 1 | 1 + | 2 | 1 + | | 2 +(7 rows) + +-- 9. Grouping sets not referencing the self-joined relations (SJE should +-- still apply to t1 and t2) +explain (costs off) +select t3.b, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t1.a = t3.a +group by rollup (t3.b) order by 1, 2; + QUERY PLAN +--------------------------------------------------- + Sort + Sort Key: t3.b, (count(*)) + -> MixedAggregate + Hash Key: t3.b + Group Key: () + -> Nested Loop + Join Filter: (t2.a = t3.a) + -> Seq Scan on sj t3 + -> Materialize + -> Seq Scan on sj t2 + Filter: (a IS NOT NULL) +(11 rows) + +select t3.b, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t1.a = t3.a +group by rollup (t3.b) order by 1, 2; + b | count +---+------- + 1 | 1 + | 1 + | 2 +(3 rows) + +-- 10. Constant in GROUPING SETS (SJE should still apply) +explain (costs off) +select 1, count(*) +from sj t1 join sj t2 on t1.a = t2.a +group by rollup (1); + QUERY PLAN +--------------------------------- + MixedAggregate + Hash Key: 1 + Group Key: () + -> Seq Scan on sj t2 + Filter: (a IS NOT NULL) +(5 rows) + +-- 11. Outer self-join with inner subquery containing grouping sets (SJE should +-- still apply) +explain (costs off) +select t1.a, t2.a, sub.ct from sj t1 join sj t2 on t1.a = t2.a +join (select a, count(*) as ct from sj group by rollup(a)) sub on t1.a = sub.a +order by 1, 2; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: t2.a + -> Nested Loop + Join Filter: (t2.a = sj.a) + -> MixedAggregate + Hash Key: sj.a + Group Key: () + -> Seq Scan on sj + -> Materialize + -> Seq Scan on sj t2 + Filter: (a IS NOT NULL) +(11 rows) + +select t1.a, t2.a, sub.ct from sj t1 join sj t2 on t1.a = t2.a +join (select a, count(*) as ct from sj group by rollup(a)) sub on t1.a = sub.a +order by 1, 2; + a | a | ct +---+---+---- + 1 | 1 | 1 + 2 | 2 | 1 +(2 rows) + -- Degenerated case. explain (costs off) select * from diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 27b44016717..5062ca59ace 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -3674,6 +3674,105 @@ select * from sj t1 on true where t1.a = s.a; +-- Ensure that SJE is not performed on relations referenced in grouping sets, +-- since grouping sets can independently nullify columns that would otherwise +-- be considered equivalent. +-- 1. CUBE with aggregates +explain (costs off) +select t1.a, t2.a, count(*), sum(t1.b) from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) order by 1, 2; + +select t1.a, t2.a, count(*), sum(t1.b) from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) order by 1, 2; + +-- 2. ROLLUP +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by rollup (t1.a, t2.a) order by 1, 2; + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by rollup (t1.a, t2.a) order by 1, 2; + +-- 3. Explicit GROUPING SETS +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a, t2.a), (t1.a), (t2.a), ()) order by 1, 2; + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a, t2.a), (t1.a), (t2.a), ()) order by 1, 2; + +-- 4. Asymmetric GROUPING SETS +explain (costs off) +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a), (t2.a)) order by 1, 2; + +select t1.a, t2.a, count(*) from sj t1 join sj t2 on t1.a = t2.a +group by grouping sets ((t1.a), (t2.a)) order by 1, 2; + +-- 5. Multiple unique keys with CUBE +explain (costs off) +select t1.a, t2.c, count(*) from sj t1 join sj t2 on t1.a = t2.a and t1.c = t2.c +group by cube (t1.a, t2.c) order by 1, 2; + +select t1.a, t2.c, count(*) from sj t1 join sj t2 on t1.a = t2.a and t1.c = t2.c +group by cube (t1.a, t2.c) order by 1, 2; + +-- 6. Pulled-up subquery with self-join under grouping sets +explain (costs off) +select x, y, count(*) +from (select t1.a as x, t2.a as y from sj t1 join sj t2 on t1.a = t2.a) s +group by cube (x, y) order by 1, 2; + +select x, y, count(*) +from (select t1.a as x, t2.a as y from sj t1 join sj t2 on t1.a = t2.a) s +group by cube (x, y) order by 1, 2; + +-- 7. CUBE with HAVING and non-Var targetlist expression +select t1.a, t2.a, coalesce(t2.a, -1) +from sj t1 join sj t2 on t1.a = t2.a +group by cube (t1.a, t2.a) +having t2.a is null +order by 1, 2; + +-- 8. 3-way self-join where t1 and t3 are in GROUP BY (SJE must not +-- transitively merge t1 and t3 via t2) +explain (costs off) +select t1.a, t3.a, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t2.a = t3.a +group by cube (t1.a, t3.a) order by 1, 2; + +select t1.a, t3.a, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t2.a = t3.a +group by cube (t1.a, t3.a) order by 1, 2; + +-- 9. Grouping sets not referencing the self-joined relations (SJE should +-- still apply to t1 and t2) +explain (costs off) +select t3.b, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t1.a = t3.a +group by rollup (t3.b) order by 1, 2; + +select t3.b, count(*) +from sj t1 join sj t2 on t1.a = t2.a join sj t3 on t1.a = t3.a +group by rollup (t3.b) order by 1, 2; + +-- 10. Constant in GROUPING SETS (SJE should still apply) +explain (costs off) +select 1, count(*) +from sj t1 join sj t2 on t1.a = t2.a +group by rollup (1); + +-- 11. Outer self-join with inner subquery containing grouping sets (SJE should +-- still apply) +explain (costs off) +select t1.a, t2.a, sub.ct from sj t1 join sj t2 on t1.a = t2.a +join (select a, count(*) as ct from sj group by rollup(a)) sub on t1.a = sub.a +order by 1, 2; + +select t1.a, t2.a, sub.ct from sj t1 join sj t2 on t1.a = t2.a +join (select a, count(*) as ct from sj group by rollup(a)) sub on t1.a = sub.a +order by 1, 2; + -- Degenerated case. explain (costs off) select * from -- 2.56.0.611.g1030f0b940-goog