From 11daacd64aea2a972243c878cf955a7d5b59b507 Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 18:14:25 -0700
Subject: [PATCH v1 14/16] 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          | 48 ++++++++++++++++++-
 src/test/regress/expected/eager_aggregate.out | 28 +++++------
 src/test/regress/sql/eager_aggregate.sql      |  4 +-
 3 files changed, 62 insertions(+), 18 deletions(-)

diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 69f769da050..0787d308bb9 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -4056,6 +4056,38 @@ complete_dedup_path(Path *path)
 	return (Path *) aggpath;
 }
 
+/*
+ * 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
  *
@@ -4447,9 +4479,23 @@ 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;
+			}
+			else
+				path = complete_dedup_path(path);
+
 			add_path(dedup_rel,
 					 (Path *) create_projection_path(root, dedup_rel,
-													 complete_dedup_path(path),
+													 path,
 													 input_rel->reltarget));
 		}
 
diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out
index 64929d07d68..5dd43d39f3a 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -2917,31 +2917,29 @@ HAVING count(*) > 5;
                      ->  Seq Scan on eager_distinct_a1 a1
 (10 rows)
 
--- A DISTINCT aggregate beside a grouping clause of the query's own is no
--- obstacle, though at this size the plain join still costs less
+-- A DISTINCT aggregate beside a grouping clause of the query's own adds its
+-- argument as a further key, and the deduplication goes down as usual
 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)
-                     ->  Partial 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)
+               ->  Partial 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 cd37db8e79a..f929a2a4db0 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 beside a grouping clause of the query's own is no
--- obstacle, though at this size the plain join still costs less
+-- A DISTINCT aggregate beside a grouping clause of the query's own adds its
+-- argument as a further key, and the deduplication goes down as usual
 EXPLAIN (COSTS OFF)
 SELECT a1.id, count(DISTINCT a1.title)
   FROM eager_distinct_a1 a1
