From fa703ad98206a477c42b38cd7b28bc6a3aceb1c0 Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 18:14:25 -0700
Subject: [PATCH v2 15/17] Drop a deduplication repeated by the query's own
 grouping

A deduplication pushed below the joins can group on the same keys the
query groups by, and the Agg placed above it then groups on them a second
time. We should plan its subpath instead.
---
 src/backend/optimizer/plan/planner.c          | 44 +++++++++++++
 src/test/regress/expected/eager_aggregate.out | 61 +++++++++----------
 src/test/regress/sql/eager_aggregate.sql      |  4 +-
 3 files changed, 74 insertions(+), 35 deletions(-)

diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fc8a0d63d40..8b11e3dd930 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -4017,6 +4017,38 @@ get_number_of_groups(PlannerInfo *root,
 	return dNumGroups;
 }
 
+/*
+ * dedup_groups_as_query_does
+ *	  Does this deduplication group on exactly the query's grouping keys?
+ *
+ * The caller's grouping Agg groups on those keys again, so it can take the
+ * path's subpath instead.
+ */
+static bool
+dedup_groups_as_query_does(PlannerInfo *root, Path *path)
+{
+	AggPath    *aggpath;
+	ListCell   *lc;
+
+	if (!IsA(path, AggPath))
+		return false;
+
+	aggpath = (AggPath *) path;
+
+	if (aggpath->groupClause == NIL ||
+		list_length(aggpath->groupClause) !=
+		list_length(root->processed_groupClause))
+		return false;
+
+	foreach(lc, aggpath->groupClause)
+	{
+		if (!list_member_ptr(root->processed_groupClause, lfirst(lc)))
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * create_grouping_paths
  *
@@ -4408,6 +4440,18 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 		{
 			Path	   *path = (Path *) lfirst(lc);
 
+			/*
+			 * The aggregate above repeats a deduplication on the query's own
+			 * keys, so take its input instead.  Any projection under it goes
+			 * too, since we project onto the target we want here anyway.
+			 */
+			if (dedup_groups_as_query_does(root, path))
+			{
+				path = ((AggPath *) path)->subpath;
+				if (IsA(path, ProjectionPath))
+					path = ((ProjectionPath *) path)->subpath;
+			}
+
 			add_path(dedup_rel,
 					 (Path *) create_projection_path(root, dedup_rel,
 													 path,
diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out
index 3e9451d2a7a..18579bcd788 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -2092,30 +2092,27 @@ SELECT t1.id
   FROM eager_distinct_t1 t1
   JOIN eager_distinct_t2 t2 ON t2.t1_id = t1.id
 GROUP BY t1.id ORDER BY t1.id;
-                                 QUERY PLAN                                  
------------------------------------------------------------------------------
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
  Group
    Output: t1.id
    Group Key: t1.id
-   ->  GroupAggregate
+   ->  Sort
          Output: t1.id
-         Group Key: t1.id
-         ->  Sort
+         Sort Key: t1.id
+         ->  Hash Join
                Output: t1.id
-               Sort Key: t1.id
-               ->  Hash Join
-                     Output: t1.id
-                     Hash Cond: (t1.id = t2.t1_id)
-                     ->  Seq Scan on public.eager_distinct_t1 t1
-                           Output: t1.id, t1.val
-                     ->  Hash
+               Hash Cond: (t1.id = t2.t1_id)
+               ->  Seq Scan on public.eager_distinct_t1 t1
+                     Output: t1.id, t1.val
+               ->  Hash
+                     Output: t2.t1_id
+                     ->  HashAggregate
                            Output: t2.t1_id
-                           ->  HashAggregate
-                                 Output: t2.t1_id
-                                 Group Key: t2.t1_id
-                                 ->  Seq Scan on public.eager_distinct_t2 t2
-                                       Output: t2.id, t2.t1_id, t2.flag
-(21 rows)
+                           Group Key: t2.t1_id
+                           ->  Seq Scan on public.eager_distinct_t2 t2
+                                 Output: t2.id, t2.t1_id, t2.flag
+(18 rows)
 
 SELECT t1.id
   FROM eager_distinct_t1 t1
@@ -2917,31 +2914,29 @@ HAVING count(*) > 5;
                      ->  Seq Scan on eager_distinct_a1 a1
 (10 rows)
 
--- A DISTINCT aggregate is idempotent, so the query's own grouping clause
--- drives the deduplication
+-- The deduplication groups on the keys the query groups by, so the query's
+-- own aggregate takes its input directly
 EXPLAIN (COSTS OFF)
 SELECT a1.id, count(DISTINCT a1.title)
   FROM eager_distinct_a1 a1
   JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id
  WHERE a2.flag
  GROUP BY a1.id;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                        QUERY PLAN                        
+----------------------------------------------------------
  GroupAggregate
    Group Key: a1.id
    ->  Sort
          Sort Key: a1.id, a1.title
-         ->  HashAggregate
-               Group Key: a1.id
-               ->  Hash Join
-                     Hash Cond: (a2.a1_id = a1.id)
-                     ->  HashAggregate
-                           Group Key: a2.a1_id
-                           ->  Seq Scan on eager_distinct_a2 a2
-                                 Filter: flag
-                     ->  Hash
-                           ->  Seq Scan on eager_distinct_a1 a1
-(14 rows)
+         ->  Hash Join
+               Hash Cond: (a2.a1_id = a1.id)
+               ->  HashAggregate
+                     Group Key: a2.a1_id
+                     ->  Seq Scan on eager_distinct_a2 a2
+                           Filter: flag
+               ->  Hash
+                     ->  Seq Scan on eager_distinct_a1 a1
+(12 rows)
 
 -- The deduplication threshold governs the derived keys too
 SET min_eager_distinct_group_size TO 8;
diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql
index fafa60edc78..604fe61f63b 100644
--- a/src/test/regress/sql/eager_aggregate.sql
+++ b/src/test/regress/sql/eager_aggregate.sql
@@ -908,8 +908,8 @@ SELECT count(DISTINCT a1.id)
  WHERE a2.flag
 HAVING count(*) > 5;
 
--- A DISTINCT aggregate is idempotent, so the query's own grouping clause
--- drives the deduplication
+-- The deduplication groups on the keys the query groups by, so the query's
+-- own aggregate takes its input directly
 EXPLAIN (COSTS OFF)
 SELECT a1.id, count(DISTINCT a1.title)
   FROM eager_distinct_a1 a1
