From 1401fa0b2090fa32f762041a80a1f2f345fcbba1 Mon Sep 17 00:00:00 2001
From: kenxx <zengxx@vastdata.com.cn>
Date: Wed, 16 Sep 2026 22:22:19 +0800
Subject: [PATCH v1 2/2] Skip grouping when every input row is its own group

---
 doc/src/sgml/ref/select.sgml                  |   5 +
 src/backend/optimizer/path/indxpath.c         |  63 +-
 src/backend/optimizer/plan/planner.c          | 156 ++++-
 src/include/nodes/pathnodes.h                 |   2 +-
 src/include/optimizer/paths.h                 |   3 +
 src/test/regress/expected/aggregates.out      |  74 +--
 .../regress/expected/collate.icu.utf8.out     |  20 +
 src/test/regress/expected/join.out            |  10 +-
 .../regress/expected/singleton_grouping.out   | 628 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +
 src/test/regress/sql/collate.icu.utf8.sql     |  14 +
 src/test/regress/sql/singleton_grouping.sql   | 311 +++++++++
 12 files changed, 1230 insertions(+), 58 deletions(-)
 create mode 100644 src/test/regress/expected/singleton_grouping.out
 create mode 100644 src/test/regress/sql/singleton_grouping.sql

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 18392f7cae..82a1f8aae1 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -2146,6 +2146,11 @@ SELECT 2+2;
     The SQL standard specifies additional conditions that should be
     recognized.
    </para>
+
+   <para>
+    The planner can also avoid a separate grouping step in restricted cases
+    where it can prove that every input row is its own group.
+   </para>
   </refsect2>
 
   <refsect2>
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index a4cd25d15f..82b78b057c 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -74,7 +74,6 @@ typedef struct
 	int			indexcol;		/* index column we want to match to */
 } ec_member_matches_arg;
 
