From 628e49767812a4124c03b163c46dae0579c40cbd Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Thu, 9 Jul 2026 17:51:10 +0900
Subject: [PATCH v1] Introduce UniqueKeys to track the distinctness of
 relations

Add a UniqueKeys facility that records, for each relation the planner
builds, the sets of expressions over which its output is known to be
distinct.  A RelOptInfo's uniquekeys list holds UniqueKey nodes, each
a set of EquivalenceClasses that no two output rows agree on; an empty
set means the relation emits at most one row.  Expressing keys as
EquivalenceClasses, as pathkeys do, lets a key stand for any member of
the class and keeps the representation compact.

Each UniqueKey also carries a "nullable" flag.  A non-nullable key is
NULL-aware (no two rows are equal even treating NULLs as equal), which
is what justifies removing a DISTINCT or GROUP BY.  A nullable key
guarantees distinctness only among rows whose key expressions are all
non-NULL, which still suffices to prove a join inner-unique or to
match a strict join clause.  Keys are always kept over the base
(un-nulled) EquivalenceClasses; the nullable flag records the effect
of an outer join null-extending a column.

UniqueKeys are deduced bottom-up.  A base relation's keys come from
its unique, immediately enforced, non-partial indexes.  A subquery
inherits the keys already deduced for its own final relation,
translated into the outer query, plus a whole-row key for a non-ALL
set operation.  A join relation's keys are derived from its inputs by
preserving one side's key when the other side is unique for the join
clauses, and by combining a key from each side; an inner join's strict
clauses can strengthen a nullable key back to non-nullable.  Finally
the grouping and DISTINCT steps are distinct over their clause
columns, and steps that neither add nor duplicate rows carry their
input's keys through unchanged.

To bound the bookkeeping, keys are tracked only over ECs that some
consumer could use.  Those consumers are: removing a redundant
DISTINCT or GROUP BY step when the input is already distinct over the
relevant columns; proving a join inner-unique in innerrel_is_unique(),
now including multi-relation inner sides; and skipping the
unique-ification of a semijoin's RHS when it is already distinct.

See src/backend/optimizer/README for a fuller description.
---
 .../expected/pg_stash_advice.out              |   42 +-
 .../postgres_fdw/expected/postgres_fdw.out    |   28 +-
 src/backend/optimizer/README                  |   97 ++
 src/backend/optimizer/path/Makefile           |    3 +-
 src/backend/optimizer/path/allpaths.c         |   39 +
 src/backend/optimizer/path/equivclass.c       |    8 +-
 src/backend/optimizer/path/meson.build        |    1 +
 src/backend/optimizer/path/pathkeys.c         |    3 +-
 src/backend/optimizer/path/uniquekeys.c       | 1296 +++++++++++++++++
 src/backend/optimizer/plan/analyzejoins.c     |   18 +-
 src/backend/optimizer/plan/planner.c          |  150 +-
 src/backend/optimizer/util/relnode.c          |    5 +
 src/include/nodes/pathnodes.h                 |   42 +
 src/include/optimizer/paths.h                 |   38 +-
 src/test/regress/expected/aggregates.out      |   49 +-
 src/test/regress/expected/join.out            |   39 +-
 src/test/regress/expected/uniquekeys.out      |  354 +++++
 src/test/regress/parallel_schedule            |    2 +
 src/test/regress/sql/aggregates.sql           |   24 +-
 src/test/regress/sql/uniquekeys.sql           |  118 ++
 src/tools/pgindent/typedefs.list              |    1 +
 21 files changed, 2235 insertions(+), 122 deletions(-)
 create mode 100644 src/backend/optimizer/path/uniquekeys.c
 create mode 100644 src/test/regress/expected/uniquekeys.out
 create mode 100644 src/test/regress/sql/uniquekeys.sql

diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice.out b/contrib/pg_stash_advice/expected/pg_stash_advice.out
index 8c24a21295e..4e1b6d958a7 100644
--- a/contrib/pg_stash_advice/expected/pg_stash_advice.out
+++ b/contrib/pg_stash_advice/expected/pg_stash_advice.out
@@ -142,21 +142,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                    QUERY PLAN                     
----------------------------------------------------
- Nested Loop
-   ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
-         ->  Seq Scan on aa_fact f
-         ->  Hash
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Hash Join
+   Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id))
+   ->  Seq Scan on aa_fact f
+   ->  Hash
+         ->  Nested Loop
                ->  Seq Scan on aa_dim2 d2
                      Filter: (val2 = 1)
-   ->  Index Scan using aa_dim1_pkey on aa_dim1 d1
-         Index Cond: (id = f.dim1_id)
-         Filter: (val1 = 1)
+               ->  Seq Scan on aa_dim1 d1
+                     Filter: (val1 = 1)
  Supplied Plan Advice:
    NESTED_LOOP_PLAIN(d1) /* matched */
-(12 rows)
+(11 rows)
 
 -- Add a useless extra entry to our test stash. Shouldn't change the result
 -- from the previous test.
@@ -172,21 +171,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                    QUERY PLAN                     
----------------------------------------------------
- Nested Loop
-   ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
-         ->  Seq Scan on aa_fact f
-         ->  Hash
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Hash Join
+   Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id))
+   ->  Seq Scan on aa_fact f
+   ->  Hash
+         ->  Nested Loop
                ->  Seq Scan on aa_dim2 d2
                      Filter: (val2 = 1)
-   ->  Index Scan using aa_dim1_pkey on aa_dim1 d1
-         Index Cond: (id = f.dim1_id)
-         Filter: (val1 = 1)
+               ->  Seq Scan on aa_dim1 d1
+                     Filter: (val1 = 1)
  Supplied Plan Advice:
    NESTED_LOOP_PLAIN(d1) /* matched */
-(12 rows)
+(11 rows)
 
 -- Try an empty stash to be sure it does nothing
 SELECT pg_create_advice_stash('regress_empty_stash');
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index d19121b05da..2cfb040aef8 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3458,22 +3458,20 @@ select sum(c1%3), sum(distinct c1%3 order by c1%3) filter (where c1%3 < 2), c2 f
 -- Outer query is aggregation query
 explain (verbose, costs off)
 select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1;
-                                                          QUERY PLAN                                                          
-------------------------------------------------------------------------------------------------------------------------------
- Unique
+                                                       QUERY PLAN                                                       
+------------------------------------------------------------------------------------------------------------------------
+ Sort
    Output: ((SubPlan expr_1))
-   ->  Sort
-         Output: ((SubPlan expr_1))
-         Sort Key: ((SubPlan expr_1))
-         ->  Foreign Scan
-               Output: (SubPlan expr_1)
-               Relations: Aggregate on (public.ft2 t2)
-               Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 % 6) = 0))
-               SubPlan expr_1
-                 ->  Foreign Scan on public.ft1 t1
-                       Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10))))
-                       Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6))
-(13 rows)
+   Sort Key: ((SubPlan expr_1))
+   ->  Foreign Scan
+         Output: (SubPlan expr_1)
+         Relations: Aggregate on (public.ft2 t2)
+         Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 % 6) = 0))
+         SubPlan expr_1
+           ->  Foreign Scan on public.ft1 t1
+                 Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10))))
+                 Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6))
+(11 rows)
 
 select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1;
  count 
diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README
index 78a307cc523..5ebc0f42e9a 100644
--- a/src/backend/optimizer/README
+++ b/src/backend/optimizer/README
@@ -1056,6 +1056,103 @@ Currently this happens only for queries involving multiple window functions
 with different orderings, for which extra sorts are needed anyway.
 
 
+UniqueKeys
+----------
+
+The UniqueKeys data structure represents what is known about the distinctness
+of the tuples generated by a relation.  A RelOptInfo's uniquekeys field is a
+list of UniqueKey nodes, each asserting that the relation cannot output two
+rows that are equal on a particular set of expressions.  As with PathKeys,
+those expressions are represented by EquivalenceClasses, so a UniqueKey is
+essentially a set of ECs that no two output rows agree on.  A UniqueKey with
+an empty set means the relation emits at most one row.
+
+Each UniqueKey also carries a "nullable" flag.  When it is false the
+guarantee is NULL-aware: no two rows are equal on the key expressions even
+treating NULLs as equal, which is what justifies removing a redundant
+DISTINCT or GROUP BY step.  When it is true, distinctness is guaranteed only
+among rows whose key expressions are all non-NULL; that is weaker, but still
+enough to prove a join inner-unique or to match a strict join clause, where
+rows with NULL key values cannot match anything anyway.
+
+Uniquekeys reference whole EquivalenceClasses for the same reason pathkeys
+do: whenever two of an EC's members both appear in a relation's output, a
+restriction or join clause derived from the EC has been applied, so the two
+are equal in every emitted row and naming the class rather than a particular
+member loses nothing.  Keys are always expressed over the base (un-nulled)
+ECs.  A Var emitted from the nullable side of an outer join carries
+varnullingrel bits and so belongs to a different EC than the base Var (just
+as for pathkeys); rather than switch a key onto that EC as it crosses the
+join, we leave the key on the base EC and let the "nullable" flag record the
+outer join's effect.  A consumer looking up a column's EC strips the
+varnullingrel bits first, so a reference to the null-extended column still
+matches the base-EC key.
+
+UniqueKeys are derived bottom-up, as each RelOptInfo is built.
+
+A base relation's keys come from its own structure.  A plain table is
+distinct over the columns of any unique, immediately enforced, non-partial
+index; a key column equated to a constant may be dropped, since the remaining
+columns still determine the row.  Such a key is non-nullable only if every
+remaining column is known non-NULL, from a NOT NULL constraint or a strict
+restriction clause, or the index is declared NULLS NOT DISTINCT.  A subquery
+in FROM inherits the distinctness already deduced for its own final relation:
+whatever uniquekeys that relation carries are translated, with their
+nullability, into the outer query's EquivalenceClasses.  A set operation is
+the one deduplicating step that leaves no key on the subquery's final
+relation, so a non-ALL set operation's whole-output-row key is derived
+directly.
+
+A join relation's keys are derived from those of its two inputs, say A and B.
+Each output row of the join pairs one row of A with one row of B, except that
+an outer join may emit a row of one side paired with a null-extension of the
+other.  Two rules generate the join's keys:
+
+	Preservation: a key of one side alone remains a key of the join if the
+	join does not duplicate that side's rows, that is, each contributes at
+	most one output row.  This holds when the other side is unique for the
+	join clauses (each row matches at most one row of it), and
+	unconditionally for the retained side of a semijoin or antijoin (whose
+	output is a subset of it).
+
+	Combination: the union of a key of A and a key of B is a key of the
+	join.  Two output rows equal on the union agree on A's key, hence come
+	from the same A row, and on B's key, hence the same B row; a given pair
+	of source rows is emitted at most once.
+
+Both rules inherit the input keys' nullability, and a Preservation key is
+additionally nullable if the join null-extends its side.  In that case the
+empty key does not survive at all: it has no columns for the nullable flag to
+exclude, yet the join emits a fresh null-extended row for every unmatched row
+of the other side, so "at most one row" no longer holds.
+
+Conversely, a nullable key can be strengthened back to non-nullable at an
+inner join whose own clauses force its columns non-NULL.  An inner join
+applies every clause to all output rows, so any expression a strict clause
+requires non-NULL is non-NULL throughout the output; if every EC of a
+nullable key holds such an expression, no output row has a NULL key column
+and the key is in fact NULL-aware.  It does not apply at outer joins, whose
+clauses do not filter their null-extended rows.
+
+A side with lateral references into the other is re-executed once per row of
+that other side, so its rows are effectively multiplied and it keeps its keys
+only through Combination, not Preservation.
+
+Finally, the query's upper relations get keys too.  The grouping step is
+non-nullable distinct over the grouping columns, and emits a single row (the
+empty key) for a plain aggregation or grouping only by constants; the DISTINCT
+step is non-nullable distinct over the DISTINCT columns.  A step that cannot
+introduce duplicate rows, such as window-function evaluation, sorting, or the
+final LockRows/Limit/ModifyTable, carries its input's keys through unchanged.
+
+To bound the bookkeeping, keys are built only over EquivalenceClasses that
+some consumer could actually use: those of the DISTINCT clause, of the GROUP
+BY clause when the grouping step could be a no-op, and those that can form
+join clauses (including the single-member ECs of an outer join's clause
+sides, and the un-nulled counterparts of any of these).  A key column outside
+this set is not tracked, so not every unique index gives rise to a UniqueKey.
+
+
 Parameterized Paths
 -------------------
 
diff --git a/src/backend/optimizer/path/Makefile b/src/backend/optimizer/path/Makefile
index 1e199ff66f7..7b9820c25f3 100644
--- a/src/backend/optimizer/path/Makefile
+++ b/src/backend/optimizer/path/Makefile
@@ -21,6 +21,7 @@ OBJS = \
 	joinpath.o \
 	joinrels.o \
 	pathkeys.o \
-	tidpath.o
+	tidpath.o \
+	uniquekeys.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 9c040ecefc8..4b3fda0fc46 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -95,6 +95,7 @@ join_search_hook_type join_search_hook = NULL;
 static void set_base_rel_consider_startup(PlannerInfo *root);
 static void set_base_rel_sizes(PlannerInfo *root);
 static void setup_simple_grouped_rels(PlannerInfo *root);
+static void set_base_rel_uniquekeys(PlannerInfo *root);
 static void set_base_rel_pathlists(PlannerInfo *root);
 static void set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 						 Index rti, RangeTblEntry *rte);
@@ -200,6 +201,11 @@ make_one_rel(PlannerInfo *root, List *joinlist)
 	 */
 	setup_simple_grouped_rels(root);
 
