From e46eb01ac1834b82dd765c0106f2e7db8081fa8d Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Wed, 29 Jul 2026 13:04:32 +0300
Subject: [PATCH v3] Show estimated number of groups for IncrementalSort in
 EXPLAIN

IncrementalSort cost heavily depends on the estimated number of groups
with equal presorted key values. A bad estimate can cause the planner
to choose IncrementalSort over Sort even when it would actually be
slower, with no visible indication of why.
---
 doc/src/sgml/perform.sgml               |  1 +
 src/backend/commands/explain.c          |  5 +++++
 src/backend/optimizer/path/costsize.c   | 15 ++++++++++++---
 src/backend/optimizer/plan/createplan.c |  4 +++-
 src/backend/optimizer/util/pathnode.c   |  6 ++++--
 src/include/nodes/pathnodes.h           |  1 +
 src/include/nodes/plannodes.h           |  2 ++
 src/include/optimizer/cost.h            |  3 ++-
 8 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 604e8578a8d..ea8da01b779 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -325,6 +325,7 @@ EXPLAIN SELECT * FROM tenk1 ORDER BY hundred, ten LIMIT 100;
    -&gt;  Incremental Sort  (cost=19.35..2033.39 rows=10000 width=244)
          Sort Key: hundred, ten
          Presorted Key: hundred
+         Estimated Groups: 100
          -&gt;  Index Scan using tenk1_hundred on tenk1  (cost=0.29..1574.20 rows=10000 width=244)
 </screen>
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a40d03d35f3..f223c12f294 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3306,11 +3306,16 @@ static void
 show_incremental_sort_info(IncrementalSortState *incrsortstate,
 						   ExplainState *es)
 {
+	IncrementalSort *plan = (IncrementalSort *) incrsortstate->ss.ps.plan;
 	IncrementalSortGroupInfo *fullsortGroupInfo;
 	IncrementalSortGroupInfo *prefixsortGroupInfo;
 
 	fullsortGroupInfo = &incrsortstate->incsort_info.fullsortGroupInfo;
 
+	if (es->costs)
+		ExplainPropertyFloat("Estimated Groups", NULL,
+							 plan->numGroups, 0, es);
+
 	if (!es->analyze)
 		return;
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index ac523ecf9a8..f01b2c93c9d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2046,6 +2046,9 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
  * 'presorted_keys' is the number of leading pathkeys by which the input path
  * is sorted.
  *
+ * If p_numGroups is not NULL, *p_numGroups is set to the estimated number of
+ * sort groups (groups of tuples sharing equal presorted-key values).
+ *
  * We estimate the number of groups into which the relation is divided by the
  * leading pathkeys, and then calculate the cost of sorting a single group
  * with tuplesort using cost_tuplesort().
@@ -2056,7 +2059,8 @@ cost_incremental_sort(Path *path,
 					  int input_disabled_nodes,
 					  Cost input_startup_cost, Cost input_total_cost,
 					  double input_tuples, int width, Cost comparison_cost, int sort_mem,
-					  double limit_tuples)
+					  double limit_tuples,
+					  Cardinality *p_numGroups)
 {
 	Cost		startup_cost,
 				run_cost,
@@ -2172,6 +2176,9 @@ cost_incremental_sort(Path *path,
 	 */
 	run_cost += 2.0 * cpu_tuple_cost * input_groups;
 
+	if (p_numGroups)
+		*p_numGroups = input_groups;
+
 	path->rows = input_tuples;
 
 	/*
@@ -2407,7 +2414,8 @@ cost_append(AppendPath *apath, PlannerInfo *root)
 											  subpath->pathtarget->width,
 											  0.0,
 											  work_mem,
-											  apath->limit_tuples);
+											  apath->limit_tuples,
+											  NULL);
 					}
 					else
 					{
@@ -3850,7 +3858,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 								  outer_path->pathtarget->width,
 								  0.0,
 								  work_mem,
-								  -1.0);
+								  -1.0,
+								  NULL);
 		}
 		else
 		{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 2c696ea0268..3029efe9872 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2073,6 +2073,7 @@ create_incrementalsort_plan(PlannerInfo *root, IncrementalSortPath *best_path,
 											  best_path->nPresortedCols);
 
 	copy_generic_path_info(&plan->sort.plan, (Path *) best_path);
+	plan->numGroups = best_path->numGroups;
 
 	return plan;
 }
@@ -5468,7 +5469,8 @@ label_incrementalsort_with_costsize(PlannerInfo *root, IncrementalSort *plan,
 						  lefttree->plan_width,
 						  0.0,
 						  work_mem,
-						  limit_tuples);
+						  limit_tuples,
+						  &plan->numGroups);
 	plan->sort.plan.startup_cost = sort_path.startup_cost;
 	plan->sort.plan.total_cost = sort_path.total_cost;
 	plan->sort.plan.plan_rows = lefttree->plan_rows;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 3875e3dd571..a8dcad72958 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1605,7 +1605,8 @@ create_merge_append_path(PlannerInfo *root,
 									  subpath->pathtarget->width,
 									  0.0,
 									  work_mem,
-									  pathnode->limit_tuples);
+									  pathnode->limit_tuples,
+									  NULL);
 			}
 			else
 			{
@@ -2883,7 +2884,8 @@ create_incremental_sort_path(PlannerInfo *root,
 						  subpath->rows,
 						  subpath->pathtarget->width,
 						  0.0,	/* XXX comparison_cost shouldn't be 0? */
-						  work_mem, limit_tuples);
+						  work_mem, limit_tuples,
+						  &sort->numGroups);
 
 	sort->nPresortedCols = presorted_keys;
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c6815b7..185e2fb8761 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2548,6 +2548,7 @@ typedef struct IncrementalSortPath
 {
 	SortPath	spath;
 	int			nPresortedCols; /* number of presorted columns */
+	Cardinality numGroups;		/* estimated number of groups */
 } IncrementalSortPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b880ce0f4be..781651693eb 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1171,6 +1171,8 @@ typedef struct IncrementalSort
 	Sort		sort;
 	/* number of presorted columns */
 	int			nPresortedCols;
+	/* estimated number of groups (groups with equal presorted keys) */
+	Cardinality numGroups;
 } IncrementalSort;
 
 /* ---------------
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index bda3f1690c0..9d571e513c6 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -118,7 +118,8 @@ extern void cost_incremental_sort(Path *path,
 								  int input_disabled_nodes,
 								  Cost input_startup_cost, Cost input_total_cost,
 								  double input_tuples, int width, Cost comparison_cost, int sort_mem,
-								  double limit_tuples);
+								  double limit_tuples,
+								  Cardinality *p_numGroups);
 extern void cost_append(AppendPath *apath, PlannerInfo *root);
 extern void cost_merge_append(Path *path, PlannerInfo *root,
 							  List *pathkeys, int n_streams,
-- 
2.34.1

