From a124140b82ad54cdbfae9ba591040495d479b41b Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 13:44:07 -0700
Subject: [PATCH v1 11/16] Compute aggregates above a pushed-down deduplication

If the grouping keys come from the aggregates, eager aggregation pushes down a deduplication, and the aggregates stay above it.
- create_ordinary_grouping_paths() aggregates the deduplicated rows as another path.
- create_partial_grouping_paths() ignores them

Also split copy_rel_without_paths() out of build_grouped_rel()
---
 src/backend/optimizer/path/allpaths.c |  5 ++--
 src/backend/optimizer/plan/planmain.c |  1 +
 src/backend/optimizer/plan/planner.c  | 38 +++++++++++++++++++++++++--
 src/backend/optimizer/util/relnode.c  | 38 ++++++++++++++++++---------
 src/include/nodes/pathnodes.h         |  3 +++
 src/include/optimizer/pathnode.h      |  1 +
 6 files changed, 70 insertions(+), 16 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index a841a5ec45a..db3b69f60fa 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -3558,9 +3558,10 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 
 	/*
 	 * Determine whether we should consider hash-based implementations of
-	 * grouping.
+	 * grouping.  An ordered aggregate cannot be hashed.  Under eager
+	 * deduplication the aggregates stay above the join.
 	 */
-	Assert(root->numOrderedAggs == 0);
+	Assert(root->numOrderedAggs == 0 || root->eager_dedup_only);
 	can_hash = (agg_info->group_clauses != NIL &&
 				grouping_is_hashable(agg_info->group_clauses));
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 7ebe8d966ed..ec30ee584b0 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -79,6 +79,7 @@ query_planner(PlannerInfo *root,
 	root->agg_clause_list = NIL;
 	root->group_expr_list = NIL;
 	root->eager_group_clause = NIL;
+	root->eager_dedup_only = false;
 	root->filter_only_rels = NULL;
 	root->tlist_vars = NIL;
 	root->fkey_list = NIL;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 750b430f495..69f769da050 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -4427,6 +4427,37 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							  partially_grouped_rel, agg_costs, gd,
 							  extra);
 
+	/*
+	 * Where the grouping keys come from the aggregates, eager aggregation may
+	 * push a deduplication below the joins.  Every aggregate here ignores how
+	 * often a row arrives, so the deduplicated rows yield the same results as
+	 * the join's rows.  Aggregate them as a path.
+	 */
+	if (root->eager_dedup_only &&
+		input_rel->grouped_rel != NULL &&
+		!IS_DUMMY_REL(input_rel->grouped_rel) &&
+		input_rel->grouped_rel->pathlist != NIL)
+	{
+		RelOptInfo *dedup_rel;
+		ListCell   *lc;
+
+		dedup_rel = copy_rel_without_paths(input_rel);
+
+		foreach(lc, input_rel->grouped_rel->pathlist)
+		{
+			Path	   *path = (Path *) lfirst(lc);
+
+			add_path(dedup_rel,
+					 (Path *) create_projection_path(root, dedup_rel,
+													 complete_dedup_path(path),
+													 input_rel->reltarget));
+		}
+
+		set_cheapest(dedup_rel);
+		add_paths_to_grouping_rel(root, dedup_rel, grouped_rel, NULL,
+								  agg_costs, gd, extra);
+	}
+
 	/* Give a helpful error if we failed to find any implementation */
 	if (grouped_rel->pathlist == NIL)
 		ereport(ERROR,
@@ -7675,9 +7706,12 @@ create_partial_grouping_paths(PlannerInfo *root,
 
 	/*
 	 * Check whether any partially aggregated paths have been generated
-	 * through eager aggregation.
+	 * through eager aggregation.  Where the grouping keys come from the
+	 * aggregates, the pushdown is a deduplication rather than a partial
+	 * aggregate, and create_ordinary_grouping_paths() aggregates it.
 	 */
-	if (input_rel->grouped_rel &&
+	if (!root->eager_dedup_only &&
+		input_rel->grouped_rel &&
 		!IS_DUMMY_REL(input_rel->grouped_rel) &&
 		input_rel->grouped_rel->pathlist != NIL)
 		eager_agg_rel = input_rel->grouped_rel;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 6df6f2fc4d7..44de2cebb73 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -500,18 +500,7 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel)
 {
 	RelOptInfo *grouped_rel;
 
-	grouped_rel = makeNode(RelOptInfo);
-	memcpy(grouped_rel, rel, sizeof(RelOptInfo));
-
-	/*
-	 * clear path info
-	 */
-	grouped_rel->pathlist = NIL;
-	grouped_rel->ppilist = NIL;
-	grouped_rel->partial_pathlist = NIL;
-	grouped_rel->cheapest_startup_path = NULL;
-	grouped_rel->cheapest_total_path = NULL;
-	grouped_rel->cheapest_parameterized_paths = NIL;
+	grouped_rel = copy_rel_without_paths(rel);
 
 	/*
 	 * clear partition info
@@ -536,6 +525,31 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel)
 	return grouped_rel;
 }
 
+/*
+ * copy_rel_without_paths
+ *	  Flat copy a relation, leaving the copy's path lists empty.
+ *
+ * The copy shares everything else with the original, so a caller wanting
+ * different size estimates or partitioning must reset those itself.
+ */
+RelOptInfo *
+copy_rel_without_paths(RelOptInfo *rel)
+{
+	RelOptInfo *newrel;
+
+	newrel = makeNode(RelOptInfo);
+	memcpy(newrel, rel, sizeof(RelOptInfo));
+
+	newrel->pathlist = NIL;
+	newrel->ppilist = NIL;
+	newrel->partial_pathlist = NIL;
+	newrel->cheapest_startup_path = NULL;
+	newrel->cheapest_total_path = NULL;
+	newrel->cheapest_parameterized_paths = NIL;
+
+	return newrel;
+}
+
 /*
  * find_base_rel
  *	  Find a base or otherrel relation entry, which must already exist.
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e899c3bec37..6e8b8b4c300 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -504,6 +504,9 @@ struct PlannerInfo
 	/* the SortGroupClauses the grouping expressions were derived from */
 	List	   *eager_group_clause;
 
+	/* true if a deduplication is pushed down, with the aggregates above it */
+	bool		eager_dedup_only;
+
 	/* base rels supplying nothing the query outputs, or NULL if none */
 	Relids		filter_only_rels;
 
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index e8db321f92b..1f5a911c388 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -346,6 +346,7 @@ extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 									RelOptInfo *parent);
 extern RelOptInfo *build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel);
 extern RelOptInfo *build_grouped_rel(PlannerInfo *root, RelOptInfo *rel);
+extern RelOptInfo *copy_rel_without_paths(RelOptInfo *rel);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
 extern RelOptInfo *find_base_rel_noerr(PlannerInfo *root, int relid);
 extern RelOptInfo *find_base_rel_ignore_join(PlannerInfo *root, int relid);
