Hi hackers,

In [0], a question was raised whether we should expose IncrementalSort group estimation in EXPLAIN, as we did for Memoize. Knowing the estimated number of groups helps understand why the planner chose IncrementalSort whether a bad estimate is to blame for a sub-optimal plan.

Example of EXPLAIN:

```
CREATE TABLE t (a int, b int, c int);
CREATE INDEX ON t (a);
INSERT INTO t SELECT i % 100, (random() * 1000)::int, (random() * 1000)::int FROM generate_series(1, 100000) i;
ANALYZE t;
SET enable_seqscan = off;

EXPLAIN ANALYZE SELECT * FROM t ORDER BY a, b;
                                                            QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
 Incremental Sort  (cost=90.80..10302.90 rows=100000 width=12) (actual time=6.715..29.592 rows=100000.00 loops=1)
   Sort Key: a, b
   Presorted Key: a
*Estimated Groups: 100*
   Full-sort Groups: 100  Sort Method: quicksort  Average Memory: 27kB  Peak Memory: 27kB    Pre-sorted Groups: 100  Sort Method: quicksort  Average Memory: 56kB  Peak Memory: 56kB
   Buffers: shared hit=54201 dirtied=1 written=1
   ->  Index Scan using t_a_idx on t  (cost=0.29..4068.01 rows=100000 width=12) (actual time=0.346..20.403 rows=100000.00 loops=1)
         Index Searches: 1
         Buffers: shared hit=54201 dirtied=1 written=1
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.411 ms
 Execution Time: 30.530 ms
(14 rows)
```

Thoughts?


[0]: https://www.postgresql.org/message-id/6642af90-561c-4f0c-9d5b-7e288e6e7f84%40gmail.com

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC,
https://tantorlabs.com/
From d3a5b3116653582a2f90c750d54869709c012585 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <[email protected]>
Date: Wed, 10 Jun 2026 18:35:16 +0300
Subject: [PATCH v1] 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.
---
 src/backend/commands/explain.c          |  5 +++++
 src/backend/optimizer/path/costsize.c   | 12 +++++++++---
 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 ++-
 7 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..11d2b06bcaa 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->est_input_groups, 0, es);
+
 	if (!es->analyze)
 		return;
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1c575e56ff6..080fab3686b 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2055,7 +2055,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,
+					  double *p_input_groups)
 {
 	Cost		startup_cost,
 				run_cost,
@@ -2171,6 +2172,9 @@ cost_incremental_sort(Path *path,
 	 */
 	run_cost += 2.0 * cpu_tuple_cost * input_groups;
 
+	if (p_input_groups)
+		*p_input_groups = input_groups;
+
 	path->rows = input_tuples;
 
 	/*
@@ -2406,7 +2410,8 @@ cost_append(AppendPath *apath, PlannerInfo *root)
 											  subpath->pathtarget->width,
 											  0.0,
 											  work_mem,
-											  apath->limit_tuples);
+											  apath->limit_tuples,
+											  NULL);
 					}
 					else
 					{
@@ -3827,7 +3832,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 de6a183da79..5c5791b4f2f 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2067,6 +2067,7 @@ create_incrementalsort_plan(PlannerInfo *root, IncrementalSortPath *best_path,
 											  best_path->nPresortedCols);
 
 	copy_generic_path_info(&plan->sort.plan, (Path *) best_path);
+	plan->est_input_groups = best_path->est_input_groups;
 
 	return plan;
 }
@@ -5441,7 +5442,8 @@ label_incrementalsort_with_costsize(PlannerInfo *root, IncrementalSort *plan,
 						  lefttree->plan_width,
 						  0.0,
 						  work_mem,
-						  limit_tuples);
+						  limit_tuples,
+						  &plan->est_input_groups);
 	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 73518c8f870..665e8859a28 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->est_input_groups);
 
 	sort->nPresortedCols = presorted_keys;
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c6815b7..abfc3c9aad6 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 */
+	double		est_input_groups;	/* estimated number of groups */
 } IncrementalSortPath;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 14a1dfed2b9..49e66965fe6 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1169,6 +1169,8 @@ typedef struct IncrementalSort
 	Sort		sort;
 	/* number of presorted columns */
 	int			nPresortedCols;
+	/* estimated number of groups (groups with equal presorted keys) */
+	double		est_input_groups;
 } IncrementalSort;
 
 /* ---------------
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index f2fd5d31507..8fa0b0ecad7 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -117,7 +117,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,
+								  double *p_input_groups);
 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

Reply via email to