+	/*
+	 * Deduce the base rels' unique keys before the join search uses them.
+	 */
+	set_base_rel_uniquekeys(root);
+
 	/*
 	 * We should now have size estimates for every actual table involved in
 	 * the query, and we also know which if any have been deleted from the
@@ -374,6 +380,39 @@ setup_simple_grouped_rels(PlannerInfo *root)
 	}
 }
 
+/*
+ * set_base_rel_uniquekeys
+ *	  Deduce the unique keys of each base relation's output.
+ *
+ * This needs to be done after EquivalenceClass merging and the EC-mutating
+ * steps (such as join removal) are complete, and after subqueries in FROM
+ * have been planned, since a subquery RTE's keys are translated from its
+ * final relation's uniquekeys.  It must run before the join search starts,
+ * which derives join relations' keys from the base rels'.
+ */
+static void
+set_base_rel_uniquekeys(PlannerInfo *root)
+{
+	Index		rti;
+
+	for (rti = 1; rti < root->simple_rel_array_size; rti++)
+	{
+		RelOptInfo *rel = root->simple_rel_array[rti];
+
+		/* there may be empty slots corresponding to non-baserel RTEs */
+		if (rel == NULL)
+			continue;
+
+		Assert(rel->relid == rti);	/* sanity check on array */
+
+		/* ignore RTEs that are "other rels" */
+		if (rel->reloptkind != RELOPT_BASEREL)
+			continue;
+
+		populate_baserel_uniquekeys(root, rel);
+	}
+}
+
 /*
  * set_base_rel_pathlists
  *	  Finds all paths available for scanning each base-relation entry.
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index e3697df51a2..cf808a14910 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -495,6 +495,7 @@ process_equivalence(PlannerInfo *root,
 		ec->ec_min_security = restrictinfo->security_level;
 		ec->ec_max_security = restrictinfo->security_level;
 		ec->ec_merged = NULL;
+		/* ec_index is set later */
 		em1 = add_eq_member(ec, item1, item1_relids,
 							jdomain, item1_type);
 		em2 = add_eq_member(ec, item2, item2_relids,
@@ -863,6 +864,7 @@ get_eclass_for_sort_expr(PlannerInfo *root,
 		}
 	}
 
+	newec->ec_index = list_length(root->eq_classes);
 	root->eq_classes = lappend(root->eq_classes, newec);
 
 	/*
@@ -871,7 +873,6 @@ get_eclass_for_sort_expr(PlannerInfo *root,
 	 */
 	if (root->ec_merging_done)
 	{
-		int			ec_index = list_length(root->eq_classes) - 1;
 		int			i = -1;
 
 		while ((i = bms_next_member(newec->ec_relids, i)) > 0)
@@ -891,7 +892,7 @@ get_eclass_for_sort_expr(PlannerInfo *root,
 			Assert(rel->reloptkind == RELOPT_BASEREL);
 
 			rel->eclass_indexes = bms_add_member(rel->eclass_indexes,
-												 ec_index);
+												 newec->ec_index);
 		}
 	}
 
@@ -1208,6 +1209,9 @@ generate_base_implied_equalities(PlannerInfo *root)
 		Assert(ec->ec_merged == NULL);	/* else shouldn't be in list */
 		Assert(!ec->ec_broken); /* not yet anyway... */
 
+		/* The EC's position is now final, since no more merging can happen */
+		ec->ec_index = ec_index;
+
 		/*
 		 * Generate implied equalities that are restriction clauses.
 		 * Single-member ECs won't generate any deductions, either here or at
diff --git a/src/backend/optimizer/path/meson.build b/src/backend/optimizer/path/meson.build
index 98f3ebd5192..7d480656ce6 100644
--- a/src/backend/optimizer/path/meson.build
+++ b/src/backend/optimizer/path/meson.build
@@ -10,4 +10,5 @@ backend_sources += files(
   'joinrels.c',
   'pathkeys.c',
   'tidpath.c',
+  'uniquekeys.c',
 )
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 5eb71635d15..eac8d029ddf 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -35,7 +35,6 @@ static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
 static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
 											 RelOptInfo *partrel,
 											 int partkeycol);
-static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
 
 
@@ -1248,7 +1247,7 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
  * We need this to ensure that we don't return pathkeys describing values
  * that are unavailable above the level of the subquery scan.
  */