-
 static void consider_index_join_clauses(PlannerInfo *root, RelOptInfo *rel,
 										IndexOptInfo *index,
 										IndexClauseSet *rclauseset,
@@ -4350,6 +4349,68 @@ unique_index_keys_match_groupby_cols(IndexOptInfo *index, RelOptInfo *rel,
 	return true;
 }
 
+/*
+ * relation_has_unique_index_covered_by_group_keys
+ *		Determine whether every input row is its own group under the given
+ *		plain GROUP BY keys.
+ *
+ * The caller has already restricted this to a single base relation and a plain
+ * GROUP BY list.  Only simple Vars from that relation can cover index keys;
+ * other grouping items are additional keys and cannot invalidate the proof.
+ * Index keys may be a subset of the grouping keys, just as an immediate PK
+ * makes every row its own group regardless of what else is listed in GROUP BY.
+ */
+bool
+relation_has_unique_index_covered_by_group_keys(RelOptInfo *rel,
+												List *groupClause,
+												List *targetList)
+{
+	List       *group_keys = NIL;
+	ListCell   *lc;
+
+	if (groupClause == NIL)
+		return false;
+
+	Assert(bms_membership(rel->relids) == BMS_SINGLETON);
+
+	foreach(lc, groupClause)
+	{
+		SortGroupClause *sgc = lfirst_node(SortGroupClause, lc);
+		TargetEntry *tle = get_sortgroupclause_tle(sgc, targetList);
+		Var            *var;
+		GroupByColInfo *key;
+
+		if (tle == NULL)
+			return false;
+
+		/* Extra grouping expressions do not affect the singleton proof. */
+		if (!IsA(tle->expr, Var))
+			continue;
+
+		var = (Var *) tle->expr;
+		if (var->varlevelsup != 0 || var->varattno <= 0 ||
+			var->varno != rel->relid)
+			continue;
+
+		key = palloc_object(GroupByColInfo);
+		key->attno = var->varattno;
+		key->eq_opfamilies = get_mergejoin_opfamilies(sgc->eqop);
+		key->coll = var->varcollid;
+		group_keys = lappend(group_keys, key);
+	}
+
+	if (group_keys == NIL)
+		return false;
+
+	foreach_node(IndexOptInfo, index, rel->indexlist)
+	{
+		if (unique_index_keys_match_groupby_cols(index, rel, group_keys, NULL))
+			return true;
+	}
+
+	return false;
+}
+
 /*
  * indexcol_is_bool_constant_for_query
  *
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8d30131855..8cbd295e02 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -177,11 +177,19 @@ static double get_number_of_groups(PlannerInfo *root,
 								   double path_rows,
 								   grouping_sets_data *gd,
 								   List *target_list);
+static bool grouping_input_is_singleton(PlannerInfo *root,
+										RelOptInfo *input_rel,
+										List *targetList,
+										bool setop_child);
+static void create_singleton_grouping_paths(PlannerInfo *root,
+											RelOptInfo *input_rel,
+											RelOptInfo *grouped_rel);
 static RelOptInfo *create_grouping_paths(PlannerInfo *root,
 										 RelOptInfo *input_rel,
 										 PathTarget *target,
 										 bool target_parallel_safe,
-										 grouping_sets_data *gd);
+										 grouping_sets_data *gd,
+										 SetOperationStmt *setops);
 static bool is_degenerate_grouping(PlannerInfo *root);
 static void create_degenerate_grouping_paths(PlannerInfo *root,
 											 RelOptInfo *input_rel,
@@ -196,6 +204,10 @@ static void create_ordinary_grouping_paths(PlannerInfo *root,
 										   grouping_sets_data *gd,
 										   GroupPathExtraData *extra,
 										   RelOptInfo **partially_grouped_rel_p);
+static void add_foreign_and_custom_grouping_paths(PlannerInfo *root,
+												  RelOptInfo *input_rel,
+												  RelOptInfo *grouped_rel,
+												  GroupPathExtraData *extra);
 static void consider_groupingsets_paths(PlannerInfo *root,
 										RelOptInfo *grouped_rel,
 										Path *path,
@@ -2090,7 +2102,8 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 												current_rel,
 												grouping_target,
 												grouping_target_parallel_safe,
-												gset_data);
+												gset_data,
+												setops);
 			/* Fix things up if grouping_target contains SRFs */
 			if (parse->hasTargetSRFs)
 				adjust_paths_for_srfs(root, current_rel,
@@ -4048,6 +4061,97 @@ get_number_of_groups(PlannerInfo *root,
 	return dNumGroups;
 }
 
+/*
+ * grouping_input_is_singleton
+ *		Can this plain GROUP BY be implemented without a grouping node?
+ *
+ * This is intentionally narrower than the general uniqueness machinery.  The
+ * current proof accepts one ordinary base relation (or partitioned parent) and
+ * requires its immediate unique index keys to appear as simple Var grouping
+ * keys.  Additional grouping expressions are allowed and left to normal
+ * projection.  In particular, set operation children are rejected because
+ * their output target conventions are planned separately.
+ */
+static bool
+grouping_input_is_singleton(PlannerInfo *root, RelOptInfo *input_rel,
+							List *targetList, bool setop_child)
+{
+	Query	   *parse = root->parse;
+	Index		rti;
+	RangeTblEntry *rte;
+
+	if (setop_child || parse->groupClause == NIL ||
+		parse->groupingSets != NIL || parse->hasAggs ||
+		parse->hasWindowFuncs || parse->hasTargetSRFs ||
+		parse->distinctClause != NIL || parse->hasDistinctOn ||
+		parse->rowMarks != NIL || parse->setOperations != NULL ||
+		root->hasHavingQual || root->numOrderedAggs > 0)
+		return false;
+
+	/* Deliberately avoid upper, join, and partitionwise-child relations. */
+	if (input_rel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	Assert(bms_membership(input_rel->relids) == BMS_SINGLETON);
+	rti = input_rel->relid;
+	rte = planner_rt_fetch(rti, root);
+	if (rte == NULL || rte->rtekind != RTE_RELATION || rte->lateral ||
+		!(rte->relkind == RELKIND_RELATION ||
+		  rte->relkind == RELKIND_PARTITIONED_TABLE))
+		return false;
+
+	/*
+	 * An old-style inheritance parent can contain the same key in more than
+	 * one child.  A partitioned table's unique constraint proves uniqueness
+	 * across all of its partitions.
+	 */
+	if (rte->inh && rte->relkind != RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	/*
+	 * Use the original clause rather than processed_groupClause: equivalence
+	 * processing may remove redundant keys, but every original key remains a
+	 * valid grouping key.  The original superset can only make the unique-key
+	 * proof stronger.
+	 */
+	return relation_has_unique_index_covered_by_group_keys(input_rel,
+														   parse->groupClause,
+														   targetList);
+}
+
+/*
+ * create_singleton_grouping_paths
+ *		Put input paths directly under the grouping upper relation.
+ *
+ * The input relation was built with make_group_input_target(), so projecting
+ * each path onto grouped_rel's target produces exactly the same rows as a
+ * grouping node when every input row is its own group.  ProjectionPath keeps
+ * the input pathkeys, which lets ORDER BY continue to use an index order.
+ */
+static void
+create_singleton_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
+								RelOptInfo *grouped_rel)
+{
+	ListCell   *lc;
+
+	foreach(lc, input_rel->pathlist)
+	{
+		Path	   *input_path = (Path *) lfirst(lc);
+
+		/*
+		 * Avoid stacking a new projection on the scan/join projection.  The
+		 * final grouped target can be computed directly from the underlying
+		 * path in the narrow case accepted by grouping_input_is_singleton().
+		 */
+		if (IsA(input_path, ProjectionPath))
+			input_path = ((ProjectionPath *) input_path)->subpath;
+
+		add_path(grouped_rel, (Path *)
+				 create_projection_path(root, grouped_rel, input_path,
+										grouped_rel->reltarget));
+	}
+}
+
 /*
  * create_grouping_paths
  *
@@ -4070,16 +4174,14 @@ create_grouping_paths(PlannerInfo *root,
 					  RelOptInfo *input_rel,
 					  PathTarget *target,
 					  bool target_parallel_safe,
-					  grouping_sets_data *gd)
+					  grouping_sets_data *gd,
+					  SetOperationStmt *setops)
 {
 	Query	   *parse = root->parse;
 	RelOptInfo *grouped_rel;
 	RelOptInfo *partially_grouped_rel;
 	AggClauseCosts agg_costs;
 
-	MemSet(&agg_costs, 0, sizeof(AggClauseCosts));
-	get_agg_clause_costs(root, AGGSPLIT_SIMPLE, &agg_costs);
-
 	/*
 	 * Create grouping relation to hold fully aggregated grouping and/or
 	 * aggregation paths.
@@ -4087,6 +4189,33 @@ create_grouping_paths(PlannerInfo *root,
 	grouped_rel = make_grouping_rel(root, input_rel, target,
 									target_parallel_safe, parse->havingQual);
 
+	if (grouping_input_is_singleton(root, input_rel, parse->targetList,
+									setops != NULL))
+	{
+		GroupPathExtraData extra;
+
+		create_singleton_grouping_paths(root, input_rel, grouped_rel);
+
+		/*
+		 * Singleton paths replace the standard grouping implementations, so
+		 * advertise no standard grouping methods.  Keep the FDW and extension
+		 * hooks in the same position as for ordinary grouping.
+		 */
+		MemSet(&extra, 0, sizeof(extra));
+		extra.target_parallel_safe = target_parallel_safe;
+		extra.havingQual = parse->havingQual;
+		extra.targetList = parse->targetList;
+		extra.patype = PARTITIONWISE_AGGREGATE_NONE;
+		add_foreign_and_custom_grouping_paths(root, input_rel, grouped_rel,
+											  &extra);
+
+		set_cheapest(grouped_rel);
+		return grouped_rel;
+	}
+
+	MemSet(&agg_costs, 0, sizeof(AggClauseCosts));
+	get_agg_clause_costs(root, AGGSPLIT_SIMPLE, &agg_costs);
+
 	/*
 	 * Create either paths for a degenerate grouping or paths for ordinary
 	 * grouping, as appropriate.
@@ -4426,6 +4555,21 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 				 errmsg("could not implement GROUP BY"),
 				 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));
 
+	add_foreign_and_custom_grouping_paths(root, input_rel, grouped_rel,
+										  extra);
+}
+
+/*
+ * add_foreign_and_custom_grouping_paths
+ *
+ * Give FDWs and extensions a chance to add or replace paths for the fully
+ * grouped relation.  Callers must set grouped_rel->pathlist before calling.
+ */
+static void
+add_foreign_and_custom_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
+									  RelOptInfo *grouped_rel,
+									  GroupPathExtraData *extra)
+{
 	/*
 	 * If there is an FDW that's responsible for all baserels of the query,
 	 * let it consider adding ForeignPaths.
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 1c6d1fe3d0..0904847f93 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1079,7 +1079,7 @@ typedef struct RelOptInfo
 	Relids	   *attr_needed pg_node_attr(read_write_ignore);
 	/* array indexed [min_attr .. max_attr] */
 	int32	   *attr_widths pg_node_attr(read_write_ignore);
-	/* zero-based set containing attnums of NOT NULL columns */
+	/* set of heap attnums for columns with valid NOT NULL constraints */
 	Bitmapset  *notnullattnums;
 	/* relids of outer joins that can null this baserel */
 	Relids		nulling_relids;
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 3285bd77af..3954d01a95 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -98,6 +98,9 @@ extern bool unique_index_keys_match_groupby_cols(IndexOptInfo *index,
 												 RelOptInfo *rel,
 												 List *groupbycols,
 												 Bitmapset **index_attnos);
+extern bool relation_has_unique_index_covered_by_group_keys(RelOptInfo *rel,
+															List *groupClause,
+															List *targetList);
 extern bool indexcol_is_bool_constant_for_query(PlannerInfo *root,
 												IndexOptInfo *index,
 												int indexcol);
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 7d07619956..01231311b4 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1548,12 +1548,10 @@ create temp table t2 (x int, y int, z int, primary key (x, y));
 create temp table t3 (a int, b int, c int, primary key(a, b) deferrable);
 -- Non-primary-key columns can be removed from GROUP BY
 explain (costs off) select * from t1 group by a,b,c,d;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: a, b
-   ->  Seq Scan on t1
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t1
+(1 row)
 
 -- No removal can happen if the complete PK is not present in GROUP BY
 explain (costs off) select a,c from t1 group by a,c,d;
@@ -1617,12 +1615,10 @@ explain (costs off) select * from t1 group by a,b,c,d;
 
 -- Okay to remove columns if we're only querying the parent.
 explain (costs off) select * from only t1 group by a,b,c,d;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: a, b
-   ->  Seq Scan on t1
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t1
+(1 row)
 
 create temp table p_t1 (
   a int,
@@ -1635,14 +1631,12 @@ create temp table p_t1_1 partition of p_t1 for values in(1);
 create temp table p_t1_2 partition of p_t1 for values in(2);
 -- Ensure we can remove non-PK columns for partitioned tables.
 explain (costs off) select * from p_t1 group by a,b,c,d;
-           QUERY PLAN           
---------------------------------
- HashAggregate
-   Group Key: p_t1.a, p_t1.b
-   ->  Append
-         ->  Seq Scan on p_t1_1
-         ->  Seq Scan on p_t1_2
-(5 rows)
+        QUERY PLAN        
+--------------------------
+ Append
+   ->  Seq Scan on p_t1_1
+   ->  Seq Scan on p_t1_2
+(3 rows)
 
 create unique index t2_z_uidx on t2(z);
 -- Ensure we don't remove any columns from the GROUP BY for a unique
@@ -1658,33 +1652,27 @@ explain (costs off) select y,z from t2 group by y,z;
 -- Make the column NOT NULL and ensure we remove the redundant column
 alter table t2 alter column z set not null;
 explain (costs off) select y,z from t2 group by y,z;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: z
-   ->  Seq Scan on t2
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t2
+(1 row)
 
 -- When there are multiple supporting unique indexes and the GROUP BY contains
 -- columns to cover all of those, ensure we pick the index with the least
 -- number of columns so that we can remove more columns from the GROUP BY.
 explain (costs off) select x,y,z from t2 group by x,y,z;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: z
-   ->  Seq Scan on t2
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t2
+(1 row)
 
 -- As above but try ordering the columns differently to ensure we get the
 -- same result.
 explain (costs off) select x,y,z from t2 group by z,x,y;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: z
-   ->  Seq Scan on t2
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t2
+(1 row)
 
 -- Ensure we don't use a partial index as proof of functional dependency
 drop index t2_z_uidx;
@@ -1704,12 +1692,10 @@ drop index t2_z_uidx;
 alter table t2 alter column z drop not null;
 create unique index t2_z_uidx on t2(z) nulls not distinct;
 explain (costs off) select y,z from t2 group by y,z;
-      QUERY PLAN      
-----------------------
- HashAggregate
-   Group Key: z
-   ->  Seq Scan on t2
-(3 rows)
+   QUERY PLAN   
+----------------
+ Seq Scan on t2
+(1 row)
 
 drop table t1 cascade;
 NOTICE:  drop cascades to table t1c
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index cb5795f036..7facc3d3fb 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -3847,6 +3847,26 @@ LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLAT...
                                                              ^
 DETAIL:  "C" versus "case_insensitive"
 DROP DOMAIN d1, d2;
+-- A unique index proves uniqueness only under its own collation.  The
+-- case-insensitive GROUP BY may merge rows that the deterministic "C" unique
+-- index treats as distinct, so singleton grouping elimination must reject the
+-- index as a proof.
+CREATE TABLE singleton_collation_mismatch (
+    val text COLLATE case_insensitive
+);
+CREATE UNIQUE INDEX singleton_collation_mismatch_val
+    ON singleton_collation_mismatch (val COLLATE "C");
+INSERT INTO singleton_collation_mismatch VALUES ('abc'), ('ABC');
+EXPLAIN (COSTS OFF)
+SELECT val FROM singleton_collation_mismatch GROUP BY val;
+                   QUERY PLAN                   
+------------------------------------------------
+ HashAggregate
+   Group Key: val
+   ->  Seq Scan on singleton_collation_mismatch
+(3 rows)
+
+DROP TABLE singleton_collation_mismatch;
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 75544fe6aa..6c1cc6f0d5 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -7553,17 +7553,15 @@ select d.* from d left join (select distinct * from b) s
 explain (costs off)
 select d.* from d left join (select * from b group by b.id, b.c_id) s
   on d.a = s.id;
-                QUERY PLAN                
-------------------------------------------
+             QUERY PLAN             
+------------------------------------
  Merge Right Join
    Merge Cond: (b.id = d.a)
-   ->  Group
-         Group Key: b.id
-         ->  Index Scan using b_pkey on b
+   ->  Index Scan using b_pkey on b
    ->  Sort
          Sort Key: d.a
          ->  Seq Scan on d
-(8 rows)
+(6 rows)
 
 -- join removal is not possible when the GROUP BY contains non-empty grouping
 -- sets or multiple empty grouping sets
diff --git a/src/test/regress/expected/singleton_grouping.out b/src/test/regress/expected/singleton_grouping.out
new file mode 100644
index 0000000000..a1c202388a
--- /dev/null
+++ b/src/test/regress/expected/singleton_grouping.out
@@ -0,0 +1,628 @@
+-- Test path-level elimination of grouping when each input row is its own group.
+CREATE TEMP TABLE singleton_base (
+    id int PRIMARY KEY,
+    status text NOT NULL,
+    payload text
+);
+INSERT INTO singleton_base
+SELECT g, 'status' || g, 'payload' || g FROM generate_series(1, 5) g;
+-- The unique key makes each row its own group.
+EXPLAIN (COSTS OFF)
+SELECT id, status, payload FROM singleton_base GROUP BY id, status, payload;
+         QUERY PLAN         
+----------------------------
+ Seq Scan on singleton_base
+(1 row)
+
+SELECT id, string_agg(status, ',' ORDER BY status)
+FROM singleton_base GROUP BY id, status, payload ORDER BY id;
+ id | string_agg 
+----+------------
+  1 | status1
+  2 | status2
+  3 | status3
+  4 | status4
+  5 | status5
+(5 rows)
+
+-- The input pathkeys survive projection.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id ORDER BY id;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Index Only Scan using singleton_base_pkey on singleton_base
+(1 row)
+
+-- A constant-covered grouping key may be reduced by equivalence processing;
+-- the original key still proves singleton grouping.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base WHERE id = 1 GROUP BY id;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Index Only Scan using singleton_base_pkey on singleton_base
+   Index Cond: (id = 1)
+(2 rows)
+
+-- Equivalence processing may remove only some original keys.  The untouched
+-- original clause still contains the unique key and remains a valid proof.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base WHERE id = 1 GROUP BY id, status;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Scan using singleton_base_pkey on singleton_base
+   Index Cond: (id = 1)
+(2 rows)
+
+-- A composite key is sufficient; remaining grouping keys may be redundant.
+CREATE TEMP TABLE singleton_composite (
+    a int, b int, c int, d text, PRIMARY KEY (a, b)
+);
+INSERT INTO singleton_composite
+VALUES (1, 1, 1, 'one'), (1, 2, 2, 'two'), (2, 1, 3, 'three');
+EXPLAIN (COSTS OFF)
+SELECT a, b, c, d FROM singleton_composite GROUP BY a, b, c, d;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on singleton_composite
+(1 row)
+
+-- Non-deferrable unique constraints and indexes, including NULLS NOT
+-- DISTINCT, also prove singleton grouping.
+CREATE TEMP TABLE singleton_unique (
+    id int NOT NULL, val text, UNIQUE (id)
+);
+INSERT INTO singleton_unique VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_unique GROUP BY id, val;
+          QUERY PLAN          
+------------------------------
+ Seq Scan on singleton_unique
+(1 row)
+
+CREATE TEMP TABLE singleton_plain_unique (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_plain_unique_id ON singleton_plain_unique (id);
+INSERT INTO singleton_plain_unique VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_plain_unique GROUP BY id, val;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on singleton_plain_unique
+(1 row)
+
+CREATE TEMP TABLE singleton_nulls_not_distinct (
+    id int, val text, UNIQUE NULLS NOT DISTINCT (id)
+);
+INSERT INTO singleton_nulls_not_distinct VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_nulls_not_distinct GROUP BY id, val;
+                QUERY PLAN                
+------------------------------------------
+ Seq Scan on singleton_nulls_not_distinct
+(1 row)
+
+-- A composite NULLS NOT DISTINCT index treats rows with NULL keys as
+-- duplicates, so nullable keys can still prove singleton grouping.
+CREATE TEMP TABLE singleton_composite_nnd (
+    a int, b int, val text,
+    UNIQUE NULLS NOT DISTINCT (a, b)
+);
+INSERT INTO singleton_composite_nnd
+VALUES (1, NULL, 'one'), (NULL, 1, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_composite_nnd GROUP BY a, b, val;
+             QUERY PLAN              
+-------------------------------------
+ Seq Scan on singleton_composite_nnd
+(1 row)
+
+-- A partitioned table has a table-wide unique proof.
+CREATE TEMP TABLE singleton_partitioned (
+    id int PRIMARY KEY, val text
+) PARTITION BY RANGE (id);
+CREATE TEMP TABLE singleton_partitioned_1
+    PARTITION OF singleton_partitioned FOR VALUES FROM (0) TO (10);
+CREATE TEMP TABLE singleton_partitioned_2
+    PARTITION OF singleton_partitioned FOR VALUES FROM (10) TO (20);
+INSERT INTO singleton_partitioned VALUES (1, 'one'), (11, 'eleven');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_partitioned GROUP BY id, val;
+                QUERY PLAN                 
+-------------------------------------------
+ Append
+   ->  Seq Scan on singleton_partitioned_1
+   ->  Seq Scan on singleton_partitioned_2
+(3 rows)
+
+SET enable_partitionwise_aggregate = on;
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_partitioned GROUP BY id, val;
+                QUERY PLAN                 
+-------------------------------------------
+ Append
+   ->  Seq Scan on singleton_partitioned_1
+   ->  Seq Scan on singleton_partitioned_2
+(3 rows)
+
+RESET enable_partitionwise_aggregate;
+-- Aggregate semantics must never be removed.
+EXPLAIN (COSTS OFF)
+SELECT id, count(*) FROM singleton_base GROUP BY id;
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Group Key: id
+   ->  Seq Scan on singleton_base
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id HAVING true;
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Group Key: id
+   ->  Seq Scan on singleton_base
+(3 rows)
+
+-- Aggregate-less implicit/degenerate grouping phases must not use singleton
+-- elimination.  A pure DISTINCT is a separate upper relation and has no
+-- GROUP BY proof.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM singleton_base;
+            QUERY PLAN            
+----------------------------------
+ Aggregate
+   ->  Seq Scan on singleton_base
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT true FROM singleton_base HAVING true;
+      QUERY PLAN       
+-----------------------
+ Result
+   Replaces: Aggregate
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT id, status FROM singleton_base
+GROUP BY GROUPING SETS ((id), (status));
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Hash Key: id
+   Hash Key: status
+   ->  Seq Scan on singleton_base
+(4 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT id FROM singleton_base;
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Group Key: id
+   ->  Seq Scan on singleton_base
+(3 rows)
+
+-- A grouped view is planned as its own query.  Its explicit unique-key proof
+-- may be applied there even though the outer query sees only a subquery RTE.
+CREATE TEMP VIEW singleton_grouped_view AS
+    SELECT id FROM singleton_base GROUP BY id;
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_grouped_view;
+         QUERY PLAN         
+----------------------------
+ Seq Scan on singleton_base
+(1 row)
+
+-- Other row-changing or ordering-sensitive semantics remain excluded.
+EXPLAIN (COSTS OFF)
+SELECT id, row_number() OVER (ORDER BY id)
+FROM singleton_base GROUP BY id;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ WindowAgg
+   Window: w1 AS (ORDER BY id ROWS UNBOUNDED PRECEDING)
+   ->  Group
+         Group Key: id
+         ->  Index Only Scan using singleton_base_pkey on singleton_base
+(5 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT id, generate_series(1, 1) FROM singleton_base GROUP BY id;
+               QUERY PLAN               
+----------------------------------------
+ ProjectSet
+   ->  HashAggregate
+         Group Key: id
+         ->  Seq Scan on singleton_base
+(4 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT id FROM singleton_base GROUP BY id;
+               QUERY PLAN               
+----------------------------------------
+ HashAggregate
+   Group Key: id
+   ->  HashAggregate
+         Group Key: id
+         ->  Seq Scan on singleton_base
+(5 rows)
+
+-- Set-operation children use a separate target convention and stay rejected.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id
+UNION
+SELECT id FROM singleton_base GROUP BY id;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ HashAggregate
+   Group Key: singleton_base.id
+   ->  Append
+         ->  HashAggregate
+               Group Key: singleton_base.id
+               ->  Seq Scan on singleton_base
+         ->  HashAggregate
+               Group Key: singleton_base_1.id
+               ->  Seq Scan on singleton_base singleton_base_1
+(9 rows)
+
+-- A non-flattenable subquery input stays rejected.
+EXPLAIN (COSTS OFF)
+SELECT id FROM (SELECT id FROM singleton_base OFFSET 0) s GROUP BY id;
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Group Key: singleton_base.id
+   ->  Seq Scan on singleton_base
+(3 rows)
+
+-- Additional non-simple grouping keys do not invalidate a unique-key proof.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id, length(payload);
+         QUERY PLAN         
+----------------------------
+ Seq Scan on singleton_base
+(1 row)
+
+SELECT id, length(payload) FROM singleton_base GROUP BY id, length(payload)
+ORDER BY id, length(payload);
+ id | length 
+----+--------
+  1 |      8
+  2 |      8
+  3 |      8
+  4 |      8
+  5 |      8
+(5 rows)
+
+-- Only direct grouping Vars can cover unique keys; extra expressions may
+-- appear before, after, or alongside those Vars.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c
+FROM singleton_composite
+GROUP BY c, a, length(d), b;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on singleton_composite
+(1 row)
+
+SELECT a, b, c, length(d)
+FROM singleton_composite
+GROUP BY c, a, length(d), b
+ORDER BY a, b, c;
+ a | b | c | length 
+---+---+---+--------
+ 1 | 1 | 1 |      3
+ 1 | 2 | 2 |      3
+ 2 | 1 | 3 |      5
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT a, b
+FROM singleton_composite
+GROUP BY a, b, a + 0, b + 0;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on singleton_composite
+(1 row)
+
+-- An expression cannot itself cover the unique key.
+EXPLAIN (COSTS OFF)
+SELECT length(payload) FROM singleton_base GROUP BY length(payload);
+            QUERY PLAN            
+----------------------------------
+ HashAggregate
+   Group Key: length(payload)
+   ->  Seq Scan on singleton_base
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT a, b + 0 FROM singleton_composite GROUP BY a, b + 0;
+              QUERY PLAN               
+---------------------------------------
+ HashAggregate
+   Group Key: a, (b + 0)
+   ->  Seq Scan on singleton_composite
+(3 rows)
+
+-- Expressions are also allowed with NULLS NOT DISTINCT proofs.
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_composite_nnd
+GROUP BY a, b, val, length(val);
+             QUERY PLAN              
+-------------------------------------
+ Seq Scan on singleton_composite_nnd
+(1 row)
+
+-- Preserve evaluation of extra volatile grouping expressions.  The singleton
+-- plan must evaluate them once per input row.
+CREATE TEMP SEQUENCE singleton_expr_calls;
+CREATE FUNCTION pg_temp.singleton_expr_call() RETURNS int
+LANGUAGE plpgsql VOLATILE AS $$
+BEGIN
+    RETURN nextval('singleton_expr_calls');
+END
+$$;
+SELECT setval('singleton_expr_calls', 1, false);
+ setval 
+--------
+      1
+(1 row)
+
+SELECT count(*) FROM (
+    SELECT id FROM singleton_base
+    GROUP BY id, pg_temp.singleton_expr_call()
+) q;
+ count 
+-------
+     5
+(1 row)
+
+SELECT last_value FROM singleton_expr_calls;
+ last_value 
+------------
+          5
+(1 row)
+
+-- A unique side does not make a join output unique.
+CREATE TEMP TABLE singleton_many (sid int, val text);
+INSERT INTO singleton_many
+SELECT g % 5, 'many' || g FROM generate_series(1, 10) g;
+EXPLAIN (COSTS OFF)
+SELECT b.id
+FROM singleton_base b JOIN singleton_many m ON m.sid = b.id
+GROUP BY b.id;
+                   QUERY PLAN                   
+------------------------------------------------
+ HashAggregate
+   Group Key: b.id
+   ->  Hash Join
+         Hash Cond: (m.sid = b.id)
+         ->  Seq Scan on singleton_many m
+         ->  Hash
+               ->  Seq Scan on singleton_base b
+(7 rows)
+
+-- A unique index proves uniqueness only under its own equality semantics.
+-- record_image_ops is bytewise equality, while plain GROUP BY on a composite
+-- uses record_ops.  Rows that the index distinguishes can still be one group.
+CREATE TYPE pg_temp.singleton_avg_rec AS (x numeric);
+CREATE TEMP TABLE singleton_opf (a singleton_avg_rec NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_opf_a ON singleton_opf (a record_image_ops);
+INSERT INTO singleton_opf
+VALUES (row(1.0)::singleton_avg_rec, 'X'),
+       (row(1.00)::singleton_avg_rec, 'Y');
+EXPLAIN (COSTS OFF)
+SELECT a, val FROM singleton_opf GROUP BY a, val;
+           QUERY PLAN            
+---------------------------------
+ HashAggregate
+   Group Key: a, val
+   ->  Seq Scan on singleton_opf
+(3 rows)
+
+-- Unsafe uniqueness proofs remain rejected.
+CREATE TEMP TABLE singleton_nullable (id int, val text);
+CREATE UNIQUE INDEX singleton_nullable_id ON singleton_nullable (id);
+INSERT INTO singleton_nullable VALUES (NULL, 'one'), (NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_nullable GROUP BY id, val;
+              QUERY PLAN              
+--------------------------------------
+ HashAggregate
+   Group Key: id, val
+   ->  Seq Scan on singleton_nullable
+(3 rows)
+
+-- Every key of a NULLS DISTINCT unique index must be NOT NULL.
+CREATE TEMP TABLE singleton_composite_nullable (
+    a int NOT NULL, b int, val text,
+    CONSTRAINT singleton_composite_nullable_key UNIQUE (a, b)
+);
+INSERT INTO singleton_composite_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_composite_nullable GROUP BY a, b, val;
+                   QUERY PLAN                   
+------------------------------------------------
+ HashAggregate
+   Group Key: a, b, val
+   ->  Seq Scan on singleton_composite_nullable
+(3 rows)
+
+-- Extra expressions cannot repair the missing NOT NULL proof.
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_composite_nullable
+GROUP BY a, b, val, length(val);
+                   QUERY PLAN                   
+------------------------------------------------
+ HashAggregate
+   Group Key: a, b, val, length(val)
+   ->  Seq Scan on singleton_composite_nullable
+(3 rows)
+
+CREATE TEMP TABLE singleton_deferrable (
+    id int, CONSTRAINT singleton_deferrable_id UNIQUE (id) DEFERRABLE
+);
+INSERT INTO singleton_deferrable VALUES (1), (2);
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_deferrable GROUP BY id;
+               QUERY PLAN               
+----------------------------------------
+ HashAggregate
+   Group Key: id
+   ->  Seq Scan on singleton_deferrable
+(3 rows)
+
+CREATE TEMP TABLE singleton_partial (id int NOT NULL, active boolean);
+CREATE UNIQUE INDEX singleton_partial_id
+    ON singleton_partial (id) WHERE active;
+INSERT INTO singleton_partial VALUES (1, true), (2, true);
+EXPLAIN (COSTS OFF)
+SELECT id, active FROM singleton_partial GROUP BY id, active;
+             QUERY PLAN              
+-------------------------------------
+ HashAggregate
+   Group Key: id, active
+   ->  Seq Scan on singleton_partial
+(3 rows)
+
+-- Even an apparently NULL-safe partial predicate is outside V1.
+CREATE TEMP TABLE singleton_partial_nullable (a int, b int, val text);
+CREATE UNIQUE INDEX singleton_partial_nullable_ab
+    ON singleton_partial_nullable (a, b)
+    WHERE a IS NOT NULL AND b IS NOT NULL;
+INSERT INTO singleton_partial_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_partial_nullable GROUP BY a, b, val;
+                  QUERY PLAN                  
+----------------------------------------------
+ HashAggregate
+   Group Key: a, b, val
+   ->  Seq Scan on singleton_partial_nullable
+(3 rows)
+
+CREATE TEMP TABLE singleton_expression (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_expression_upper
+    ON singleton_expression (upper(val));
+INSERT INTO singleton_expression VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_expression GROUP BY id, val;
+               QUERY PLAN               
+----------------------------------------
+ HashAggregate
+   Group Key: id, val
+   ->  Seq Scan on singleton_expression
+(3 rows)
+
+CREATE TEMP TABLE singleton_parent (id int PRIMARY KEY, val text);
+CREATE TEMP TABLE singleton_child () INHERITS (singleton_parent);
+INSERT INTO singleton_parent VALUES (1, 'parent');
+INSERT INTO singleton_child VALUES (1, 'child');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_parent GROUP BY id, val;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ HashAggregate
+   Group Key: singleton_parent.id, singleton_parent.val
+   ->  Append
+         ->  Seq Scan on singleton_parent singleton_parent_1
+         ->  Seq Scan on singleton_child singleton_parent_2
+(5 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM ONLY singleton_parent GROUP BY id, val;
+          QUERY PLAN          
+------------------------------
+ Seq Scan on singleton_parent
+(1 row)
+
+-- A partitioned composite unique index must still prove every key.
+CREATE TEMP TABLE singleton_partitioned_nullable (
+    a int, b int, val text
+) PARTITION BY RANGE (a);
+CREATE UNIQUE INDEX singleton_partitioned_nullable_ab
+    ON singleton_partitioned_nullable (a, b);
+CREATE TEMP TABLE singleton_partitioned_nullable_1
+    PARTITION OF singleton_partitioned_nullable FOR VALUES FROM (0) TO (10);
+INSERT INTO singleton_partitioned_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_partitioned_nullable GROUP BY a, b, val;
+                                                     QUERY PLAN                                                      
+---------------------------------------------------------------------------------------------------------------------
+ HashAggregate
+   Group Key: singleton_partitioned_nullable.a, singleton_partitioned_nullable.b, singleton_partitioned_nullable.val
+   ->  Seq Scan on singleton_partitioned_nullable_1 singleton_partitioned_nullable
+(3 rows)
+
+-- Dropping the proof must invalidate a cached plan.
+CREATE TEMP TABLE singleton_invalidate (id int PRIMARY KEY, val text);
+INSERT INTO singleton_invalidate VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_prep AS
+    SELECT id, val FROM singleton_invalidate GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_prep;
+            QUERY PLAN            
+----------------------------------
+ Seq Scan on singleton_invalidate
+(1 row)
+
+ALTER TABLE singleton_invalidate DROP CONSTRAINT singleton_invalidate_pkey;
+EXPLAIN (COSTS OFF) EXECUTE singleton_prep;
+               QUERY PLAN               
+----------------------------------------
+ HashAggregate
+   Group Key: id, val
+   ->  Seq Scan on singleton_invalidate
+(3 rows)
+
+DEALLOCATE singleton_prep;
+-- A plain unique index is also invalidation evidence for the proof.
+CREATE TEMP TABLE singleton_invalidate_index (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_invalidate_index_id
+    ON singleton_invalidate_index (id);
+INSERT INTO singleton_invalidate_index VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_invalidate_index_prep AS
+    SELECT id, val FROM singleton_invalidate_index GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_index_prep;
+               QUERY PLAN               
+----------------------------------------
+ Seq Scan on singleton_invalidate_index
+(1 row)
+
+DROP INDEX singleton_invalidate_index_id;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_index_prep;
+                  QUERY PLAN                  
+----------------------------------------------
+ HashAggregate
+   Group Key: id, val
+   ->  Seq Scan on singleton_invalidate_index
+(3 rows)
+
+DEALLOCATE singleton_invalidate_index_prep;
+-- Removing a key column's NOT NULL constraint must invalidate the proof.
+CREATE TEMP TABLE singleton_invalidate_notnull (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_invalidate_notnull_id
+    ON singleton_invalidate_notnull (id);
+INSERT INTO singleton_invalidate_notnull VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_invalidate_notnull_prep AS
+    SELECT id, val FROM singleton_invalidate_notnull GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_notnull_prep;
+                QUERY PLAN                
+------------------------------------------
+ Seq Scan on singleton_invalidate_notnull
+(1 row)
+
+ALTER TABLE singleton_invalidate_notnull ALTER COLUMN id DROP NOT NULL;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_notnull_prep;
+                   QUERY PLAN                   
+------------------------------------------------
+ HashAggregate
+   Group Key: id, val
+   ->  Seq Scan on singleton_invalidate_notnull
+(3 rows)
+
+DEALLOCATE singleton_invalidate_notnull_prep;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 75063f87a4..d006945159 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -61,6 +61,8 @@ test: sanity_check
 # aggregates depends on create_aggregate
 # join depends on create_misc
 # ----------
+test: singleton_grouping
+
 test: select_into select_distinct select_distinct_on select_implicit select_having subselect union case join aggregates transactions random portals arrays btree_index hash_index update delete namespace prepared_xacts
 
 # ----------
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index e96ad737aa..1ef5001675 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -1475,6 +1475,20 @@ SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = '
 SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error
 DROP DOMAIN d1, d2;
 
+-- A unique index proves uniqueness only under its own collation.  The
+-- case-insensitive GROUP BY may merge rows that the deterministic "C" unique
+-- index treats as distinct, so singleton grouping elimination must reject the
+-- index as a proof.
+CREATE TABLE singleton_collation_mismatch (
+    val text COLLATE case_insensitive
+);
+CREATE UNIQUE INDEX singleton_collation_mismatch_val
+    ON singleton_collation_mismatch (val COLLATE "C");
+INSERT INTO singleton_collation_mismatch VALUES ('abc'), ('ABC');
+EXPLAIN (COSTS OFF)
+SELECT val FROM singleton_collation_mismatch GROUP BY val;
+DROP TABLE singleton_collation_mismatch;
+
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;
diff --git a/src/test/regress/sql/singleton_grouping.sql b/src/test/regress/sql/singleton_grouping.sql
new file mode 100644
index 0000000000..0059d1c1df
--- /dev/null
+++ b/src/test/regress/sql/singleton_grouping.sql
@@ -0,0 +1,311 @@
+-- Test path-level elimination of grouping when each input row is its own group.
+CREATE TEMP TABLE singleton_base (
+    id int PRIMARY KEY,
+    status text NOT NULL,
+    payload text
+);
+INSERT INTO singleton_base
+SELECT g, 'status' || g, 'payload' || g FROM generate_series(1, 5) g;
+
+-- The unique key makes each row its own group.
+EXPLAIN (COSTS OFF)
+SELECT id, status, payload FROM singleton_base GROUP BY id, status, payload;
+SELECT id, string_agg(status, ',' ORDER BY status)
+FROM singleton_base GROUP BY id, status, payload ORDER BY id;
+
+-- The input pathkeys survive projection.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id ORDER BY id;
+
+-- A constant-covered grouping key may be reduced by equivalence processing;
+-- the original key still proves singleton grouping.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base WHERE id = 1 GROUP BY id;
+
+-- Equivalence processing may remove only some original keys.  The untouched
+-- original clause still contains the unique key and remains a valid proof.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base WHERE id = 1 GROUP BY id, status;
+
+-- A composite key is sufficient; remaining grouping keys may be redundant.
+CREATE TEMP TABLE singleton_composite (
+    a int, b int, c int, d text, PRIMARY KEY (a, b)
+);
+INSERT INTO singleton_composite
+VALUES (1, 1, 1, 'one'), (1, 2, 2, 'two'), (2, 1, 3, 'three');
+EXPLAIN (COSTS OFF)
+SELECT a, b, c, d FROM singleton_composite GROUP BY a, b, c, d;
+
+-- Non-deferrable unique constraints and indexes, including NULLS NOT
+-- DISTINCT, also prove singleton grouping.
+CREATE TEMP TABLE singleton_unique (
+    id int NOT NULL, val text, UNIQUE (id)
+);
+INSERT INTO singleton_unique VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_unique GROUP BY id, val;
+
+CREATE TEMP TABLE singleton_plain_unique (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_plain_unique_id ON singleton_plain_unique (id);
+INSERT INTO singleton_plain_unique VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_plain_unique GROUP BY id, val;
+
+CREATE TEMP TABLE singleton_nulls_not_distinct (
+    id int, val text, UNIQUE NULLS NOT DISTINCT (id)
+);
+INSERT INTO singleton_nulls_not_distinct VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_nulls_not_distinct GROUP BY id, val;
+
+-- A composite NULLS NOT DISTINCT index treats rows with NULL keys as
+-- duplicates, so nullable keys can still prove singleton grouping.
+CREATE TEMP TABLE singleton_composite_nnd (
+    a int, b int, val text,
+    UNIQUE NULLS NOT DISTINCT (a, b)
+);
+INSERT INTO singleton_composite_nnd
+VALUES (1, NULL, 'one'), (NULL, 1, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_composite_nnd GROUP BY a, b, val;
+
+-- A partitioned table has a table-wide unique proof.
+CREATE TEMP TABLE singleton_partitioned (
+    id int PRIMARY KEY, val text
+) PARTITION BY RANGE (id);
+CREATE TEMP TABLE singleton_partitioned_1
+    PARTITION OF singleton_partitioned FOR VALUES FROM (0) TO (10);
+CREATE TEMP TABLE singleton_partitioned_2
+    PARTITION OF singleton_partitioned FOR VALUES FROM (10) TO (20);
+INSERT INTO singleton_partitioned VALUES (1, 'one'), (11, 'eleven');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_partitioned GROUP BY id, val;
+SET enable_partitionwise_aggregate = on;
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_partitioned GROUP BY id, val;
+RESET enable_partitionwise_aggregate;
+
+-- Aggregate semantics must never be removed.
+EXPLAIN (COSTS OFF)
+SELECT id, count(*) FROM singleton_base GROUP BY id;
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id HAVING true;
+
+-- Aggregate-less implicit/degenerate grouping phases must not use singleton
+-- elimination.  A pure DISTINCT is a separate upper relation and has no
+-- GROUP BY proof.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM singleton_base;
+EXPLAIN (COSTS OFF)
+SELECT true FROM singleton_base HAVING true;
+EXPLAIN (COSTS OFF)
+SELECT id, status FROM singleton_base
+GROUP BY GROUPING SETS ((id), (status));
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT id FROM singleton_base;
+-- A grouped view is planned as its own query.  Its explicit unique-key proof
+-- may be applied there even though the outer query sees only a subquery RTE.
+CREATE TEMP VIEW singleton_grouped_view AS
+    SELECT id FROM singleton_base GROUP BY id;
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_grouped_view;
+
+-- Other row-changing or ordering-sensitive semantics remain excluded.
+EXPLAIN (COSTS OFF)
+SELECT id, row_number() OVER (ORDER BY id)
+FROM singleton_base GROUP BY id;
+EXPLAIN (COSTS OFF)
+SELECT id, generate_series(1, 1) FROM singleton_base GROUP BY id;
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT id FROM singleton_base GROUP BY id;
+
+-- Set-operation children use a separate target convention and stay rejected.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id
+UNION
+SELECT id FROM singleton_base GROUP BY id;
+
+-- A non-flattenable subquery input stays rejected.
+EXPLAIN (COSTS OFF)
+SELECT id FROM (SELECT id FROM singleton_base OFFSET 0) s GROUP BY id;
+
+-- Additional non-simple grouping keys do not invalidate a unique-key proof.
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_base GROUP BY id, length(payload);
+SELECT id, length(payload) FROM singleton_base GROUP BY id, length(payload)
+ORDER BY id, length(payload);
+
+-- Only direct grouping Vars can cover unique keys; extra expressions may
+-- appear before, after, or alongside those Vars.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c
+FROM singleton_composite
+GROUP BY c, a, length(d), b;
+SELECT a, b, c, length(d)
+FROM singleton_composite
+GROUP BY c, a, length(d), b
+ORDER BY a, b, c;
+EXPLAIN (COSTS OFF)
+SELECT a, b
+FROM singleton_composite
+GROUP BY a, b, a + 0, b + 0;
+
+-- An expression cannot itself cover the unique key.
+EXPLAIN (COSTS OFF)
+SELECT length(payload) FROM singleton_base GROUP BY length(payload);
+EXPLAIN (COSTS OFF)
+SELECT a, b + 0 FROM singleton_composite GROUP BY a, b + 0;
+
+-- Expressions are also allowed with NULLS NOT DISTINCT proofs.
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_composite_nnd
+GROUP BY a, b, val, length(val);
+
+-- Preserve evaluation of extra volatile grouping expressions.  The singleton
+-- plan must evaluate them once per input row.
+CREATE TEMP SEQUENCE singleton_expr_calls;
+CREATE FUNCTION pg_temp.singleton_expr_call() RETURNS int
+LANGUAGE plpgsql VOLATILE AS $$
+BEGIN
+    RETURN nextval('singleton_expr_calls');
+END
+$$;
+SELECT setval('singleton_expr_calls', 1, false);
+SELECT count(*) FROM (
+    SELECT id FROM singleton_base
+    GROUP BY id, pg_temp.singleton_expr_call()
+) q;
+SELECT last_value FROM singleton_expr_calls;
+
+-- A unique side does not make a join output unique.
+CREATE TEMP TABLE singleton_many (sid int, val text);
+INSERT INTO singleton_many
+SELECT g % 5, 'many' || g FROM generate_series(1, 10) g;
+EXPLAIN (COSTS OFF)
+SELECT b.id
+FROM singleton_base b JOIN singleton_many m ON m.sid = b.id
+GROUP BY b.id;
+
+-- A unique index proves uniqueness only under its own equality semantics.
+-- record_image_ops is bytewise equality, while plain GROUP BY on a composite
+-- uses record_ops.  Rows that the index distinguishes can still be one group.
+CREATE TYPE pg_temp.singleton_avg_rec AS (x numeric);
+CREATE TEMP TABLE singleton_opf (a singleton_avg_rec NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_opf_a ON singleton_opf (a record_image_ops);
+INSERT INTO singleton_opf
+VALUES (row(1.0)::singleton_avg_rec, 'X'),
+       (row(1.00)::singleton_avg_rec, 'Y');
+EXPLAIN (COSTS OFF)
+SELECT a, val FROM singleton_opf GROUP BY a, val;
+
+-- Unsafe uniqueness proofs remain rejected.
+CREATE TEMP TABLE singleton_nullable (id int, val text);
+CREATE UNIQUE INDEX singleton_nullable_id ON singleton_nullable (id);
+INSERT INTO singleton_nullable VALUES (NULL, 'one'), (NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_nullable GROUP BY id, val;
+
+-- Every key of a NULLS DISTINCT unique index must be NOT NULL.
+CREATE TEMP TABLE singleton_composite_nullable (
+    a int NOT NULL, b int, val text,
+    CONSTRAINT singleton_composite_nullable_key UNIQUE (a, b)
+);
+INSERT INTO singleton_composite_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_composite_nullable GROUP BY a, b, val;
+
+-- Extra expressions cannot repair the missing NOT NULL proof.
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_composite_nullable
+GROUP BY a, b, val, length(val);
+
+CREATE TEMP TABLE singleton_deferrable (
+    id int, CONSTRAINT singleton_deferrable_id UNIQUE (id) DEFERRABLE
+);
+INSERT INTO singleton_deferrable VALUES (1), (2);
+EXPLAIN (COSTS OFF)
+SELECT id FROM singleton_deferrable GROUP BY id;
+
+CREATE TEMP TABLE singleton_partial (id int NOT NULL, active boolean);
+CREATE UNIQUE INDEX singleton_partial_id
+    ON singleton_partial (id) WHERE active;
+INSERT INTO singleton_partial VALUES (1, true), (2, true);
+EXPLAIN (COSTS OFF)
+SELECT id, active FROM singleton_partial GROUP BY id, active;
+
+-- Even an apparently NULL-safe partial predicate is outside V1.
+CREATE TEMP TABLE singleton_partial_nullable (a int, b int, val text);
+CREATE UNIQUE INDEX singleton_partial_nullable_ab
+    ON singleton_partial_nullable (a, b)
+    WHERE a IS NOT NULL AND b IS NOT NULL;
+INSERT INTO singleton_partial_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val FROM singleton_partial_nullable GROUP BY a, b, val;
+
+CREATE TEMP TABLE singleton_expression (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_expression_upper
+    ON singleton_expression (upper(val));
+INSERT INTO singleton_expression VALUES (1, 'one'), (2, 'two');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_expression GROUP BY id, val;
+
+CREATE TEMP TABLE singleton_parent (id int PRIMARY KEY, val text);
+CREATE TEMP TABLE singleton_child () INHERITS (singleton_parent);
+INSERT INTO singleton_parent VALUES (1, 'parent');
+INSERT INTO singleton_child VALUES (1, 'child');
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM singleton_parent GROUP BY id, val;
+EXPLAIN (COSTS OFF)
+SELECT id, val FROM ONLY singleton_parent GROUP BY id, val;
+
+-- A partitioned composite unique index must still prove every key.
+CREATE TEMP TABLE singleton_partitioned_nullable (
+    a int, b int, val text
+) PARTITION BY RANGE (a);
+CREATE UNIQUE INDEX singleton_partitioned_nullable_ab
+    ON singleton_partitioned_nullable (a, b);
+CREATE TEMP TABLE singleton_partitioned_nullable_1
+    PARTITION OF singleton_partitioned_nullable FOR VALUES FROM (0) TO (10);
+INSERT INTO singleton_partitioned_nullable
+VALUES (1, NULL, 'one'), (1, NULL, 'two');
+EXPLAIN (COSTS OFF)
+SELECT a, b, val
+FROM singleton_partitioned_nullable GROUP BY a, b, val;
+
+-- Dropping the proof must invalidate a cached plan.
+CREATE TEMP TABLE singleton_invalidate (id int PRIMARY KEY, val text);
+INSERT INTO singleton_invalidate VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_prep AS
+    SELECT id, val FROM singleton_invalidate GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_prep;
+ALTER TABLE singleton_invalidate DROP CONSTRAINT singleton_invalidate_pkey;
+EXPLAIN (COSTS OFF) EXECUTE singleton_prep;
+DEALLOCATE singleton_prep;
+
+-- A plain unique index is also invalidation evidence for the proof.
+CREATE TEMP TABLE singleton_invalidate_index (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_invalidate_index_id
+    ON singleton_invalidate_index (id);
+INSERT INTO singleton_invalidate_index VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_invalidate_index_prep AS
+    SELECT id, val FROM singleton_invalidate_index GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_index_prep;
+DROP INDEX singleton_invalidate_index_id;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_index_prep;
+DEALLOCATE singleton_invalidate_index_prep;
+
+-- Removing a key column's NOT NULL constraint must invalidate the proof.
+CREATE TEMP TABLE singleton_invalidate_notnull (id int NOT NULL, val text);
+CREATE UNIQUE INDEX singleton_invalidate_notnull_id
+    ON singleton_invalidate_notnull (id);
+INSERT INTO singleton_invalidate_notnull VALUES (1, 'one'), (2, 'two');
+PREPARE singleton_invalidate_notnull_prep AS
+    SELECT id, val FROM singleton_invalidate_notnull GROUP BY id, val;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_notnull_prep;
+ALTER TABLE singleton_invalidate_notnull ALTER COLUMN id DROP NOT NULL;
+EXPLAIN (COSTS OFF) EXECUTE singleton_invalidate_notnull_prep;
+DEALLOCATE singleton_invalidate_notnull_prep;
-- 
2.43.0

