From 19f0d2b3224ca82835f5fc234e53e58f4ed0b7f4 Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 08:05:01 -0700
Subject: [PATCH v1 03/16] Give eager deduplication its own usefulness
 threshold

---
 doc/src/sgml/config.sgml                      | 23 ++++++++++++++-
 src/backend/optimizer/path/allpaths.c         |  1 +
 src/backend/optimizer/util/relnode.c          | 29 +++++++++++++++----
 src/backend/utils/misc/guc_parameters.dat     |  9 ++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/nodes/pathnodes.h                 |  3 +-
 src/include/optimizer/paths.h                 |  1 +
 7 files changed, 60 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0165eb9ec02..cbac2942abb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6553,7 +6553,28 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
         Sets the minimum average group size required to consider applying
         eager aggregation. This helps avoid the overhead of eager
         aggregation when it does not offer significant row count reduction.
-        The default is <literal>8</literal>.
+        This parameter applies only when the query has aggregate functions;
+        see <xref linkend="guc-min-eager-distinct-group-size"/> for the case
+        where it does not. The default is <literal>8</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-min-eager-distinct-group-size" xreflabel="min_eager_distinct_group_size">
+      <term><varname>min_eager_distinct_group_size</varname> (<type>floating point</type>)
+      <indexterm>
+       <primary><varname>min_eager_distinct_group_size</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the minimum average group size required to consider pushing a
+        deduplication below a join, which is what eager aggregation does for a
+        query that discards duplicate rows without computing any aggregates.
+        The threshold is lower than
+        <xref linkend="guc-min-eager-agg-group-size"/> because a
+        deduplication does not carry transition states. The default is
+        <literal>2</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 8cc702cface..a841a5ec45a 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -82,6 +82,7 @@ bool		enable_geqo = false;	/* just in case GUC doesn't set it */
 bool		enable_eager_aggregate = true;
 int			geqo_threshold;
 double		min_eager_agg_group_size;
+double		min_eager_distinct_group_size;
 int			min_parallel_table_scan_size;
 int			min_parallel_index_scan_size;
 
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 2d7b8f85fdf..6df6f2fc4d7 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -103,6 +103,7 @@ static bool init_grouping_targets(PlannerInfo *root, RelOptInfo *rel,
 static bool is_var_in_aggref_only(PlannerInfo *root, Var *var);
 static bool is_var_needed_by_join(PlannerInfo *root, Var *var, RelOptInfo *rel);
 static Index get_expression_sortgroupref(PlannerInfo *root, Expr *expr);
+static double min_useful_group_size(PlannerInfo *root);
 
 
 /*
@@ -2701,6 +2702,24 @@ build_child_join_reltarget(PlannerInfo *root,
 	childrel->reltarget->width = parentrel->reltarget->width;
 }
 
+/*
+ * min_useful_group_size
+ *	  The average group size at which pushing down starts to be worthwhile.
+ *
+ * A deduplication is judged separately from a partial aggregate.  Both shrink
+ * the joins above them, but a deduplication only builds the groups, while a
+ * partial aggregate also calls a transition function for every row and holds
+ * state for every group.
+ */
+static double
+min_useful_group_size(PlannerInfo *root)
+{
+	if (root->agg_clause_list == NIL)
+		return min_eager_distinct_group_size;
+
+	return min_eager_agg_group_size;
+}
+
 /*
  * create_rel_agg_info
  *	  Create the RelAggInfo structure for the given relation if it can produce
@@ -2758,11 +2777,11 @@ create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel,
 
 			/*
 			 * The grouped paths for the given relation are considered useful
-			 * iff the average group size is no less than
-			 * min_eager_agg_group_size.
+			 * iff the average group size is no less than the applicable
+			 * minimum.
 			 */
 			agg_info->agg_useful =
-				(rel->rows / agg_info->grouped_rows) >= min_eager_agg_group_size;
+				(rel->rows / agg_info->grouped_rows) >= min_useful_group_size(root);
 		}
 
 		return agg_info;
@@ -2824,10 +2843,10 @@ create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel,
 
 		/*
 		 * The grouped paths for the given relation are considered useful iff
-		 * the average group size is no less than min_eager_agg_group_size.
+		 * the average group size is no less than the applicable minimum.
 		 */
 		result->agg_useful =
-			(rel->rows / result->grouped_rows) >= min_eager_agg_group_size;
+			(rel->rows / result->grouped_rows) >= min_useful_group_size(root);
 	}
 
 	return result;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3c5e16ad1e7..41b1a049a11 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2241,6 +2241,15 @@
   max => 'DBL_MAX',
 },
 
+{ name => 'min_eager_distinct_group_size', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST',
+  short_desc => 'Sets the minimum average group size required to consider applying eager deduplication.',
+  flags => 'GUC_EXPLAIN',
+  variable => 'min_eager_distinct_group_size',
+  boot_val => '2.0',
+  min => '0.0',
+  max => 'DBL_MAX',
+},
+
 { name => 'min_parallel_index_scan_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST',
   short_desc => 'Sets the minimum amount of index data for a parallel scan.',
   long_desc => 'If the planner estimates that it will read a number of index pages too small to reach this limit, a parallel scan will not be considered.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e759f06b50f..ece05204022 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -466,6 +466,7 @@
 #min_parallel_index_scan_size = 512kB
 #effective_cache_size = 4GB
 #min_eager_agg_group_size = 8.0
+#min_eager_distinct_group_size = 2.0
 
 #jit_above_cost = 100000                # perform JIT compilation if available
                                         # and query more expensive than this;
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b0b210bd07e..158d689c377 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1282,7 +1282,8 @@ typedef struct RelOptInfo
  *
  * "agg_useful" is a flag to indicate whether the grouped paths are considered
  * useful.  It is set true if the average partial group size is no less than
- * min_eager_agg_group_size, suggesting a significant row count reduction.
+ * min_eager_agg_group_size (or min_eager_distinct_group_size when there are no
+ * aggregates), suggesting a significant row count reduction.
  */
 typedef struct RelAggInfo
 {
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 17f2099ec3b..49e21fcb56c 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -24,6 +24,7 @@ extern PGDLLIMPORT bool enable_geqo;
 extern PGDLLIMPORT bool enable_eager_aggregate;
 extern PGDLLIMPORT int geqo_threshold;
 extern PGDLLIMPORT double min_eager_agg_group_size;
+extern PGDLLIMPORT double min_eager_distinct_group_size;
 extern PGDLLIMPORT int min_parallel_table_scan_size;
 extern PGDLLIMPORT int min_parallel_index_scan_size;
 extern PGDLLIMPORT bool enable_group_by_reordering;