-static Var *
+Var *
 find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle)
 {
 	ListCell   *lc;
diff --git a/src/backend/optimizer/path/uniquekeys.c b/src/backend/optimizer/path/uniquekeys.c
new file mode 100644
index 00000000000..91be8a7f6ec
--- /dev/null
+++ b/src/backend/optimizer/path/uniquekeys.c
@@ -0,0 +1,1296 @@
+/*-------------------------------------------------------------------------
+ *
+ * uniquekeys.c
+ *	  Routines to deduce what expression sets a relation is distinct over
+ *
+ * A relation's UniqueKeys record the expression sets over which its output
+ * is distinct.  They let us elide a redundant DISTINCT or GROUP BY step and
+ * detect unique joins (inner_unique).
+ *
+ * This file populates the keys of base relations, join relations, and upper
+ * relations, and provides the lookups by which the various consumers answer
+ * specific distinctness questions.
+ *
+ * See src/backend/optimizer/README for a great deal of information about
+ * the nature and use of unique keys.
+ *
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/path/uniquekeys.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/sysattr.h"
+#include "nodes/makefuncs.h"
+#include "nodes/multibitmapset.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/pathnode.h"
+#include "optimizer/paths.h"
+#include "optimizer/planmain.h"
+#include "parser/parse_agg.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteManip.h"
+#include "utils/lsyscache.h"
+
+/* Arbitrary cap on the number of UniqueKeys tracked per relation */
+#define MAX_UNIQUEKEYS			8
+
+static void populate_plain_rel_uniquekeys(PlannerInfo *root,
+										  RelOptInfo *rel);
+static void populate_subquery_rel_uniquekeys(PlannerInfo *root,
+											 RelOptInfo *rel);
+static void translate_subquery_uniquekeys(PlannerInfo *root, RelOptInfo *rel);
+static bool add_subquery_key_col(PlannerInfo *root, RelOptInfo *rel,
+								 SortGroupClause *sgc, TargetEntry *tle,
+								 Bitmapset **key_ecs);
+static void strengthen_uniquekeys_for_inner_join(PlannerInfo *root,
+												 RelOptInfo *joinrel,
+												 List *restrictlist);
+static bool uniquekey_forced_nonnullable(PlannerInfo *root, UniqueKey *ukey,
+										 List *nonnullable_vars);
+static bool var_in_nonnullable_vars(Var *var, List *nonnullable_vars);
+static Bitmapset *get_interesting_unique_ecs(PlannerInfo *root);
+static Bitmapset *add_ojclause_ecs(PlannerInfo *root, Bitmapset *result,
+								   List *ojclauses);
+static Bitmapset *add_base_ec_positions(PlannerInfo *root, Bitmapset *result,
+										EquivalenceClass *ec);
+static bool collect_clause_ecs(PlannerInfo *root, List *clause,
+							   Bitmapset **ecs_p);
+static void add_uniquekey(RelOptInfo *rel, Bitmapset *eclass_indexes,
+						  bool nullable);
+static bool rel_has_uniquekey_within(RelOptInfo *rel, Bitmapset *ecs,
+									 bool null_aware);
+static bool side_is_unique(PlannerInfo *root, RelOptInfo *joinrel,
+						   RelOptInfo *side, RelOptInfo *otherside,
+						   JoinType jointype, List *restrictlist);
+static int	find_ec_position(PlannerInfo *root, Expr *expr,
+							 List *opfamilies, Oid collation,
+							 Bitmapset *candidates);
+static int	base_ec_position(PlannerInfo *root, EquivalenceClass *ec);
+
+
+/*
+ * populate_baserel_uniquekeys
+ *		Deduce the unique keys of a base relation's output.
+ *
+ * This is called once per base rel, after its restriction clauses and (for
+ * plain relations) index information have been set up.
+ */
+void
+populate_baserel_uniquekeys(PlannerInfo *root, RelOptInfo *rel)
+{
+	Assert(rel->reloptkind == RELOPT_BASEREL);
+
+	if (rel->rtekind == RTE_RELATION)
+		populate_plain_rel_uniquekeys(root, rel);
+	else if (rel->rtekind == RTE_SUBQUERY)
+		populate_subquery_rel_uniquekeys(root, rel);
+	/* We have no deduction rules for other rtekinds */
+}
+
+/*
+ * populate_plain_rel_uniquekeys
+ *		Deduce unique keys for a plain relation from its unique indexes.
+ */
+static void
+populate_plain_rel_uniquekeys(PlannerInfo *root, RelOptInfo *rel)
+{
+	Bitmapset  *interesting = get_interesting_unique_ecs(root);
+	List	   *nonnullable_vars = NIL;
+	bool		nonnullable_valid = false;
+
+	if (bms_is_empty(interesting))
+		return;
+
+	foreach_node(IndexOptInfo, ind, rel->indexlist)
+	{
+		Bitmapset  *key_ecs = NULL;
+		bool		nullable = false;
+		bool		useless = false;
+		int			c;
+
+		/*
+		 * If the index is not unique, or not immediately enforced, or if it's
+		 * a partial index, it's useless here.
+		 */
+		if (!ind->unique || !ind->immediate || ind->indpred != NIL)
+			continue;
+
+		for (c = 0; c < ind->nkeycolumns; c++)
+		{
+			Expr	   *colexpr;
+			EquivalenceClass *ec;
+			int			pos;
+
+			colexpr = list_nth_node(TargetEntry, ind->indextlist, c)->expr;
+
+			/*
+			 * Find the EC mentioning this column.  A column no suitable EC
+			 * mentions cannot be referenced by any consumer, so such an index
+			 * gives no usable key.
+			 */
+			pos = find_ec_position(root, colexpr,
+								   list_make1_oid(ind->opfamily[c]),
+								   ind->indexcollations[c],
+								   rel->eclass_indexes);
+			if (pos < 0)
+			{
+				useless = true;
+				break;
+			}
+			ec = (EquivalenceClass *) list_nth(root->eq_classes, pos);
+
+			/*
+			 * A column equated to a constant can be left out of the key;
+			 * uniqueness is then guaranteed by the remaining columns.
+			 */
+			if (ec->ec_has_const)
+				continue;
+
+			if (!bms_is_member(pos, interesting))
+			{
+				useless = true;
+				break;
+			}
+
+			key_ecs = bms_add_member(key_ecs, pos);
+
+			/*
+			 * The column might be NULL, unless a NOT NULL constraint or some
+			 * strict restriction clause says otherwise; a NULLS NOT DISTINCT
+			 * index enforces uniqueness even among NULLs, though.
+			 */
+			if (!nullable && !ind->nullsnotdistinct)
+			{
+				if (!nonnullable_valid)
+				{
+					foreach_node(RestrictInfo, rinfo, rel->baserestrictinfo)
+					{
+						nonnullable_vars =
+							mbms_add_members(nonnullable_vars,
+											 find_nonnullable_vars((Node *) rinfo->clause));
+					}
+					nonnullable_valid = true;
+				}
+
+				if (!IsA(colexpr, Var) ||
+					(!var_is_nonnullable(root, (Var *) colexpr, NOTNULL_SOURCE_RELOPT) &&
+					 !var_in_nonnullable_vars((Var *) colexpr, nonnullable_vars)))
+					nullable = true;
+			}
+		}
+
+		if (!useless)
+			add_uniquekey(rel, key_ecs, nullable);
+	}
+}
+
+/*
+ * populate_subquery_rel_uniquekeys
+ *		Deduce unique keys for a subquery RTE relation, in the same spirit as
+ *		query_is_distinct_for().
+ */
+static void
+populate_subquery_rel_uniquekeys(PlannerInfo *root, RelOptInfo *rel)
+{
+	Query	   *subquery = planner_rt_fetch(rel->relid, root)->subquery;
+	Bitmapset  *interesting = get_interesting_unique_ecs(root);
+	Bitmapset  *key_ecs = NULL;
+
+	if (bms_is_empty(interesting))
+		return;
+
+	/*
+	 * Most of a subquery's distinctness is recorded in the UniqueKeys of its
+	 * final relation, so we translate those into outer representation.
+	 */
+	translate_subquery_uniquekeys(root, rel);
+
+	/*
+	 * A tlist SRF is expanded above the set operation below and can duplicate
+	 * its rows, which would void the whole-row key we deduce there, so skip
+	 * when the subquery has one.
+	 */
+	if (subquery->hasTargetSRFs)
+		return;
+
+	/*
+	 * A set operation is the one remaining case with no UniqueKey built for
+	 * the subquery's final relation, so record that here.
+	 */
+	if (subquery->setOperations)
+	{
+		/*
+		 * UNION, INTERSECT, EXCEPT guarantee uniqueness of the whole output
+		 * row, except with ALL.
+		 */
+		SetOperationStmt *topop = castNode(SetOperationStmt,
+										   subquery->setOperations);
+		ListCell   *lg;
+
+		if (topop->all)
+			return;
+
+		lg = list_head(topop->groupClauses);
+		foreach_node(TargetEntry, tle, subquery->targetList)
+		{
+			SortGroupClause *sgc;
+
+			if (tle->resjunk)
+				continue;
+
+			Assert(lg != NULL);
+			sgc = (SortGroupClause *) lfirst(lg);
+			lg = lnext(topop->groupClauses, lg);
+
+			if (!add_subquery_key_col(root, rel, sgc, tle, &key_ecs))
+				return;
+		}
+
+		/* The whole-row key is NULL-aware, so non-nullable */
+		add_uniquekey(rel, key_ecs, false);
+	}
+}
+
+/*
+ * add_subquery_key_col
+ *		Add the EC of one deduplicated subquery output column to the key being
+ *		built; returns false if the column makes a key impossible.
+ */
+static bool
+add_subquery_key_col(PlannerInfo *root, RelOptInfo *rel,
+					 SortGroupClause *sgc, TargetEntry *tle,
+					 Bitmapset **key_ecs)
+{
+	List	   *opfamilies = get_mergejoin_opfamilies(sgc->eqop);
+	Oid			collation = exprCollation((Node *) tle->expr);
+	Var		   *colvar;
+	EquivalenceClass *ec;
+	int			pos;
+
+	/* a resjunk column is not visible to the outer query */
+	if (tle->resjunk)
+		return false;
+
+	/* if the equality operator isn't btree equality, no EC can match */
+	if (opfamilies == NIL)
+		return false;
+
+	/* Find the EC mentioning the corresponding output Var */
+	colvar = makeVar(rel->relid, tle->resno,
+					 exprType((Node *) tle->expr),
+					 exprTypmod((Node *) tle->expr),
+					 collation, 0);
+	pos = find_ec_position(root, (Expr *) colvar, opfamilies, collation,
+						   rel->eclass_indexes);
+	if (pos < 0)
+		return false;
+	ec = (EquivalenceClass *) list_nth(root->eq_classes, pos);
+
+	if (ec->ec_has_const)
+		return true;
+
+	if (!bms_is_member(pos, get_interesting_unique_ecs(root)))
+		return false;
+
+	*key_ecs = bms_add_member(*key_ecs, pos);
+	return true;
+}
+
+/*
+ * translate_subquery_uniquekeys
+ *		Build an uniquekeys list that describes the distinctness of a
+ *		subquery's result, in the terms of the outer query.  This is
+ *		essentially a task of conversion.
+ *
+ * The subquery's final relation carries UniqueKeys over the subquery's own
+ * ECs.  For each such key we map every EC to the parent EC of the
+ * corresponding output column and, if all of them map into the parent's
+ * interesting set, record the translated key on "rel".
+ */
+static void
+translate_subquery_uniquekeys(PlannerInfo *root, RelOptInfo *rel)
+{
+	PlannerInfo *subroot = rel->subroot;
+	RelOptInfo *subfinal;
+	List	   *subquery_tlist;
+	Bitmapset  *interesting;
+
+	if (subroot == NULL)
+		return;
+
+	subfinal = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
+	if (subfinal->uniquekeys == NIL)
+		return;
+
+	subquery_tlist = subroot->processed_tlist;
+	interesting = get_interesting_unique_ecs(root);
+
+	foreach_node(UniqueKey, subkey, subfinal->uniquekeys)
+	{
+		Bitmapset  *parent_ecs = NULL;
+		bool		ok = true;
+		int			i = -1;
+
+		while ((i = bms_next_member(subkey->eclass_indexes, i)) >= 0)
+		{
+			EquivalenceClass *sub_ec = list_nth_node(EquivalenceClass,
+													 subroot->eq_classes, i);
+			EquivalenceClass *outer_ec = NULL;
+
+			/*
+			 * Find the parent EC that represents this subquery EC, reached
+			 * through one of the subquery's output columns.  This is the
+			 * non-volatile arm of convert_subquery_pathkeys(): any matching
+			 * outer EC will do, since all of an EC's members are equal, so
+			 * uniqueness over one is uniqueness over any.  A volatile EC
+			 * can't be a key and won't match, so we don't bother searching
+			 * for it.
+			 */
+			if (!sub_ec->ec_has_volatile)
+			{
+				foreach_node(EquivalenceMember, sub_member, sub_ec->ec_members)
+				{
+					Expr	   *sub_expr = sub_member->em_expr;
+					Oid			sub_expr_type = sub_member->em_datatype;
+					Oid			sub_expr_coll = sub_ec->ec_collation;
+
+					/* Child members should not exist in ec_members */
+					Assert(!sub_member->em_is_child);
+
+					foreach_node(TargetEntry, tle, subquery_tlist)
+					{
+						Var		   *outer_var;
+						Expr	   *tle_expr;
+
+						/* Is the TLE actually available to the outer query? */
+						outer_var = find_var_for_subquery_tle(rel, tle);
+						if (!outer_var)
+							continue;
+
+						/*
+						 * The targetlist entry is considered to match if it
+						 * matches after sort-key canonicalization.  That is
+						 * needed since the sub_expr has been through the same
+						 * process.
+						 */
+						tle_expr = canonicalize_ec_expression(tle->expr,
+															  sub_expr_type,
+															  sub_expr_coll);
+						if (!equal(tle_expr, sub_expr))
+							continue;
+
+						/* See if we have a matching EC for the TLE */
+						outer_ec = get_eclass_for_sort_expr(root,
+															(Expr *) outer_var,
+															sub_ec->ec_opfamilies,
+															sub_expr_type,
+															sub_expr_coll,
+															0,
+															rel->relids,
+															false);
+
+						if (outer_ec)
+							break;
+					}
+
+					if (outer_ec)
+						break;
+				}
+			}
+
+			/* It must map to an EC the parent tracks keys over */
+			if (outer_ec == NULL ||
+				!bms_is_member(outer_ec->ec_index, interesting))
+			{
+				ok = false;
+				break;
+			}
+
+			parent_ecs = bms_add_member(parent_ecs, outer_ec->ec_index);
+		}
+
+		/*
+		 * An empty subkey (the subquery emits at most one row) translates to
+		 * an empty parent key, which the loop above yields with ok == true.
+		 */
+		if (ok)
+			add_uniquekey(rel, parent_ecs, subkey->nullable);
+	}
+}
+
+/*
+ * populate_joinrel_uniquekeys
+ *		Deduce the unique keys of a join relation's output from those of its
+ *		input relations.
+ *
+ * This is called just once per joinrel, with whichever pair of input relations
+ * the joinrel happens to be built from first.  Uniqueness is a property of the
+ * joinrel's output, so it is independent of the pair used to prove it; other
+ * pairs might prove additional keys, but chasing those isn't worth the cycles.
+ */
+void
+populate_joinrel_uniquekeys(PlannerInfo *root, RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							SpecialJoinInfo *sjinfo,
+							List *restrictlist)
+{
+	JoinType	jointype = sjinfo->jointype;
+	RelOptInfo *lhs = outerrel;
+	RelOptInfo *rhs = innerrel;
+
+	/* A full join null-extends both sides; nothing survives. */
+	if (jointype == JOIN_FULL)
+		return;
+
+	/* Nothing to derive if neither input has keys */
+	if (outerrel->uniquekeys == NIL && innerrel->uniquekeys == NIL)
+		return;
+
+	/* Identify the special join's LHS */
+	if (jointype != JOIN_INNER)
+	{
+		if (bms_is_subset(sjinfo->min_lefthand, outerrel->relids) &&
+			bms_is_subset(sjinfo->min_righthand, innerrel->relids))
+		{
+			lhs = outerrel;
+			rhs = innerrel;
+		}
+		else if (bms_is_subset(sjinfo->min_lefthand, innerrel->relids) &&
+				 bms_is_subset(sjinfo->min_righthand, outerrel->relids))
+		{
+			lhs = innerrel;
+			rhs = outerrel;
+		}
+		else
+		{
+			/*
+			 * The special join is not applied at this join: a semijoin RHS
+			 * unique-ified and joined to only part of its LHS (see
+			 * join_is_legal).  We could derive keys from the unique-ified
+			 * rel's distinctness, but don't bother.
+			 */
+			Assert(jointype == JOIN_SEMI);
+			return;
+		}
+	}
+
+	/*
+	 * Preservation of the LHS keys: unconditional for a semi/antijoin, else
+	 * the RHS must be unique for the clauses so each LHS row yields one join
+	 * row.  A lateral reference into the RHS re-executes the LHS per RHS row,
+	 * voiding this.
+	 */
+	if (lhs->uniquekeys != NIL &&
+		!bms_overlap(lhs->lateral_relids, rhs->relids) &&
+		((jointype == JOIN_SEMI || jointype == JOIN_ANTI) ||
+		 side_is_unique(root, joinrel, rhs, lhs, jointype, restrictlist)))
+	{
+		foreach_node(UniqueKey, ukey, lhs->uniquekeys)
+		{
+			add_uniquekey(joinrel, ukey->eclass_indexes, ukey->nullable);
+		}
+	}
+
+	/* The RHS keys are not usable above a semi/anti join */
+	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+		return;
+
+	/*
+	 * Preservation of the RHS keys, if the LHS is unique for the clauses.
+	 * Across a left join they become nullable, and an empty key does not
+	 * survive null-extension (one null-extended row per unmatched LHS row).
+	 */
+	if (rhs->uniquekeys != NIL &&
+		!bms_overlap(rhs->lateral_relids, lhs->relids) &&
+		side_is_unique(root, joinrel, lhs, rhs, jointype, restrictlist))
+	{
+		foreach_node(UniqueKey, ukey, rhs->uniquekeys)
+		{
+			if (jointype == JOIN_INNER)
+				add_uniquekey(joinrel, ukey->eclass_indexes, ukey->nullable);
+			else if (!bms_is_empty(ukey->eclass_indexes))
+				add_uniquekey(joinrel, ukey->eclass_indexes, true);
+		}
+	}
+
+	/* Combination: the union of one key from each side is a key */
+	foreach_node(UniqueKey, lkey, lhs->uniquekeys)
+	{
+		foreach_node(UniqueKey, rkey, rhs->uniquekeys)
+		{
+			add_uniquekey(joinrel,
+						  bms_union(lkey->eclass_indexes,
+									rkey->eclass_indexes),
+						  lkey->nullable || rkey->nullable);
+		}
+	}
+
+	/* Strengthen nullable keys the inner join's clauses force non-NULL */
+	if (jointype == JOIN_INNER)
+		strengthen_uniquekeys_for_inner_join(root, joinrel, restrictlist);
+}
+
+/*
+ * strengthen_uniquekeys_for_inner_join
+ *		Flip a joinrel's nullable keys to non-nullable where the inner join's
+ *		own clauses force their columns non-NULL.
+ *
+ * An inner join applies each clause to every output row, so any Var a strict
+ * clause requires non-NULL is non-NULL throughout the output.  If every EC of
+ * a nullable key holds such a Var, no output row has a NULL key column, so the
+ * key is really NULL-aware.  Outer-join clauses are not among these, so a key
+ * nullable from null-extension is left alone.
+ */
+static void
+strengthen_uniquekeys_for_inner_join(PlannerInfo *root, RelOptInfo *joinrel,
+									 List *restrictlist)
+{
+	List	   *nonnullable_vars = NIL;
+
+	/* Which Vars do this inner join's clauses force non-NULL? */
+	foreach_node(RestrictInfo, rinfo, restrictlist)
+	{
+		nonnullable_vars =
+			mbms_add_members(nonnullable_vars,
+							 find_nonnullable_vars((Node *) rinfo->clause));
+	}
+
+	if (nonnullable_vars == NIL)
+		return;
+
+	foreach_node(UniqueKey, ukey, joinrel->uniquekeys)
+	{
+		/* Only a nullable key that has columns can be strengthened */
+		if (!ukey->nullable || bms_is_empty(ukey->eclass_indexes))
+			continue;
+
+		if (uniquekey_forced_nonnullable(root, ukey, nonnullable_vars))
+			ukey->nullable = false;
+	}
+}
+
+/*
+ * uniquekey_forced_nonnullable
+ *		Does every EquivalenceClass of ukey hold a Var that nonnullable_vars
+ *		forces non-NULL?
+ */
+static bool
+uniquekey_forced_nonnullable(PlannerInfo *root, UniqueKey *ukey,
+							 List *nonnullable_vars)
+{
+	int			i = -1;
+
+	while ((i = bms_next_member(ukey->eclass_indexes, i)) >= 0)
+	{
+		EquivalenceClass *ec = list_nth_node(EquivalenceClass,
+											 root->eq_classes, i);
+		ListCell   *lc;
+
+		/* The EC must hold a Var some clause forces non-NULL */
+		foreach(lc, ec->ec_members)
+		{
+			EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
+
+			if (IsA(em->em_expr, Var) &&
+				var_in_nonnullable_vars((Var *) em->em_expr, nonnullable_vars))
+				break;
+		}
+		if (lc == NULL)
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * var_in_nonnullable_vars
+ *		Is "var" listed in the "nonnullable_vars" multibitmapset?
+ */
+static bool
+var_in_nonnullable_vars(Var *var, List *nonnullable_vars)
+{
+	/* skip upper-level Vars */
+	if (var->varlevelsup != 0)
+		return false;
+
+	return mbms_is_member(var->varno,
+						  var->varattno - FirstLowInvalidHeapAttributeNumber,
+						  nonnullable_vars);
+}
+
+/*
+ * populate_grouped_rel_uniquekeys
+ *		Deduce the unique keys of the output of the query's grouping step: it
+ *		is NULL-aware distinct over the grouping columns.  With no
+ *		non-redundant grouping columns, at most one row is emitted.
+ *
+ * When the grouping step is a no-op, the output is just the input, and every
+ * key of the input survives.
+ */
+void
+populate_grouped_rel_uniquekeys(PlannerInfo *root, RelOptInfo *grouped_rel,
+								RelOptInfo *input_rel, bool is_noop)
+{
+	Query	   *parse = root->parse;
+	Bitmapset  *key_ecs;
+
+	/* Set-returning functions are expanded above the grouping step */
+	if (parse->hasTargetSRFs)
+		return;
+
+	if (parse->groupingSets)
+	{
+		/*
+		 * Grouping sets emit one row per set, so the output is distinct only
+		 * when a single (necessarily empty) set remains, or GROUP BY DISTINCT
+		 * collapses them to one; grouping sets over expressions (groupClause
+		 * set) are not distinct.  Either way the result is one row: an empty
+		 * key.
+		 */
+		if (parse->groupClause ||
+			(!parse->groupDistinct &&
+			 list_length(parse->groupingSets) != 1))
+			return;
+
+		add_uniquekey(grouped_rel, NULL, false);
+		return;
+	}
+
+	/* Carry over the input's keys if grouping does no work */
+	if (is_noop)
+	{
+		foreach_node(UniqueKey, ukey, input_rel->uniquekeys)
+		{
+			add_uniquekey(grouped_rel, ukey->eclass_indexes, ukey->nullable);
+		}
+	}
+
+	/* Collect the ECs of the query's grouping columns */
+	if (!collect_clause_ecs(root, root->processed_groupClause, &key_ecs))
+		return;
+
+	if (!bms_is_subset(key_ecs, get_interesting_unique_ecs(root)))
+		return;
+
+	add_uniquekey(grouped_rel, key_ecs, false);
+}
+
+/*
+ * populate_distinct_rel_uniquekeys
+ *		Deduce the unique keys of the output of the query's DISTINCT step: it
+ *		is NULL-aware distinct over the DISTINCT (or DISTINCT ON) columns.
+ *
+ * Like grouping, when the DISTINCT step is a no-op, the output is just the
+ * input, and every key of the input survives.
+ *
+ * Unlike grouping, DISTINCT is applied after target-list SRF expansion, so its
+ * output is distinct over the clause columns even when the tlist has SRFs.
+ */
+void
+populate_distinct_rel_uniquekeys(PlannerInfo *root, RelOptInfo *distinct_rel,
+								 RelOptInfo *input_rel, bool is_noop)
+{
+	Bitmapset  *key_ecs;
+
+	/*
+	 * Nothing within a query level consumes distinctness after the DISTINCT
+	 * step, so these keys are useful only to a parent query.  Don't bother
+	 * unless we are a subquery.
+	 */
+	if (root->query_level <= 1)
+		return;
+
+	/* Carry over the input's keys if DISTINCT does no work */
+	if (is_noop)
+	{
+		foreach_node(UniqueKey, ukey, input_rel->uniquekeys)
+		{
+			add_uniquekey(distinct_rel, ukey->eclass_indexes, ukey->nullable);
+		}
+	}
+
+	/* Collect the ECs of the query's DISTINCT columns */
+	if (!collect_clause_ecs(root, root->processed_distinctClause, &key_ecs))
+		return;
+
+	if (!bms_is_subset(key_ecs, get_interesting_unique_ecs(root)))
+		return;
+
+	add_uniquekey(distinct_rel, key_ecs, false);
+}
+
+/*
+ * populate_unique_rel_uniquekeys
+ *		Deduce the unique keys established by unique-ifying a semijoin's RHS.
+ *
+ * Like grouping, when the unique-ification step is a no-op, the output is just
+ * the input, and every key of the input survives.
+ */
+void
+populate_unique_rel_uniquekeys(PlannerInfo *root, RelOptInfo *unique_rel,
+							   RelOptInfo *input_rel, SpecialJoinInfo *sjinfo,
+							   bool is_noop)
+{
+	Bitmapset  *key_ecs = NULL;
+	ListCell   *lc1;
+	ListCell   *lc2;
+
+	Assert(sjinfo->jointype == JOIN_SEMI);
+
+	/* Carry over the input's keys if DISTINCT does no work */
+	if (is_noop)
+	{
+		foreach_node(UniqueKey, ukey, input_rel->uniquekeys)
+		{
+			add_uniquekey(unique_rel, ukey->eclass_indexes, ukey->nullable);
+		}
+	}
+
+	/*
+	 * Unique-ification deduplicates the RHS over the semijoin's right-hand
+	 * expressions, keeping one row per group (NULL-aware, like GROUP BY), so
+	 * the result is non-nullable distinct over those expressions.
+	 */
+	forboth(lc1, sjinfo->semi_rhs_exprs, lc2, sjinfo->semi_operators)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc1);
+		List	   *opfamilies = get_mergejoin_opfamilies(lfirst_oid(lc2));
+		Oid			collation = exprCollation((Node *) expr);
+		EquivalenceClass *ec;
+		int			pos;
+
+		if (opfamilies == NIL)
+			continue;
+
+		/* Match in base-EC space: strip any outer-join nulling first */
+		expr = (Expr *) remove_nulling_relids((Node *) expr,
+											  root->outer_join_rels, NULL);
+		pos = find_ec_position(root, expr, opfamilies, collation, NULL);
+		if (pos < 0)
+			return;
+
+		ec = list_nth_node(EquivalenceClass, root->eq_classes, pos);
+
+		/* A column equated to a constant is not needed in the key */
+		if (ec->ec_has_const)
+			continue;
+
+		key_ecs = bms_add_member(key_ecs, pos);
+	}
+
+	add_uniquekey(unique_rel, key_ecs, false);
+}
+
+/*
+ * uniquekeys_distinct_is_noop
+ *		Is the input relation of the query's DISTINCT step already provably
+ *		distinct over the DISTINCT columns, making the step a no-op?
+ */
+bool
+uniquekeys_distinct_is_noop(PlannerInfo *root, RelOptInfo *input_rel)
+{
+	Query	   *parse = root->parse;
+	Bitmapset  *covered = NULL;
+
+	if (input_rel->uniquekeys == NIL)
+		return false;
+
+	if (parse->hasDistinctOn)
+		return false;
+
+	if (parse->hasTargetSRFs)
+		return false;
+
+	/* Collect the DISTINCT columns' ECs */
+	foreach_node(PathKey, pathkey, root->distinct_pathkeys)
+	{
+		int			pos = base_ec_position(root, pathkey->pk_eclass);
+
+		if (pos >= 0)
+			covered = bms_add_member(covered, pos);
+	}
+
+	/* DISTINCT considers NULLs equal, so only NULL-aware keys qualify */
+	return rel_has_uniquekey_within(input_rel, covered, true);
+}
+
+/*
+ * uniquekeys_grouping_is_noop
+ *		Is the input relation of the query's grouping step already provably
+ *		distinct over the grouping columns, i.e. does every group consist of
+ *		exactly one row?
+ */
+bool
+uniquekeys_grouping_is_noop(PlannerInfo *root, RelOptInfo *input_rel)
+{
+	Query	   *parse = root->parse;
+	Bitmapset  *covered = NULL;
+	ListCell   *lc;
+
+	if (input_rel->uniquekeys == NIL)
+		return false;
+
+	if (parse->groupClause == NIL ||
+		parse->groupingSets != NIL ||
+		parse->hasAggs ||
+		parse->havingQual != NULL)
+		return false;
+
+	/* Collect the grouping columns' ECs from the leading pathkeys */
+	foreach(lc, root->group_pathkeys)
+	{
+		int			pos;
+
+		/* Only the leading num_groupby_pathkeys are grouping columns */
+		if (foreach_current_index(lc) >= root->num_groupby_pathkeys)
+			break;
+
+		pos = base_ec_position(root, ((PathKey *) lfirst(lc))->pk_eclass);
+		if (pos >= 0)
+			covered = bms_add_member(covered, pos);
+	}
+
+	/* like DISTINCT, grouping considers NULLs equal */
+	return rel_has_uniquekey_within(input_rel, covered, true);
+}
+
+/*
+ * uniquekeys_match_join_clauses
+ *		Do the given join clauses prove the relation distinct, i.e. do their
+ *		rel-side expressions cover some unique key of it?
+ *
+ * The caller must have already determined that each clause is a mergejoinable
+ * equality with an expression of this relation on one side, with the transient
+ * outer_is_left flags set accordingly; this matches the clause lists built for
+ * rel_is_distinct_for().
+ */
+bool
+uniquekeys_match_join_clauses(PlannerInfo *root, RelOptInfo *rel,
+							  List *clause_list)
+{
+	Bitmapset  *covered = NULL;
+
+	if (rel->uniquekeys == NIL)
+		return false;
+
+	foreach_node(RestrictInfo, rinfo, clause_list)
+	{
+		EquivalenceClass *ec;
+		int			pos;
+
+		/* caller identified the inner side for us */
+		ec = rinfo->outer_is_left ? rinfo->right_ec : rinfo->left_ec;
+		if (ec && (pos = base_ec_position(root, ec)) >= 0)
+			covered = bms_add_member(covered, pos);
+	}
+
+	/*
+	 * Nullable keys are fine here: rows with NULL key values cannot match any
+	 * outer row through these strict clauses.
+	 */
+	return rel_has_uniquekey_within(rel, covered, false);
+}
+
+/*
+ * uniquekeys_uniquification_is_noop
+ *		Would unique-ifying the given semijoin RHS relation be a no-op, because
+ *		its output is already provably distinct over the unique-ification
+ *		columns?
+ *
+ * Nullable keys are acceptable: the semijoin operators are strict, so rows
+ * with NULL unique-ification columns match nothing, and not collapsing their
+ * duplicates cannot change the result.
+ */
+bool
+uniquekeys_uniquification_is_noop(PlannerInfo *root, RelOptInfo *rel,
+								  SpecialJoinInfo *sjinfo)
+{
+	Bitmapset  *covered = NULL;
+	ListCell   *lc1;
+	ListCell   *lc2;
+
+	Assert(sjinfo->jointype == JOIN_SEMI);
+
+	if (rel->uniquekeys == NIL)
+		return false;
+
+	/*
+	 * Collect the unique-ification columns' ECs.  Columns with no matching EC
+	 * are simply left out: unique-ification groups by all the columns and
+	 * keeps one row per group, and a rel distinct over a subset of them is
+	 * necessarily distinct over the full set.  So a key covering any subset
+	 * of the columns already proves the step a no-op.
+	 */
+	forboth(lc1, sjinfo->semi_rhs_exprs, lc2, sjinfo->semi_operators)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc1);
+		List	   *opfamilies = get_mergejoin_opfamilies(lfirst_oid(lc2));
+		Oid			collation = exprCollation((Node *) expr);
+		int			pos;
+
+		if (opfamilies == NIL)
+			continue;
+		/* Match in base-EC space: strip any outer-join nulling first */
+		expr = (Expr *) remove_nulling_relids((Node *) expr,
+											  root->outer_join_rels, NULL);
+		pos = find_ec_position(root, expr, opfamilies, collation, NULL);
+		if (pos >= 0)
+			covered = bms_add_member(covered, pos);
+	}
+
+	return rel_has_uniquekey_within(rel, covered, false);
+}
+
+/*
+ * get_interesting_unique_ecs
+ *		Determine which ECs are worth tracking in UniqueKeys: those of the
+ *		DISTINCT clause, of the GROUP BY clause when its step might be a no-op,
+ *		and those that can generate join clauses.  The last group is the
+ *		multi-relation ECs, plus the single-member ECs on each side of a
+ *		mergejoinable outer-join clause; the latter never merge into a join EC
+ *		of their own, but inner_unique proofs look them up, so we track them
+ *		too.
+ *
+ * Each contributing EC is recorded as its base (un-nulled) position via
+ * add_base_ec_positions, which may materialize a base EC on demand; the
+ * resulting set therefore contains only base-EC positions.
+ *
+ * Computed once and cached.  ECs created later (e.g. for sort pathkeys) are
+ * never interesting, which is fine: keys come from clauses and pathkeys that
+ * predate the join search.
+ */
+static Bitmapset *
+get_interesting_unique_ecs(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	Bitmapset  *result = NULL;
+	int			necs;
+	int			i;
+
+	if (root->interesting_unique_ecs_valid)
+		return root->interesting_unique_ecs;
+
+	/* The multi-relation ECs, i.e. those that can generate join clauses */
+	necs = list_length(root->eq_classes);
+	for (i = 0; i < necs; i++)
+	{
+		EquivalenceClass *ec = list_nth_node(EquivalenceClass,
+											 root->eq_classes, i);
+		Relids		base_relids;
+
+		if (ec->ec_has_volatile)
+			continue;
+
+		base_relids = bms_difference(ec->ec_relids, root->outer_join_rels);
+
+		if (bms_membership(base_relids) == BMS_MULTIPLE)
+			result = add_base_ec_positions(root, result, ec);
+
+		bms_free(base_relids);
+	}
+
+	/* The DISTINCT columns */
+	foreach_node(PathKey, pathkey, root->distinct_pathkeys)
+	{
+		result = add_base_ec_positions(root, result, pathkey->pk_eclass);
+	}
+
+	/* The GROUP BY columns, when the grouping step might be a no-op */
+	if (parse->groupClause != NIL &&
+		parse->groupingSets == NIL &&
+		!parse->hasAggs &&
+		parse->havingQual == NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, root->group_pathkeys)
+		{
+			/* Only the leading num_groupby_pathkeys are grouping columns */
+			if (foreach_current_index(lc) >= root->num_groupby_pathkeys)
+				break;
+
+			result = add_base_ec_positions(root, result,
+										   ((PathKey *) lfirst(lc))->pk_eclass);
+		}
+	}
+
+	/* Both sides of each mergejoinable outer-join clause */
+	result = add_ojclause_ecs(root, result, root->left_join_clauses);
+	result = add_ojclause_ecs(root, result, root->right_join_clauses);
+	result = add_ojclause_ecs(root, result, root->full_join_clauses);
+
+	root->interesting_unique_ecs = result;
+	root->interesting_unique_ecs_valid = true;
+
+	return result;
+}
+
+/*
+ * add_ojclause_ecs
+ *		Subroutine for get_interesting_unique_ecs: add the ECs of both sides of
+ *		the given mergejoinable outer-join clauses.
+ */
+static Bitmapset *
+add_ojclause_ecs(PlannerInfo *root, Bitmapset *result, List *ojclauses)
+{
+	foreach_node(OuterJoinClauseInfo, ojcinfo, ojclauses)
+	{
+		RestrictInfo *rinfo = ojcinfo->rinfo;
+
+		if (rinfo->left_ec)
+			result = add_base_ec_positions(root, result, rinfo->left_ec);
+		if (rinfo->right_ec)
+			result = add_base_ec_positions(root, result, rinfo->right_ec);
+	}
+	return result;
+}
+
+/*
+ * add_base_ec_positions
+ *		Subroutine for get_interesting_unique_ecs: add the position(s) of the
+ *		base (un-nulled) EC(s) corresponding to "ec" to "result".
+ *
+ * Keys live in base-EC space, so the interesting set holds base positions.  If
+ * "ec" is already a base EC, we add its own position.  Otherwise we strip the
+ * nullingrels and use get_eclass_for_sort_expr to bring the base EC into
+ * existence if it doesn't already exist, so the base key can later be built
+ * and matched.
+ */
+static Bitmapset *
+add_base_ec_positions(PlannerInfo *root, Bitmapset *result,
+					  EquivalenceClass *ec)
+{
+	/* The passed ec might be non-canonical, so chase up to the top */
+	while (ec->ec_merged)
+		ec = ec->ec_merged;
+
+	/* A const-only EC has no base position */
+	if (bms_is_empty(ec->ec_relids))
+		return result;
+
+	/* If ec is already a base EC, we add its own position */
+	if (!bms_overlap(ec->ec_relids, root->outer_join_rels))
+		return bms_add_member(result, ec->ec_index);
+
+	/* Otherwise materialize the base EC of each nulled member */
+	foreach_node(EquivalenceMember, em, ec->ec_members)
+	{
+		Node	   *base_expr;
+		EquivalenceClass *base_ec;
+
+		if (em->em_is_const ||
+			!bms_overlap(em->em_relids, root->outer_join_rels))
+			continue;
+
+		base_expr = remove_nulling_relids((Node *) em->em_expr,
+										  root->outer_join_rels, NULL);
+		base_ec = get_eclass_for_sort_expr(root, (Expr *) base_expr,
+										   ec->ec_opfamilies,
+										   em->em_datatype,
+										   ec->ec_collation,
+										   0, NULL, true);
+		result = bms_add_member(result, base_ec->ec_index);
+	}
+
+	return result;
+}
+
+/*
+ * collect_clause_ecs
+ *		Find the ECs of the columns of a grouping or DISTINCT clause, omitting
+ *		columns that are equivalent to constants.
+ *
+ * If any column has no matching EC, return false: the caller is building a
+ * rel's key, which must account for all columns.
+ */
+static bool
+collect_clause_ecs(PlannerInfo *root, List *clause, Bitmapset **ecs_p)
+{
+	Bitmapset  *ecs = NULL;
+
+	*ecs_p = NULL;
+
+	foreach_node(SortGroupClause, sgc, clause)
+	{
+		TargetEntry *tle = get_sortgroupclause_tle(sgc,
+												   root->parse->targetList);
+		List	   *opfamilies = get_mergejoin_opfamilies(sgc->eqop);
+		Oid			collation = exprCollation((Node *) tle->expr);
+		EquivalenceClass *ec;
+		int			pos = -1;
+
+		if (opfamilies != NIL)
+		{
+			/* Match in base-EC space: strip any outer-join nulling first */
+			Expr	   *expr;
+
+			expr = (Expr *) remove_nulling_relids((Node *) tle->expr,
+												  root->outer_join_rels, NULL);
+			pos = find_ec_position(root, expr, opfamilies, collation, NULL);
+		}
+		if (pos < 0)
+			return false;
+		ec = (EquivalenceClass *) list_nth(root->eq_classes, pos);
+
+		if (!ec->ec_has_const)
+			ecs = bms_add_member(ecs, pos);
+	}
+
+	*ecs_p = ecs;
+	return true;
+}
+
+/*
+ * add_uniquekey
+ *		Add a UniqueKey to a relation's uniquekeys list, maintaining
+ *		minimality: a key dominated by another one, whose EC set is a
+ *		subset and whose nullability is at least as strong, is never kept.
+ */
+static void
+add_uniquekey(RelOptInfo *rel, Bitmapset *eclass_indexes, bool nullable)
+{
+	UniqueKey  *ukey;
+	ListCell   *lc;
+
+	foreach(lc, rel->uniquekeys)
+	{
+		UniqueKey  *other = lfirst_node(UniqueKey, lc);
+
+		/* Reject the new key if some existing key dominates it */
+		if ((!other->nullable || nullable) &&
+			bms_is_subset(other->eclass_indexes, eclass_indexes))
+			return;
+		/* ... and remove existing keys the new one dominates */
+		if ((!nullable || other->nullable) &&
+			bms_is_subset(eclass_indexes, other->eclass_indexes))
+			rel->uniquekeys = foreach_delete_current(rel->uniquekeys, lc);
+	}
+
+	if (list_length(rel->uniquekeys) >= MAX_UNIQUEKEYS)
+		return;
+
+	ukey = makeNode(UniqueKey);
+	ukey->eclass_indexes = eclass_indexes;
+	ukey->nullable = nullable;
+	rel->uniquekeys = lappend(rel->uniquekeys, ukey);
+}
+
+/*
+ * rel_has_uniquekey_within
+ *		Does the rel have a key whose ECs all appear in "ecs"?  If
+ *		null_aware, only NULL-aware (non-nullable) keys qualify.
+ */
+static bool
+rel_has_uniquekey_within(RelOptInfo *rel, Bitmapset *ecs, bool null_aware)
+{
+	ListCell   *lc;
+
+	foreach(lc, rel->uniquekeys)
+	{
+		UniqueKey  *ukey = lfirst_node(UniqueKey, lc);
+
+		if ((!null_aware || !ukey->nullable) &&
+			bms_is_subset(ukey->eclass_indexes, ecs))
+			return true;
+	}
+	return false;
+}
+
+/*
+ * side_is_unique
+ *		Check whether "side" provably contains at most one row matching any
+ *		given row of "otherside", using the join's restrictlist.
+ */
+static bool
+side_is_unique(PlannerInfo *root, RelOptInfo *joinrel,
+			   RelOptInfo *side, RelOptInfo *otherside,
+			   JoinType jointype, List *restrictlist)
+{
+	/* A one-row rel is trivially unique, even with no join clauses */
+	if (rel_has_uniquekey_within(side, NULL, false))
+		return true;
+
+	return innerrel_is_unique(root, joinrel->relids, otherside->relids,
+							  side, jointype, restrictlist, false);
+}
+
+/*
+ * find_ec_position
+ *		Find the position in root->eq_classes of an EC containing an expression
+ *		matching "expr".  If "candidates" is non-NULL, consider only the ECs at
+ *		those positions.  Returns -1 if there is none.
+ *
+ * Keys live in base-EC space, so a caller matching a reference that an outer
+ * join may have null-extended must first strip its varnullingrels.
+ */
+static int
+find_ec_position(PlannerInfo *root, Expr *expr,
+				 List *opfamilies, Oid collation, Bitmapset *candidates)
+{
+	foreach_node(EquivalenceClass, ec, root->eq_classes)
+	{
+		int			pos = ec->ec_index;
+
+		if (candidates && !bms_is_member(pos, candidates))
+			continue;
+		if (ec->ec_has_volatile ||
+			ec->ec_collation != collation ||
+			!equal(ec->ec_opfamilies, opfamilies))
+			continue;
+		if (find_ec_member_matching_expr(ec, expr, NULL))
+			return pos;
+	}
+	return -1;
+}
+
+/*
+ * base_ec_position
+ *		Find the position of the base (un-nulled) EC corresponding to "ec".
+ *
+ * A consumer's column reference may belong to a nulled EC, while keys are kept
+ * on the base ECs.  Take a usable member of "ec", strip it to base form, and
+ * look that up.  If "ec" is already a base EC, this returns its own position.
+ */
+static int
+base_ec_position(PlannerInfo *root, EquivalenceClass *ec)
+{
+	/* The passed ec might be non-canonical, so chase up to the top */
+	while (ec->ec_merged)
+		ec = ec->ec_merged;
+
+	/* A const-only EC has no base position */
+	if (bms_is_empty(ec->ec_relids))
+		return -1;
+
+	/* If ec is already a base EC, return its own position */
+	if (!bms_overlap(ec->ec_relids, root->outer_join_rels))
+		return ec->ec_index;
+
+	foreach_node(EquivalenceMember, em, ec->ec_members)
+	{
+		Node	   *expr;
+		int			pos;
+
+		if (em->em_is_const || bms_is_empty(em->em_relids))
+			continue;
+
+		expr = (Node *) em->em_expr;
+		if (bms_overlap(em->em_relids, root->outer_join_rels))
+			expr = remove_nulling_relids(expr, root->outer_join_rels, NULL);
+
+		pos = find_ec_position(root, (Expr *) expr, ec->ec_opfamilies,
+							   ec->ec_collation, NULL);
+		if (pos >= 0)
+			return pos;
+	}
+
+	return -1;
+}
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 881950e5264..5511597a997 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -1117,7 +1117,13 @@ reduce_unique_semijoins(PlannerInfo *root)
 static bool
 rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
 {
-	/* We only know about baserels ... */
+	/*
+	 * If UniqueKeys have already been deduced for the rel, distinctness may
+	 * be provable from them.
+	 */
+	if (rel->uniquekeys != NIL)
+		return true;
+	/* Otherwise we only know about baserels ... */
 	if (rel->reloptkind != RELOPT_BASEREL)
 		return false;
 	if (rel->rtekind == RTE_RELATION)
@@ -1179,6 +1185,16 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list,
 					List **extra_clauses)
 {
 	/*
+	 * UniqueKey fast path: if some key's expressions are all covered by the
+	 * clauses, the rel is distinct for them.
+	 */
+	if (extra_clauses == NULL &&
+		uniquekeys_match_join_clauses(root, rel, clause_list))
+		return true;
+
+	/*
+	 * The fallback proofs below handle base relations only.
+	 *
 	 * We could skip a couple of tests here if we assume all callers checked
 	 * rel_supports_distinctness first, but it doesn't seem worth taking any
 	 * risk for.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 3225185d16f..2b8755bcf25 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2157,6 +2157,13 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 	final_rel->useridiscurrent = current_rel->useridiscurrent;
 	final_rel->fdwroutine = current_rel->fdwroutine;
 
+	/*
+	 * LockRows/Limit/ModifyTable never duplicate rows, so the final rel is
+	 * distinct over whatever the current rel was.  Carrying the keys here
+	 * lets a parent query reuse them.
+	 */
+	final_rel->uniquekeys = current_rel->uniquekeys;
+
 	/*
 	 * Generate paths for the final_rel.  Insert all surviving paths, with
 	 * LockRows, Limit, and/or ModifyTable steps added if needed.
@@ -4057,6 +4064,7 @@ create_grouping_paths(PlannerInfo *root,
 	RelOptInfo *grouped_rel;
 	RelOptInfo *partially_grouped_rel;
 	AggClauseCosts agg_costs;
+	bool		grouping_is_noop;
 
 	MemSet(&agg_costs, 0, sizeof(AggClauseCosts));
 	get_agg_clause_costs(root, AGGSPLIT_SIMPLE, &agg_costs);
@@ -4068,14 +4076,38 @@ create_grouping_paths(PlannerInfo *root,
 	grouped_rel = make_grouping_rel(root, input_rel, target,
 									target_parallel_safe, parse->havingQual);
 
-	/*
-	 * Create either paths for a degenerate grouping or paths for ordinary
-	 * grouping, as appropriate.
-	 */
-	if (is_degenerate_grouping(root))
+	grouping_is_noop = uniquekeys_grouping_is_noop(root, input_rel);
+
+	if (grouping_is_noop)
+	{
+		/*
+		 * The input relation is provably distinct over the grouping columns
+		 * and no aggregation work is needed, each input row forms a group all
+		 * by itself, so grouping is a no-op: just use the input paths as they
+		 * are, projecting the desired target.
+		 */
+		foreach_ptr(Path, path, input_rel->pathlist)
+		{
+			/*
+			 * The input paths may already be ProjectionPaths, and we mustn't
+			 * stack another rel's ProjectionPath atop one.  Just project
+			 * directly from the input's subpath.
+			 */
+			if (IsA(path, ProjectionPath))
+				path = ((ProjectionPath *) path)->subpath;
+
+			add_path(grouped_rel, (Path *)
+					 create_projection_path(root, grouped_rel, path, target));
+		}
+	}
+	else if (is_degenerate_grouping(root))
+	{
+		/* Create paths for a degenerate grouping */
 		create_degenerate_grouping_paths(root, input_rel, grouped_rel);
+	}
 	else
 	{
+		/* Create paths for ordinary grouping */
 		int			flags = 0;
 		GroupPathExtraData extra;
 
@@ -4148,6 +4180,11 @@ create_grouping_paths(PlannerInfo *root,
 	}
 
 	set_cheapest(grouped_rel);
+
+	/* Deduce the grouped rel's unique keys */
+	populate_grouped_rel_uniquekeys(root, grouped_rel, input_rel,
+									grouping_is_noop);
+
 	return grouped_rel;
 }
 
@@ -4824,6 +4861,12 @@ create_window_paths(PlannerInfo *root,
 	window_rel->useridiscurrent = input_rel->useridiscurrent;
 	window_rel->fdwroutine = input_rel->fdwroutine;
 
+	/*
+	 * Window functions don't change the set of rows, so uniqueness carries
+	 * over.
+	 */
+	window_rel->uniquekeys = input_rel->uniquekeys;
+
 	/*
 	 * Consider computing window functions starting from the existing
 	 * cheapest-total path (which will likely require a sort) as well as any
@@ -5054,6 +5097,7 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel,
 					  PathTarget *target)
 {
 	RelOptInfo *distinct_rel;
+	bool		distinct_is_noop;
 
 	/* For now, do all work in the (DISTINCT, NULL) upperrel */
 	distinct_rel = fetch_upper_rel(root, UPPERREL_DISTINCT, NULL);
@@ -5075,11 +5119,37 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel,
 	distinct_rel->useridiscurrent = input_rel->useridiscurrent;
 	distinct_rel->fdwroutine = input_rel->fdwroutine;
 
-	/* build distinct paths based on input_rel's pathlist */
-	create_final_distinct_paths(root, input_rel, distinct_rel);
+	distinct_is_noop = uniquekeys_distinct_is_noop(root, input_rel);
 
-	/* now build distinct paths based on input_rel's partial_pathlist */
-	create_partial_distinct_paths(root, input_rel, distinct_rel, target);
+	if (distinct_is_noop)
+	{
+		/*
+		 * The input relation is provably distinct over the DISTINCT columns
+		 * already, so no de-duplication step is required: just use the input
+		 * paths as they are, projecting the desired target.
+		 */
+		foreach_ptr(Path, path, input_rel->pathlist)
+		{
+			/*
+			 * The input paths may already be ProjectionPaths, and we mustn't
+			 * stack another rel's ProjectionPath atop one.  Just project
+			 * directly from the input's subpath.
+			 */
+			if (IsA(path, ProjectionPath))
+				path = ((ProjectionPath *) path)->subpath;
+
+			add_path(distinct_rel, (Path *)
+					 create_projection_path(root, distinct_rel, path, target));
+		}
+	}
+	else
+	{
+		/* build distinct paths based on input_rel's pathlist */
+		create_final_distinct_paths(root, input_rel, distinct_rel);
+
+		/* now build distinct paths based on input_rel's partial_pathlist */
+		create_partial_distinct_paths(root, input_rel, distinct_rel, target);
+	}
 
 	/* Give a helpful error if we failed to create any paths */
 	if (distinct_rel->pathlist == NIL)
@@ -5108,6 +5178,10 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel,
 	/* Now choose the best path(s) */
 	set_cheapest(distinct_rel);
 
+	/* Deduce the distinct rel's unique keys */
+	populate_distinct_rel_uniquekeys(root, distinct_rel, input_rel,
+									 distinct_is_noop);
+
 	return distinct_rel;
 }
 
@@ -5600,6 +5674,11 @@ create_ordered_paths(PlannerInfo *root,
 	ordered_rel->useridiscurrent = input_rel->useridiscurrent;
 	ordered_rel->fdwroutine = input_rel->fdwroutine;
 
+	/*
+	 * Sorting doesn't change the set of rows, so uniqueness carries over.
+	 */
+	ordered_rel->uniquekeys = input_rel->uniquekeys;
+
 	foreach(lc, input_rel->pathlist)
 	{
 		Path	   *input_path = (Path *) lfirst(lc);
@@ -8666,6 +8745,7 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo)
 	List	   *sortPathkeys = NIL;
 	List	   *groupClause = NIL;
 	MemoryContext oldcontext;
+	bool		unique_is_noop;
 
 	/* Caller made a mistake if SpecialJoinInfo is the wrong one */
 	Assert(sjinfo->jointype == JOIN_SEMI);
@@ -8713,6 +8793,11 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo)
 	unique_rel->cheapest_total_path = NULL;
 	unique_rel->cheapest_parameterized_paths = NIL;
 
+	/*
+	 * clear unique keys
+	 */
+	unique_rel->uniquekeys = NIL;
+
 	/*
 	 * Build the target list for the unique rel.  We also build the pathkeys
 	 * that represent the ordering requirements for the sort-based
@@ -8897,17 +8982,52 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo)
 		unique_rel->reltarget = create_pathtarget(root, newtlist);
 	}
 
-	/* build unique paths based on input rel's pathlist */
-	create_final_unique_paths(root, rel, sortPathkeys, groupClause,
-							  sjinfo, unique_rel);
+	unique_is_noop = uniquekeys_uniquification_is_noop(root, rel, sjinfo);
+
+	if (unique_is_noop)
+	{
+		/*
+		 * The input relation is already provably distinct over the columns to
+		 * be unique-ified, so no de-duplication step is required: just use
+		 * the input paths as they are, projecting the new target.
+		 */
+		Path	   *cheapest_input_path = rel->cheapest_total_path;
+
+		unique_rel->rows = rel->rows;
+
+		foreach_ptr(Path, input_path, rel->pathlist)
+		{
+			/*
+			 * Ignore parameterized paths that are not the cheapest-total
+			 * path, as in create_final_unique_paths.
+			 */
+			if (input_path->param_info &&
+				input_path != cheapest_input_path)
+				continue;
+
+			add_path(unique_rel, (Path *)
+					 create_projection_path(root, unique_rel, input_path,
+											unique_rel->reltarget));
+		}
+
+	}
+	else
+	{
+		/* build unique paths based on input rel's pathlist */
+		create_final_unique_paths(root, rel, sortPathkeys, groupClause,
+								  sjinfo, unique_rel);
 
-	/* build unique paths based on input rel's partial_pathlist */
-	create_partial_unique_paths(root, rel, sortPathkeys, groupClause,
-								sjinfo, unique_rel);
+		/* build unique paths based on input rel's partial_pathlist */
+		create_partial_unique_paths(root, rel, sortPathkeys, groupClause,
+									sjinfo, unique_rel);
+	}
 
 	/* Now choose the best path(s) */
 	set_cheapest(unique_rel);
 
+	populate_unique_rel_uniquekeys(root, unique_rel, rel, sjinfo,
+								   unique_is_noop);
+
 	/*
 	 * There shouldn't be any partial paths for the unique relation;
 	 * otherwise, we won't be able to properly guarantee uniqueness.
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 687e923c46c..9e1afe4ecee 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -289,6 +289,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->fdw_private = NULL;
 	rel->unique_for_rels = NIL;
 	rel->non_unique_for_rels = NIL;
+	rel->uniquekeys = NIL;
 	rel->unique_rel = NULL;
 	rel->unique_pathkeys = NIL;
 	rel->unique_groupclause = NIL;
@@ -878,6 +879,7 @@ build_join_rel(PlannerInfo *root,
 	joinrel->fdw_private = NULL;
 	joinrel->unique_for_rels = NIL;
 	joinrel->non_unique_for_rels = NIL;
+	joinrel->uniquekeys = NIL;
 	joinrel->unique_rel = NULL;
 	joinrel->unique_pathkeys = NIL;
 	joinrel->unique_groupclause = NIL;
@@ -988,6 +990,9 @@ build_join_rel(PlannerInfo *root,
 	build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo,
 								 restrictlist);
 
+	populate_joinrel_uniquekeys(root, joinrel, outer_rel, inner_rel,
+								sjinfo, restrictlist);
+
 	/* Add the joinrel to the PlannerInfo. */
 	add_join_rel(root, joinrel);
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c6815b7..6343dd31e65 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -534,6 +534,14 @@ struct PlannerInfo
 	/* set operator pathkeys, if any */
 	List	   *setop_pathkeys;
 
+	/*
+	 * Indexes in PlannerInfo's eq_classes list of ECs that are worth tracking
+	 * in UniqueKeys.
+	 */
+	Bitmapset  *interesting_unique_ecs;
+	/* true once interesting_unique_ecs has been computed */
+	bool		interesting_unique_ecs_valid;
+
 	/* Canonicalised partition schemes used in the query. */
 	List	   *part_schemes pg_node_attr(read_write_ignore);
 
@@ -875,6 +883,10 @@ typedef struct PartitionSchemeData *PartitionScheme;
  *					other rels for which we have tried and failed to prove
  *					this one unique
  *
+ * In addition, uniquekeys is a list of UniqueKey nodes describing expression
+ * sets over which this relation is known to be distinct.  It is populated
+ * bottom-up, for base rels and join rels alike; see uniquekeys.c.
+ *
  * Three fields are used to cache information about unique-ification of this
  * relation.  This is used to support semijoins where the relation appears on
  * the RHS: the relation is first unique-ified, and then a regular join is
@@ -1125,6 +1137,9 @@ typedef struct RelOptInfo
 	/* known not unique for these set(s) */
 	List	   *non_unique_for_rels;
 
+	/* list of UniqueKey: expression sets this rel is distinct over */
+	List	   *uniquekeys;
+
 	/*
 	 * information about unique-ification of this relation
 	 */
@@ -1674,6 +1689,7 @@ typedef struct EquivalenceClass
 	Index		ec_sortref;		/* originating sortclause label, or 0 */
 	Index		ec_min_security;	/* minimum security_level in ec_sources */
 	Index		ec_max_security;	/* maximum security_level in ec_sources */
+	int			ec_index;		/* position of this EC in root->eq_classes */
 	struct EquivalenceClass *ec_merged; /* set if merged into another EC */
 } EquivalenceClass;
 
@@ -1816,6 +1832,32 @@ typedef struct PathKey
 	bool		pk_nulls_first; /* do NULLs come before normal values? */
 } PathKey;
 
+/*
+ * UniqueKeys
+ *
+ * Asserts that a relation cannot emit two rows that are equal on the
+ * values of one expression from each of the EquivalenceClasses identified
+ * by eclass_indexes (indexes into root->eq_classes).  An empty set means
+ * the relation emits at most one row.
+ *
+ * If "nullable" is false, the proof is NULL-aware (no two rows are equal
+ * even treating NULLs as equal), so the key can justify removing a
+ * DISTINCT or GROUP BY step.  If it is true, uniqueness is only guaranteed
+ * among rows whose key expressions are all non-NULL, which still suffices
+ * for proofs based on strict join clauses, such as inner_unique detection.
+ */
+typedef struct UniqueKey
+{
+	pg_node_attr(no_copy_equal, no_read, no_query_jumble)
+
+	NodeTag		type;
+
+	/* indexes into root->eq_classes of the key's expressions */
+	Bitmapset  *eclass_indexes;
+	/* is uniqueness only guaranteed among non-NULL key values? */
+	bool		nullable;
+} UniqueKey;
+
 /*
  * Contains an order of group-by clauses and the corresponding list of
  * pathkeys.
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 17f2099ec3b..11a577772cc 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -74,6 +74,8 @@ extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
 										Path *bitmapqual);
 extern void generate_partitionwise_join_paths(PlannerInfo *root,
 											  RelOptInfo *rel);
+extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
+									List *live_childrels);
 
 /*
  * indxpath.c
@@ -245,6 +247,7 @@ extern List *build_expression_pathkey(PlannerInfo *root, Expr *expr,
 extern List *convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
 									   List *subquery_pathkeys,
 									   List *subquery_tlist);
+extern Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle);
 extern List *build_join_pathkeys(PlannerInfo *root,
 								 RelOptInfo *joinrel,
 								 JoinType jointype,
@@ -283,7 +286,38 @@ extern List *append_pathkeys(List *target, List *source);
 extern PathKey *make_canonical_pathkey(PlannerInfo *root,
 									   EquivalenceClass *eclass, Oid opfamily,
 									   CompareType cmptype, bool nulls_first);
-extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
-									List *live_childrels);
+
+/*
+ * uniquekeys.c
+ *	  routines to deduce what expression sets a relation is distinct over
+ */
+extern void populate_baserel_uniquekeys(PlannerInfo *root, RelOptInfo *rel);
+extern void populate_joinrel_uniquekeys(PlannerInfo *root, RelOptInfo *joinrel,
+										RelOptInfo *outerrel,
+										RelOptInfo *innerrel,
+										SpecialJoinInfo *sjinfo,
+										List *restrictlist);
+extern void populate_grouped_rel_uniquekeys(PlannerInfo *root,
+											RelOptInfo *grouped_rel,
+											RelOptInfo *input_rel,
+											bool is_noop);
+extern void populate_distinct_rel_uniquekeys(PlannerInfo *root,
+											 RelOptInfo *distinct_rel,
+											 RelOptInfo *input_rel,
+											 bool is_noop);
+extern void populate_unique_rel_uniquekeys(PlannerInfo *root,
+										   RelOptInfo *unique_rel,
+										   RelOptInfo *input_rel,
+										   SpecialJoinInfo *sjinfo,
+										   bool is_noop);
+extern bool uniquekeys_distinct_is_noop(PlannerInfo *root,
+										RelOptInfo *input_rel);
+extern bool uniquekeys_grouping_is_noop(PlannerInfo *root,
+										RelOptInfo *input_rel);
+extern bool uniquekeys_match_join_clauses(PlannerInfo *root, RelOptInfo *rel,
+										  List *clause_list);
+extern bool uniquekeys_uniquification_is_noop(PlannerInfo *root,
+											  RelOptInfo *rel,
+											  SpecialJoinInfo *sjinfo);
 
 #endif							/* PATHS_H */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 5f0668382ed..9f4c9ae897c 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1226,15 +1226,13 @@ explain (costs off)
   select distinct max(unique2) from tenk1;
                              QUERY PLAN                              
 ---------------------------------------------------------------------
- HashAggregate
-   Group Key: (InitPlan minmax_1).col1
+ Result
+   Replaces: MinMaxAggregate
    InitPlan minmax_1
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
-   ->  Result
-         Replaces: MinMaxAggregate
-(8 rows)
+(6 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1417,7 +1415,8 @@ explain (costs off)
   select distinct min(f1), max(f1) from minmaxtest;
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
- Unique
+ Result
+   Replaces: MinMaxAggregate
    InitPlan minmax_1
      ->  Limit
            ->  Merge Append
@@ -1440,11 +1439,7 @@ explain (costs off)
                  ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
-   ->  Sort
-         Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_2).col1)
-         ->  Result
-               Replaces: MinMaxAggregate
-(27 rows)
+(24 rows)
 
 select distinct min(f1), max(f1) from minmaxtest;
  min | max 
@@ -1467,15 +1462,13 @@ explain (costs off)
 ---------------------------------------------------------------------
  Seq Scan on int4_tbl t0
    SubPlan expr_1
-     ->  HashAggregate
-           Group Key: (InitPlan minmax_1).col1
+     ->  Result
+           Replaces: MinMaxAggregate
            InitPlan minmax_1
              ->  Limit
                    ->  Seq Scan on int4_tbl t1
                          Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1))
-           ->  Result
-                 Replaces: MinMaxAggregate
-(10 rows)
+(8 rows)
 
 select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1)
 from int4_tbl t0;
@@ -1511,7 +1504,7 @@ create temp table t1 (a int, b int, c int, d int, primary key (a, b));
 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;
+explain (costs off) select count(*) from t1 group by a,b,c,d;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1529,7 +1522,7 @@ explain (costs off) select a,c from t1 group by a,c,d;
 (3 rows)
 
 -- Test removal across multiple relations
-explain (costs off) select *
+explain (costs off) select count(*)
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
                       QUERY PLAN                      
@@ -1544,7 +1537,7 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
 (7 rows)
 
 -- Test case where t1 can be optimized but not t2
-explain (costs off) select t1.*,t2.x,t2.z
+explain (costs off) select t1.*,t2.x,t2.z,count(*)
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
                       QUERY PLAN                      
@@ -1569,7 +1562,7 @@ explain (costs off) select * from t3 group by a,b,c;
 
 create temp table t1c () inherits (t1);
 -- Ensure we don't remove any columns when t1 has a child table
-explain (costs off) select * from t1 group by a,b,c,d;
+explain (costs off) select count(*) from t1 group by a,b,c,d;
              QUERY PLAN              
 -------------------------------------
  HashAggregate
@@ -1580,7 +1573,7 @@ explain (costs off) select * from t1 group by a,b,c,d;
 (5 rows)
 
 -- Okay to remove columns if we're only querying the parent.
-explain (costs off) select * from only t1 group by a,b,c,d;
+explain (costs off) select count(*) from only t1 group by a,b,c,d;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1598,7 +1591,7 @@ create temp table p_t1 (
 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;
+explain (costs off) select count(*) from p_t1 group by a,b,c,d;
            QUERY PLAN           
 --------------------------------
  HashAggregate
@@ -1611,7 +1604,7 @@ explain (costs off) select * from p_t1 group by a,b,c,d;
 create unique index t2_z_uidx on t2(z);
 -- Ensure we don't remove any columns from the GROUP BY for a unique
 -- index on a NULLable column.
-explain (costs off) select y,z from t2 group by y,z;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1621,7 +1614,7 @@ 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;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1632,7 +1625,7 @@ explain (costs off) select y,z from t2 group by y,z;
 -- 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;
+explain (costs off) select x,y,z,count(*) from t2 group by x,y,z;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1642,7 +1635,7 @@ explain (costs off) select x,y,z from t2 group by x,y,z;
 
 -- 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;
+explain (costs off) select x,y,z,count(*) from t2 group by z,x,y;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1653,7 +1646,7 @@ explain (costs off) select x,y,z from t2 group by z,x,y;
 -- Ensure we don't use a partial index as proof of functional dependency
 drop index t2_z_uidx;
 create index t2_z_uidx on t2 (z) where z > 0;
-explain (costs off) select y,z from t2 group by y,z;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
       QUERY PLAN      
 ----------------------
  HashAggregate
@@ -1667,7 +1660,7 @@ explain (costs off) select y,z from t2 group by y,z;
 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;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
       QUERY PLAN      
 ----------------------
  HashAggregate
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 19e2cca548b..c9b0dbd577f 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6419,17 +6419,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                
-------------------------------------------
- Merge Right Join
-   Merge Cond: (b.id = d.a)
-   ->  Group
-         Group Key: b.id
-         ->  Index Scan using b_pkey on b
-   ->  Sort
-         Sort Key: d.a
-         ->  Seq Scan on d
-(8 rows)
+           QUERY PLAN            
+---------------------------------
+ Hash Left Join
+   Hash Cond: (d.a = s.id)
+   ->  Seq Scan on d
+   ->  Hash
+         ->  Subquery Scan on s
+               ->  Seq Scan on b
+(6 rows)
 
 -- join removal is not possible when the GROUP BY contains non-empty grouping
 -- sets or multiple empty grouping sets
@@ -6484,18 +6482,15 @@ select d.* from d left join (select 1 as x from b group by grouping sets((), gro
 explain (costs off)
 select d.* from d left join (select distinct * from b) s
   on d.a = s.id;
-              QUERY PLAN              
---------------------------------------
- Merge Right Join
-   Merge Cond: (b.id = d.a)
-   ->  Unique
-         ->  Sort
-               Sort Key: b.id, b.c_id
+           QUERY PLAN            
+---------------------------------
+ Hash Left Join
+   Hash Cond: (d.a = s.id)
+   ->  Seq Scan on d
+   ->  Hash
+         ->  Subquery Scan on s
                ->  Seq Scan on b
-   ->  Sort
-         Sort Key: d.a
-         ->  Seq Scan on d
-(9 rows)
+(6 rows)
 
 -- join removal is not possible here
 explain (costs off)
diff --git a/src/test/regress/expected/uniquekeys.out b/src/test/regress/expected/uniquekeys.out
new file mode 100644
index 00000000000..b8fe3f65907
--- /dev/null
+++ b/src/test/regress/expected/uniquekeys.out
@@ -0,0 +1,354 @@
+--
+-- UNIQUEKEYS
+--
+-- Tests for the planner's inference of what a relation is distinct over
+--
+create table uk_t1 (a int, b int, c int, d int, primary key (a, b));
+create table uk_t2 (id int primary key, v int);
+create table uk_t3 (id int primary key, v int);
+create table uk_t4 (id int primary key, v int);
+create table uk_tn (nu int unique, nz int, k int);
+create unique index uk_tn_nz on uk_tn (nz) nulls not distinct;
+create table uk_def (id int, primary key (id) deferrable);
+create table uk_part (a int, b int);
+create unique index uk_part_a on uk_part (a) where b > 0;
+--
+-- Base relation keys
+--
+-- a non-nullable multi-column key removes the DISTINCT
+explain (costs off)
+select distinct a, b from uk_t1;
+    QUERY PLAN     
+-------------------
+ Seq Scan on uk_t1
+(1 row)
+
+-- a key column equated to a constant is dropped from the key
+explain (costs off)
+select distinct b from uk_t1 where a = 5;
+              QUERY PLAN               
+---------------------------------------
+ Bitmap Heap Scan on uk_t1
+   Recheck Cond: (a = 5)
+   ->  Bitmap Index Scan on uk_t1_pkey
+         Index Cond: (a = 5)
+(4 rows)
+
+-- a nullable unique column is not enough on its own
+explain (costs off)
+select distinct nu from uk_tn;
+       QUERY PLAN        
+-------------------------
+ HashAggregate
+   Group Key: nu
+   ->  Seq Scan on uk_tn
+(3 rows)
+
+-- a NULLS NOT DISTINCT unique column is a full key
+explain (costs off)
+select distinct nz from uk_tn;
+    QUERY PLAN     
+-------------------
+ Seq Scan on uk_tn
+(1 row)
+
+-- a strict restriction makes the nullable unique column non-null
+explain (costs off)
+select distinct nu from uk_tn where nu > 0;
+               QUERY PLAN                
+-----------------------------------------
+ Bitmap Heap Scan on uk_tn
+   Recheck Cond: (nu > 0)
+   ->  Bitmap Index Scan on uk_tn_nu_key
+         Index Cond: (nu > 0)
+(4 rows)
+
+-- a deferrable unique index is unusable
+explain (costs off)
+select distinct id from uk_def;
+        QUERY PLAN        
+--------------------------
+ HashAggregate
+   Group Key: id
+   ->  Seq Scan on uk_def
+(3 rows)
+
+-- a partial unique index is unusable
+explain (costs off)
+select distinct a from uk_part where b > 0;
+                 QUERY PLAN                 
+--------------------------------------------
+ HashAggregate
+   Group Key: a
+   ->  Bitmap Heap Scan on uk_part
+         Recheck Cond: (b > 0)
+         ->  Bitmap Index Scan on uk_part_a
+(5 rows)
+
+--
+-- Subquery-in-FROM keys
+--
+-- a DISTINCT inside is translated
+explain (costs off)
+select distinct a, b from (select distinct a, b from uk_t1) s;
+    QUERY PLAN     
+-------------------
+ Seq Scan on uk_t1
+(1 row)
+
+-- a GROUP BY inside is translated
+explain (costs off)
+select distinct a, b from (select a, b from uk_t1 group by a, b) s;
+    QUERY PLAN     
+-------------------
+ Seq Scan on uk_t1
+(1 row)
+
+-- a plain aggregate emits a single row
+explain (costs off)
+select distinct m from (select max(a) as m from uk_t1) s;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Result
+   Replaces: MinMaxAggregate
+   InitPlan minmax_1
+     ->  Limit
+           ->  Index Only Scan Backward using uk_t1_pkey on uk_t1
+(5 rows)
+
+-- a key from a base index carried up through a join
+explain (costs off)
+select distinct x from (select uk_t2.id as x from uk_t2 join uk_t3 on uk_t2.id = uk_t3.id offset 0) s;
+             QUERY PLAN             
+------------------------------------
+ Hash Join
+   Hash Cond: (uk_t2.id = uk_t3.id)
+   ->  Seq Scan on uk_t2
+   ->  Hash
+         ->  Seq Scan on uk_t3
+(5 rows)
+
+-- a non-ALL set operation is distinct over its whole output row (the set
+-- operation's own dedup Aggregate ends up below the join)
+explain (costs off)
+select distinct s.a, s.b from (select a, b from uk_t1 union select a, b from uk_t1) s join uk_t2 on s.a = uk_t2.id;
+                 QUERY PLAN                  
+---------------------------------------------
+ Hash Join
+   Hash Cond: (uk_t1.a = uk_t2.id)
+   ->  HashAggregate
+         Group Key: uk_t1.a, uk_t1.b
+         ->  Append
+               ->  Seq Scan on uk_t1
+               ->  Seq Scan on uk_t1 uk_t1_1
+   ->  Hash
+         ->  Seq Scan on uk_t2
+(9 rows)
+
+-- UNION ALL is not distinct, so the DISTINCT Aggregate stays above the join
+explain (costs off)
+select distinct s.a, s.b from (select a, b from uk_t1 union all select a, b from uk_t1) s join uk_t2 on s.a = uk_t2.id;
+                 QUERY PLAN                  
+---------------------------------------------
+ HashAggregate
+   Group Key: uk_t1.a, uk_t1.b
+   ->  Hash Join
+         Hash Cond: (uk_t1.a = uk_t2.id)
+         ->  Append
+               ->  Seq Scan on uk_t1
+               ->  Seq Scan on uk_t1 uk_t1_1
+         ->  Hash
+               ->  Seq Scan on uk_t2
+(9 rows)
+
+--
+-- Join relation keys
+--
+-- an inner join preserves a side's key when the other side is unique for the
+-- join clauses
+explain (costs off)
+select distinct uk_t3.id from uk_t2 join uk_t3 on uk_t2.id = uk_t3.id;
+             QUERY PLAN             
+------------------------------------
+ Hash Join
+   Hash Cond: (uk_t2.id = uk_t3.id)
+   ->  Seq Scan on uk_t2
+   ->  Hash
+         ->  Seq Scan on uk_t3
+(5 rows)
+
+-- combination: the union of a key from each side (here a cross join)
+explain (costs off)
+select distinct uk_t2.id, uk_t3.id from uk_t2 cross join uk_t3;
+          QUERY PLAN           
+-------------------------------
+ Nested Loop
+   ->  Seq Scan on uk_t2
+   ->  Materialize
+         ->  Seq Scan on uk_t3
+(4 rows)
+
+-- a left join makes the RHS key nullable, so the DISTINCT is kept
+explain (costs off)
+select distinct uk_t3.id from uk_t2 left join uk_t3 on uk_t2.id = uk_t3.id;
+                QUERY PLAN                
+------------------------------------------
+ HashAggregate
+   Group Key: uk_t3.id
+   ->  Hash Left Join
+         Hash Cond: (uk_t2.id = uk_t3.id)
+         ->  Seq Scan on uk_t2
+         ->  Hash
+               ->  Seq Scan on uk_t3
+(7 rows)
+
+-- a left join preserves the non-nullable LHS key
+explain (costs off)
+select distinct uk_t2.id, uk_t3.v from uk_t2 left join uk_t3 on uk_t2.id = uk_t3.id;
+             QUERY PLAN             
+------------------------------------
+ Hash Left Join
+   Hash Cond: (uk_t2.id = uk_t3.id)
+   ->  Seq Scan on uk_t2
+   ->  Hash
+         ->  Seq Scan on uk_t3
+(5 rows)
+
+-- a semijoin preserves the LHS key
+explain (costs off)
+select distinct a, b from uk_t1 where exists (select 1 from uk_t2 where uk_t2.id = uk_t1.c);
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Hash Cond: (uk_t1.c = uk_t2.id)
+   ->  Seq Scan on uk_t1
+   ->  Hash
+         ->  Seq Scan on uk_t2
+(5 rows)
+
+-- an inner join's strict clause strengthens a nullable key to non-nullable
+explain (costs off)
+select distinct nu from uk_tn join uk_t2 on uk_tn.nu = uk_t2.id;
+             QUERY PLAN             
+------------------------------------
+ Hash Join
+   Hash Cond: (uk_t2.id = uk_tn.nu)
+   ->  Seq Scan on uk_t2
+   ->  Hash
+         ->  Seq Scan on uk_tn
+(5 rows)
+
+-- the strict clause does not cover the key column, so no strengthening
+explain (costs off)
+select distinct nu from uk_tn join uk_t2 on uk_tn.k = uk_t2.id;
+               QUERY PLAN                
+-----------------------------------------
+ HashAggregate
+   Group Key: uk_tn.nu
+   ->  Hash Join
+         Hash Cond: (uk_tn.k = uk_t2.id)
+         ->  Seq Scan on uk_tn
+         ->  Hash
+               ->  Seq Scan on uk_t2
+(7 rows)
+
+-- a full join yields no key
+explain (costs off)
+select distinct uk_t2.id from uk_t2 full join uk_t3 on uk_t2.id = uk_t3.id;
+                QUERY PLAN                
+------------------------------------------
+ HashAggregate
+   Group Key: uk_t2.id
+   ->  Hash Full Join
+         Hash Cond: (uk_t2.id = uk_t3.id)
+         ->  Seq Scan on uk_t2
+         ->  Hash
+               ->  Seq Scan on uk_t3
+(7 rows)
+
+-- a key survives stacked outer joins
+explain (costs off)
+select distinct uk_t2.id, y.v from uk_t2 left join uk_t3 x on uk_t2.id = x.id left join uk_t3 y on x.id = y.id;
+              QUERY PLAN               
+---------------------------------------
+ Hash Left Join
+   Hash Cond: (x.id = y.id)
+   ->  Hash Left Join
+         Hash Cond: (uk_t2.id = x.id)
+         ->  Seq Scan on uk_t2
+         ->  Hash
+               ->  Seq Scan on uk_t3 x
+   ->  Hash
+         ->  Seq Scan on uk_t3 y
+(9 rows)
+
+-- uniqueness of a multi-relation inner side is proven from that joinrel's key
+explain (verbose, costs off)
+select distinct uk_t2.id from uk_t2 left join (uk_t3 join uk_t4 on uk_t3.id = uk_t4.id) on uk_t2.v = uk_t3.id;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Hash Left Join
+   Output: uk_t2.id
+   Inner Unique: true
+   Hash Cond: (uk_t2.v = uk_t3.id)
+   ->  Seq Scan on public.uk_t2
+         Output: uk_t2.id, uk_t2.v
+   ->  Hash
+         Output: uk_t3.id
+         ->  Hash Join
+               Output: uk_t3.id
+               Inner Unique: true
+               Hash Cond: (uk_t3.id = uk_t4.id)
+               ->  Seq Scan on public.uk_t3
+                     Output: uk_t3.id, uk_t3.v
+               ->  Hash
+                     Output: uk_t4.id
+                     ->  Seq Scan on public.uk_t4
+                           Output: uk_t4.id
+(18 rows)
+
+-- uniqueness of a semijoin's multi-relation RHS skips the unique-ification step
+explain (costs off)
+select * from uk_t2, uk_t3 where (uk_t2.id, uk_t3.id) in (select t1.a, t1.b from uk_t1 t1 join uk_tn on t1.a = nu and b = nz);
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Nested Loop
+   ->  Nested Loop
+         ->  Hash Join
+               Hash Cond: ((uk_tn.nu = t1.a) AND (uk_tn.nz = t1.b))
+               ->  Seq Scan on uk_tn
+               ->  Hash
+                     ->  Seq Scan on uk_t1 t1
+         ->  Index Scan using uk_t2_pkey on uk_t2
+               Index Cond: (id = t1.a)
+   ->  Index Scan using uk_t3_pkey on uk_t3
+         Index Cond: (id = t1.b)
+(11 rows)
+
+--
+-- Grouping step: grouping by a superset of a key is a no-op (with no aggregates)
+--
+explain (costs off)
+select a, b from uk_t1 group by a, b, c;
+    QUERY PLAN     
+-------------------
+ Seq Scan on uk_t1
+(1 row)
+
+-- grouping by a non-key set is not a no-op
+explain (costs off)
+select a, c from uk_t1 group by a, c;
+       QUERY PLAN        
+-------------------------
+ HashAggregate
+   Group Key: a, c
+   ->  Seq Scan on uk_t1
+(3 rows)
+
+drop table uk_t1;
+drop table uk_t2;
+drop table uk_t3;
+drop table uk_t4;
+drop table uk_tn;
+drop table uk_def;
+drop table uk_part;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9920877c107 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -63,6 +63,8 @@ test: sanity_check
 # ----------
 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
 
+test: uniquekeys
+
 # ----------
 # Another group of parallel tests
 # ----------
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index b788152f0c2..e854948e0a2 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -517,18 +517,18 @@ 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;
+explain (costs off) select count(*) from t1 group by a,b,c,d;
 
 -- 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;
 
 -- Test removal across multiple relations
-explain (costs off) select *
+explain (costs off) select count(*)
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
 
 -- Test case where t1 can be optimized but not t2
-explain (costs off) select t1.*,t2.x,t2.z
+explain (costs off) select t1.*,t2.x,t2.z,count(*)
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
 
@@ -538,10 +538,10 @@ explain (costs off) select * from t3 group by a,b,c;
 create temp table t1c () inherits (t1);
 
 -- Ensure we don't remove any columns when t1 has a child table
-explain (costs off) select * from t1 group by a,b,c,d;
+explain (costs off) select count(*) 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;
+explain (costs off) select count(*) from only t1 group by a,b,c,d;
 
 create temp table p_t1 (
   a int,
@@ -554,31 +554,31 @@ 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;
+explain (costs off) select count(*) from p_t1 group by a,b,c,d;
 
 create unique index t2_z_uidx on t2(z);
 
 -- Ensure we don't remove any columns from the GROUP BY for a unique
 -- index on a NULLable column.
-explain (costs off) select y,z from t2 group by y,z;
+explain (costs off) select y,z,count(*) 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;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
 
 -- 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;
+explain (costs off) select x,y,z,count(*) from t2 group by x,y,z;
 
 -- 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;
+explain (costs off) select x,y,z,count(*) from t2 group by z,x,y;
 
 -- Ensure we don't use a partial index as proof of functional dependency
 drop index t2_z_uidx;
 create index t2_z_uidx on t2 (z) where z > 0;
-explain (costs off) select y,z from t2 group by y,z;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
 
 -- A unique index defined as NULLS NOT DISTINCT does not need a supporting NOT
 -- NULL constraint on the indexed columns.  Ensure the redundant columns are
@@ -586,7 +586,7 @@ explain (costs off) select y,z from t2 group by y,z;
 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;
+explain (costs off) select y,z,count(*) from t2 group by y,z;
 
 drop table t1 cascade;
 drop table t2;
diff --git a/src/test/regress/sql/uniquekeys.sql b/src/test/regress/sql/uniquekeys.sql
new file mode 100644
index 00000000000..cbfc020fa0c
--- /dev/null
+++ b/src/test/regress/sql/uniquekeys.sql
@@ -0,0 +1,118 @@
+--
+-- UNIQUEKEYS
+--
+-- Tests for the planner's inference of what a relation is distinct over
+--
+
+create table uk_t1 (a int, b int, c int, d int, primary key (a, b));
+create table uk_t2 (id int primary key, v int);
+create table uk_t3 (id int primary key, v int);
+create table uk_t4 (id int primary key, v int);
+create table uk_tn (nu int unique, nz int, k int);
+create unique index uk_tn_nz on uk_tn (nz) nulls not distinct;
+create table uk_def (id int, primary key (id) deferrable);
+create table uk_part (a int, b int);
+create unique index uk_part_a on uk_part (a) where b > 0;
+
+--
+-- Base relation keys
+--
+-- a non-nullable multi-column key removes the DISTINCT
+explain (costs off)
+select distinct a, b from uk_t1;
+-- a key column equated to a constant is dropped from the key
+explain (costs off)
+select distinct b from uk_t1 where a = 5;
+-- a nullable unique column is not enough on its own
+explain (costs off)
+select distinct nu from uk_tn;
+-- a NULLS NOT DISTINCT unique column is a full key
+explain (costs off)
+select distinct nz from uk_tn;
+-- a strict restriction makes the nullable unique column non-null
+explain (costs off)
+select distinct nu from uk_tn where nu > 0;
+-- a deferrable unique index is unusable
+explain (costs off)
+select distinct id from uk_def;
+-- a partial unique index is unusable
+explain (costs off)
+select distinct a from uk_part where b > 0;
+
+--
+-- Subquery-in-FROM keys
+--
+-- a DISTINCT inside is translated
+explain (costs off)
+select distinct a, b from (select distinct a, b from uk_t1) s;
+-- a GROUP BY inside is translated
+explain (costs off)
+select distinct a, b from (select a, b from uk_t1 group by a, b) s;
+-- a plain aggregate emits a single row
+explain (costs off)
+select distinct m from (select max(a) as m from uk_t1) s;
+-- a key from a base index carried up through a join
+explain (costs off)
+select distinct x from (select uk_t2.id as x from uk_t2 join uk_t3 on uk_t2.id = uk_t3.id offset 0) s;
+-- a non-ALL set operation is distinct over its whole output row (the set
+-- operation's own dedup Aggregate ends up below the join)
+explain (costs off)
+select distinct s.a, s.b from (select a, b from uk_t1 union select a, b from uk_t1) s join uk_t2 on s.a = uk_t2.id;
+-- UNION ALL is not distinct, so the DISTINCT Aggregate stays above the join
+explain (costs off)
+select distinct s.a, s.b from (select a, b from uk_t1 union all select a, b from uk_t1) s join uk_t2 on s.a = uk_t2.id;
+
+--
+-- Join relation keys
+--
+-- an inner join preserves a side's key when the other side is unique for the
+-- join clauses
+explain (costs off)
+select distinct uk_t3.id from uk_t2 join uk_t3 on uk_t2.id = uk_t3.id;
+-- combination: the union of a key from each side (here a cross join)
+explain (costs off)
+select distinct uk_t2.id, uk_t3.id from uk_t2 cross join uk_t3;
+-- a left join makes the RHS key nullable, so the DISTINCT is kept
+explain (costs off)
+select distinct uk_t3.id from uk_t2 left join uk_t3 on uk_t2.id = uk_t3.id;
+-- a left join preserves the non-nullable LHS key
+explain (costs off)
+select distinct uk_t2.id, uk_t3.v from uk_t2 left join uk_t3 on uk_t2.id = uk_t3.id;
+-- a semijoin preserves the LHS key
+explain (costs off)
+select distinct a, b from uk_t1 where exists (select 1 from uk_t2 where uk_t2.id = uk_t1.c);
+-- an inner join's strict clause strengthens a nullable key to non-nullable
+explain (costs off)
+select distinct nu from uk_tn join uk_t2 on uk_tn.nu = uk_t2.id;
+-- the strict clause does not cover the key column, so no strengthening
+explain (costs off)
+select distinct nu from uk_tn join uk_t2 on uk_tn.k = uk_t2.id;
+-- a full join yields no key
+explain (costs off)
+select distinct uk_t2.id from uk_t2 full join uk_t3 on uk_t2.id = uk_t3.id;
+-- a key survives stacked outer joins
+explain (costs off)
+select distinct uk_t2.id, y.v from uk_t2 left join uk_t3 x on uk_t2.id = x.id left join uk_t3 y on x.id = y.id;
+-- uniqueness of a multi-relation inner side is proven from that joinrel's key
+explain (verbose, costs off)
+select distinct uk_t2.id from uk_t2 left join (uk_t3 join uk_t4 on uk_t3.id = uk_t4.id) on uk_t2.v = uk_t3.id;
+-- uniqueness of a semijoin's multi-relation RHS skips the unique-ification step
+explain (costs off)
+select * from uk_t2, uk_t3 where (uk_t2.id, uk_t3.id) in (select t1.a, t1.b from uk_t1 t1 join uk_tn on t1.a = nu and b = nz);
+
+--
+-- Grouping step: grouping by a superset of a key is a no-op (with no aggregates)
+--
+explain (costs off)
+select a, b from uk_t1 group by a, b, c;
+-- grouping by a non-key set is not a no-op
+explain (costs off)
+select a, c from uk_t1 group by a, c;
+
+drop table uk_t1;
+drop table uk_t2;
+drop table uk_t3;
+drop table uk_t4;
+drop table uk_tn;
+drop table uk_def;
+drop table uk_part;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..c48e13e274d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3331,6 +3331,7 @@ UVersionInfo
 UnicodeNormalizationForm
 UnicodeNormalizationQC
 Unique
+UniqueKey
 UniquePath
 UniqueRelInfo
 UniqueState
-- 
2.39.5 (Apple Git-154)

