From 1049a4525a9dba51cf0e4c2bffd109538746fa40 Mon Sep 17 00:00:00 2001
From: Hannu Krosing <hannuk@google.com>
Date: Wed, 16 Sep 2026 13:22:47 +0000
Subject: [PATCH v3 2/5] Support inline ORDER BY in DISTINCT ON for SELECT and
 enable HashAgg

This patch introduces the syntax:
    SELECT DISTINCT ON (keys ORDER BY sort_keys) ...

This decouples the tie-breaker sort order used for duplicate elimination
from the final output sort order of the query, avoiding the need for
subqueries.

In addition, this enables Hash Aggregate to execute DISTINCT ON queries
with tie-breaking by introducing ReplaceTupleHashEntryIfBetter() and
using AllocSetContext for hashed tuples when sort columns are present.
---
 doc/src/sgml/ref/select.sgml                  | 37 +++++----
 src/backend/executor/execGrouping.c           | 80 +++++++++++++++++++
 src/backend/executor/nodeAgg.c                | 78 ++++++++++++++++--
 src/backend/optimizer/plan/createplan.c       | 46 +++++++++++
 src/backend/optimizer/plan/planner.c          | 42 ++++++++--
 src/backend/optimizer/util/pathnode.c         | 19 +++++
 src/backend/parser/analyze.c                  | 41 +++++++++-
 src/backend/parser/gram.y                     | 19 ++++-
 src/include/executor/executor.h               |  6 ++
 src/include/executor/nodeAgg.h                |  2 +
 src/include/nodes/parsenodes.h                |  3 +
 src/include/nodes/pathnodes.h                 |  1 +
 src/include/nodes/plannodes.h                 |  7 ++
 src/include/optimizer/pathnode.h              | 11 +++
 .../regress/expected/select_distinct_on.out   | 75 +++++++++++++++++
 src/test/regress/sql/select_distinct_on.sql   | 22 +++++
 16 files changed, 459 insertions(+), 30 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 18392f7caec..1ed53b2b88e 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1203,30 +1203,39 @@ EXCLUDE NO OTHERS
 
    <para>
     <literal>SELECT DISTINCT ON ( <replaceable
-    class="parameter">expression</replaceable> [, ...] )</literal>
+    class="parameter">expression</replaceable> [, ...] [ ORDER BY <replaceable
+    class="parameter">sort_expression</replaceable> [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] ] )</literal>
     keeps only the first row of each set of rows where the given
     expressions evaluate to equal.  The <literal>DISTINCT ON</literal>
     expressions are interpreted using the same rules as for
-    <literal>ORDER BY</literal> (see above).  Note that the <quote>first
-    row</quote> of each set is unpredictable unless <literal>ORDER
-    BY</literal> is used to ensure that the desired row appears first.  For
-    example:
+    <literal>ORDER BY</literal> (see above).
+    If the optional inline <literal>ORDER BY</literal> clause is specified,
+    it determines which row is kept from each set of duplicates (the first row
+    according to this sort order). This inline sorting applies only for duplicate
+    resolution and does not dictate the final output order of the query.
+    Note that if no inline <literal>ORDER BY</literal> is used, the <quote>first
+    row</quote> of each set is unpredictable unless a global <literal>ORDER
+    BY</literal> is used at the end of the query to ensure that the desired row appears first.
+    For example:
 <programlisting>
-SELECT DISTINCT ON (location) location, time, report
-    FROM weather_reports
-    ORDER BY location, time DESC;
+SELECT DISTINCT ON (location ORDER BY time DESC) location, time, report
+    FROM weather_reports;
 </programlisting>
     retrieves the most recent weather report for each location.  But
-    if we had not used <literal>ORDER BY</literal> to force descending order
-    of time values for each location, we'd have gotten a report from
-    an unpredictable time for each location.
+    if we had not used inline <literal>ORDER BY time DESC</literal>, we'd have gotten a report from
+    an unpredictable time for each location (unless a global <literal>ORDER BY</literal> was used).
    </para>
 
    <para>
-    The <literal>DISTINCT ON</literal> expression(s) must match the leftmost
-    <literal>ORDER BY</literal> expression(s).  The <literal>ORDER BY</literal> clause
-    will normally contain additional expression(s) that determine the
+    If a global <literal>ORDER BY</literal> clause is used at the end of the query
+    without an inline <literal>ORDER BY</literal> inside <literal>DISTINCT ON</literal>,
+    the <literal>DISTINCT ON</literal> expression(s) must match the leftmost
+    <literal>ORDER BY</literal> expression(s). The global <literal>ORDER BY</literal> clause
+    will then normally contain additional expression(s) that determine the
     desired precedence of rows within each <literal>DISTINCT ON</literal> group.
+    If an inline <literal>ORDER BY</literal> is specified, this matching requirement
+    is relaxed, and the global <literal>ORDER BY</literal> can sort the final results
+    by any columns, independent of the distinct keys.
    </para>
 
    <para>
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 948aa2f177a..7a46b29ee2f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -22,6 +22,7 @@
 #include "executor/executor.h"
 #include "miscadmin.h"
 #include "utils/lsyscache.h"
+#include "utils/sortsupport.h"
 
 static int	TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple tuple2);
 static inline uint32 TupleHashTableHash_internal(struct tuplehash_hash *tb,
@@ -621,3 +622,82 @@ TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple
 	econtext->ecxt_outertuple = slot1;
 	return !ExecQualAndReset(hashtable->cur_eq_func, econtext);
 }
+
+/*
+ * ReplaceTupleHashEntryIfBetter
+ *
+ * Compare the new slot with the stored tuple in the entry using the sort keys.
+ * If the new slot is "better" (comes before in sort order), replace the stored
+ * tuple in the entry.
+ *
+ * Returns true if replaced, false otherwise.
+ */
+bool
+ReplaceTupleHashEntryIfBetter(TupleHashTable hashtable,
+							  TupleHashEntry entry,
+							  TupleTableSlot *newslot,
+							  TupleTableSlot *firstslot,
+							  SortSupport sortKeys,
+							  int numSortCols)
+{
+	int			i;
+	bool		replace = false;
+
+	/* If no sort keys, we shouldn't be here */
+	if (numSortCols == 0)
+		return false;
+
+	/* Retrieve stored tuple and store it in firstslot */
+	ExecStoreMinimalTuple(entry->firstTuple, firstslot, false);
+
+	/* Compare sort keys one by one */
+	for (i = 0; i < numSortCols; i++)
+	{
+		SortSupport skey = &sortKeys[i];
+		AttrNumber	attno = skey->ssup_attno;
+		Datum		datum1, datum2;
+		bool		isnull1, isnull2;
+		int			compare;
+
+		datum1 = slot_getattr(firstslot, attno, &isnull1);
+		datum2 = slot_getattr(newslot, attno, &isnull2);
+
+		compare = ApplySortComparator(datum1, isnull1,
+									  datum2, isnull2,
+									  skey);
+
+		if (compare != 0)
+		{
+			/*
+			 * ApplySortComparator returns < 0 if datum1 comes BEFORE datum2.
+			 * So if compare > 0, datum2 comes BEFORE datum1, so it is better.
+			 */
+			if (compare > 0)
+				replace = true;
+			break; /* Found a difference, no need to compare further */
+		}
+	}
+
+	if (replace)
+	{
+		MinimalTuple oldtuple = entry->firstTuple;
+		MinimalTuple newtuple;
+		MemoryContext oldcxt;
+
+		/* Copy new tuple into the long-lived context */
+		oldcxt = MemoryContextSwitchTo(hashtable->tuplescxt);
+		newtuple = ExecCopySlotMinimalTuple(newslot);
+		MemoryContextSwitchTo(oldcxt);
+
+		/* Replace in entry */
+		entry->firstTuple = newtuple;
+
+		/* Free old tuple */
+		pfree(oldtuple);
+	}
+
+	/* Clear the comparison slot to avoid holding references */
+	ExecClearTuple(firstslot);
+
+	return replace;
+}
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 29037cf3122..aba6bc05d87 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -277,6 +277,7 @@
 #include "utils/memutils_memorychunk.h"
 #include "utils/syscache.h"
 #include "utils/tuplesort.h"
+#include "utils/sortsupport.h"
 
 /*
  * Control how many partitions are created when spilling HashAgg to
@@ -1684,6 +1685,30 @@ find_hash_columns(AggState *aggstate)
 			ExecAllocTableSlot(&estate->es_tupleTable, hashDesc,
 							   &TTSOpsMinimalTuple, 0);
 
+		if (perhash->aggnode->numSortCols > 0)
+		{
+			perhash->hash_firstTupleSlot =
+				ExecAllocTableSlot(&estate->es_tupleTable, hashDesc,
+								   &TTSOpsMinimalTuple, 0);
+
+			/* Initialize sort support */
+			perhash->sortKeys = (SortSupport) palloc0(sizeof(SortSupportData) * perhash->aggnode->numSortCols);
+			for (i = 0; i < perhash->aggnode->numSortCols; i++)
+			{
+				SortSupport	skey = &perhash->sortKeys[i];
+
+				skey->ssup_collation = perhash->aggnode->sortCollations[i];
+				skey->ssup_nulls_first = perhash->aggnode->sortNullsFirst[i];
+				skey->ssup_attno = perhash->aggnode->sortColIdx[i];
+				PrepareSortSupportFromOrderingOp(perhash->aggnode->sortOperators[i], skey);
+			}
+		}
+		else
+		{
+			perhash->hash_firstTupleSlot = NULL;
+			perhash->sortKeys = NULL;
+		}
+
 		list_free(hashTlist);
 		bms_free(colnos);
 	}
@@ -1996,7 +2021,25 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 static void
 hash_create_memory(AggState *aggstate)
 {
+	Agg		   *node = (Agg *) aggstate->ss.ps.plan;
 	Size		maxBlockSize = ALLOCSET_DEFAULT_MAXSIZE;
+	bool		use_allocset = false;
+	ListCell   *lc;
+
+	if (node->numSortCols > 0)
+		use_allocset = true;
+	else
+	{
+		foreach(lc, node->chain)
+		{
+			Agg *chained_node = lfirst_node(Agg, lc);
+			if (chained_node->numSortCols > 0)
+			{
+				use_allocset = true;
+				break;
+			}
+		}
+	}
 
 	/*
 	 * The hashcontext's per-tuple memory will be used for byref transition
@@ -2040,11 +2083,20 @@ hash_create_memory(AggState *aggstate)
 	/* and no smaller than ALLOCSET_DEFAULT_INITSIZE */
 	maxBlockSize = Max(maxBlockSize, ALLOCSET_DEFAULT_INITSIZE);
 
-	aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt,
-												 "HashAgg hashed tuples",
-												 ALLOCSET_DEFAULT_MINSIZE,
-												 ALLOCSET_DEFAULT_INITSIZE,
-												 maxBlockSize);
+	if (use_allocset)
+	{
+		aggstate->hash_tuplescxt = AllocSetContextCreate(aggstate->ss.ps.state->es_query_cxt,
+														 "HashAgg hashed tuples",
+														 ALLOCSET_DEFAULT_SIZES);
+	}
+	else
+	{
+		aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt,
+													 "HashAgg hashed tuples",
+													 ALLOCSET_DEFAULT_MINSIZE,
+													 ALLOCSET_DEFAULT_INITSIZE,
+													 maxBlockSize);
+	}
 
 }
 
@@ -2178,6 +2230,18 @@ initialize_hash_entry(AggState *aggstate, TupleHashTable hashtable,
  * for each grouping set, making the refilling of the hash table very
  * efficient.
  */
+static void
+replace_hash_entry_if_better(AggState *aggstate, AggStatePerHash perhash,
+							TupleHashEntry entry, TupleTableSlot *newslot)
+{
+	ReplaceTupleHashEntryIfBetter(perhash->hashtable,
+								  entry,
+								  newslot,
+								  perhash->hash_firstTupleSlot,
+								  perhash->sortKeys,
+								  perhash->aggnode->numSortCols);
+}
+
 static void
 lookup_hash_entries(AggState *aggstate)
 {
@@ -2210,6 +2274,8 @@ lookup_hash_entries(AggState *aggstate)
 		{
 			if (isnew)
 				initialize_hash_entry(aggstate, hashtable, entry);
+			else if (perhash->aggnode->numSortCols > 0)
+				replace_hash_entry_if_better(aggstate, perhash, entry, hashslot);
 			pergroup[setno] = TupleHashEntryGetAdditional(hashtable, entry);
 		}
 		else
@@ -2770,6 +2836,8 @@ agg_refill_hash_table(AggState *aggstate)
 		{
 			if (isnew)
 				initialize_hash_entry(aggstate, hashtable, entry);
+			else if (perhash->aggnode->numSortCols > 0)
+				replace_hash_entry_if_better(aggstate, perhash, entry, hashslot);
 			aggstate->hash_pergroup[batch->setno] = TupleHashEntryGetAdditional(hashtable, entry);
 			advance_aggregates(aggstate);
 		}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 2f8a08ad78e..78a6d6a3c20 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2193,6 +2193,52 @@ create_agg_plan(PlannerInfo *root, AggPath *best_path)
 					best_path->transitionSpace,
 					subplan);
 
+	/* Extract sort keys for inline DISTINCT ON ORDER BY */
+	if (best_path->distinctSortClause)
+	{
+		List	   *sortcls = best_path->distinctSortClause;
+		List	   *sub_tlist = subplan->targetlist;
+		ListCell   *l;
+		int			numsortkeys;
+		AttrNumber *sortColIdx;
+		Oid		   *sortOperators;
+		Oid		   *collations;
+		bool	   *nullsFirst;
+
+		numsortkeys = list_length(sortcls);
+		sortColIdx = (AttrNumber *) palloc(numsortkeys * sizeof(AttrNumber));
+		sortOperators = (Oid *) palloc(numsortkeys * sizeof(Oid));
+		collations = (Oid *) palloc(numsortkeys * sizeof(Oid));
+		nullsFirst = (bool *) palloc(numsortkeys * sizeof(bool));
+
+		numsortkeys = 0;
+		foreach(l, sortcls)
+		{
+			SortGroupClause *sortcl = (SortGroupClause *) lfirst(l);
+			TargetEntry *tle = get_sortgroupclause_tle(sortcl, sub_tlist);
+
+			sortColIdx[numsortkeys] = tle->resno;
+			sortOperators[numsortkeys] = sortcl->sortop;
+			collations[numsortkeys] = exprCollation((Node *) tle->expr);
+			nullsFirst[numsortkeys] = sortcl->nulls_first;
+			numsortkeys++;
+		}
+
+		plan->numSortCols = numsortkeys;
+		plan->sortColIdx = sortColIdx;
+		plan->sortOperators = sortOperators;
+		plan->sortCollations = collations;
+		plan->sortNullsFirst = nullsFirst;
+	}
+	else
+	{
+		plan->numSortCols = 0;
+		plan->sortColIdx = NULL;
+		plan->sortOperators = NULL;
+		plan->sortCollations = NULL;
+		plan->sortNullsFirst = NULL;
+	}
+
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
 	return plan;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c09d356d528..5df5641e05a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3874,16 +3874,46 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 
 		/* Make a copy since pathkey processing can modify the list */
 		root->processed_distinctClause = list_copy(parse->distinctClause);
-		root->distinct_pathkeys =
+		
+		if (parse->distinctSortClause)
+		{
+			/* We have DISTINCT ON with ORDER BY */
+			List *temp_distinct_clause = list_copy(parse->distinctSortClause);
+			bool temp_sortable;
+			root->distinct_pathkeys =
+				make_pathkeys_for_sortclauses_extended(root,
+													   &temp_distinct_clause,
+													   tlist,
+													   true,
+													   false,
+													   &sortable,
+													   false);
+			if (!sortable)
+				root->distinct_pathkeys = NIL;
+
+			/* We ALSO need to remove redundant keys from processed_distinctClause */
 			make_pathkeys_for_sortclauses_extended(root,
 												   &root->processed_distinctClause,
 												   tlist,
 												   true,
 												   false,
-												   &sortable,
+												   &temp_sortable,
 												   false);
-		if (!sortable)
-			root->distinct_pathkeys = NIL;
+		}
+		else
+		{
+			/* Standard DISTINCT or DISTINCT ON without ORDER BY */
+			root->distinct_pathkeys =
+				make_pathkeys_for_sortclauses_extended(root,
+													   &root->processed_distinctClause,
+													   tlist,
+													   true,
+													   false,
+													   &sortable,
+													   false);
+			if (!sortable)
+				root->distinct_pathkeys = NIL;
+		}
 	}
 	else
 		root->distinct_pathkeys = NIL;
@@ -5272,7 +5302,7 @@ create_partial_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel,
 					add_partial_path(partial_distinct_rel, (Path *)
 									 create_unique_path(root, partial_distinct_rel,
 														sorted_path,
-														list_length(root->distinct_pathkeys),
+														list_length(root->processed_distinctClause),
 														numDistinctRows));
 				}
 			}
@@ -5466,7 +5496,7 @@ create_final_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel,
 					add_path(distinct_rel, (Path *)
 							 create_unique_path(root, distinct_rel,
 												sorted_path,
-												list_length(root->distinct_pathkeys),
+												list_length(root->processed_distinctClause),
 												numDistinctRows));
 				}
 			}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 67c47fd7c6c..fd2dd6808b0 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3077,6 +3077,24 @@ create_agg_path(PlannerInfo *root,
 				List *qual,
 				const AggClauseCosts *aggcosts,
 				double numGroups)
+{
+	return create_agg_path_ext(root, rel, subpath, target,
+							   aggstrategy, aggsplit, groupClause,
+							   qual, aggcosts, numGroups, NIL);
+}
+
+AggPath *
+create_agg_path_ext(PlannerInfo *root,
+					RelOptInfo *rel,
+					Path *subpath,
+					PathTarget *target,
+					AggStrategy aggstrategy,
+					AggSplit aggsplit,
+					List *groupClause,
+					List *qual,
+					const AggClauseCosts *aggcosts,
+					double numGroups,
+					List *distinctSortClause)
 {
 	AggPath    *pathnode = makeNode(AggPath);
 
@@ -3115,6 +3133,7 @@ create_agg_path(PlannerInfo *root,
 	pathnode->numGroups = numGroups;
 	pathnode->transitionSpace = aggcosts ? aggcosts->transitionSpace : 0;
 	pathnode->groupClause = groupClause;
+	pathnode->distinctSortClause = distinctSortClause;
 	pathnode->qual = qual;
 
 	cost_agg(&pathnode->path, root,
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 505f0114bfe..232133f7470 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1418,6 +1418,33 @@ count_rowexpr_columns(ParseState *pstate, Node *expr)
  * Note: this covers only cases with no set operations and no VALUES lists;
  * see below for the other cases.
  */
+static List *
+prepend_distinct_to_sortby(List *distinctClause, List *distinctSortClause)
+{
+	List	   *result = list_copy(distinctSortClause);
+	ListCell   *lc;
+	List	   *prepended = NIL;
+
+	/* If distinctClause is empty or has NULL (SELECT DISTINCT), do nothing */
+	if (distinctClause == NIL || linitial(distinctClause) == NULL)
+		return distinctSortClause;
+
+	foreach(lc, distinctClause)
+	{
+		Node	   *key = (Node *) lfirst(lc);
+		SortBy	   *sb = makeNode(SortBy);
+
+		sb->node = key;
+		sb->sortby_dir = SORTBY_DEFAULT;
+		sb->sortby_nulls = SORTBY_NULLS_DEFAULT;
+		sb->useOp = NIL;
+		sb->location = -1;
+		prepended = lappend(prepended, sb);
+	}
+
+	return list_concat(prepended, result);
+}
+
 static Query *
 transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 					SelectStmtPassthrough *passthru)
@@ -1425,6 +1452,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 	Query	   *qry = makeNode(Query);
 	Node	   *qual;
 	ListCell   *l;
+	List	   *distinctSortClause = NIL;
 
 	qry->commandType = CMD_SELECT;
 
@@ -1497,6 +1525,17 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 											false /* allow SQL92 rules */ );
 	qry->groupDistinct = stmt->groupDistinct;
 
+	if (stmt->distinctSortClause)
+	{
+		List *full_sortby = prepend_distinct_to_sortby(stmt->distinctClause, stmt->distinctSortClause);
+		distinctSortClause = transformSortClause(pstate,
+												 full_sortby,
+												 &qry->targetList,
+												 EXPR_KIND_ORDER_BY,
+												 false);
+	}
+	qry->distinctSortClause = distinctSortClause;
+
 	if (stmt->distinctClause == NIL)
 	{
 		qry->distinctClause = NIL;
@@ -1517,7 +1556,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 		qry->distinctClause = transformDistinctOnClause(pstate,
 														stmt->distinctClause,
 														&qry->targetList,
-														qry->sortClause);
+														distinctSortClause ? distinctSortClause : qry->sortClause);
 		qry->hasDistinctOn = true;
 	}
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..a8bb0a4d8d5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -13231,7 +13231,8 @@ simple_select:
 				{
 					SelectStmt *n = makeNode(SelectStmt);
 
-					n->distinctClause = $2;
+					n->distinctClause = linitial($2);
+					n->distinctSortClause = lsecond($2);
 					n->targetList = $3;
 					n->intoClause = $4;
 					n->fromClause = $5;
@@ -13485,8 +13486,9 @@ set_quantifier:
  * should be placed in the DISTINCT list during parsetree analysis.
  */
 distinct_clause:
-			DISTINCT								{ $$ = list_make1(NIL); }
-			| DISTINCT ON '(' expr_list ')'			{ $$ = $4; }
+			DISTINCT								{ $$ = list_make2(list_make1(NIL), NIL); }
+			| DISTINCT ON '(' expr_list ')'			{ $$ = list_make2($4, NIL); }
+			| DISTINCT ON '(' expr_list sort_clause ')' { $$ = list_make2($4, $5); }
 		;
 
 opt_all_clause:
@@ -17951,7 +17953,16 @@ PLpgSQL_Expr: opt_distinct_clause opt_target_list
 				{
 					SelectStmt *n = makeNode(SelectStmt);
 
-					n->distinctClause = $1;
+					if ($1)
+					{
+						n->distinctClause = linitial($1);
+						n->distinctSortClause = lsecond($1);
+					}
+					else
+					{
+						n->distinctClause = NIL;
+						n->distinctSortClause = NIL;
+					}
 					n->targetList = $2;
 					n->fromClause = $3;
 					n->whereClause = $4;
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 152a1dfa568..2d8bd77284d 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -153,6 +153,12 @@ extern uint32 TupleHashTableHash(TupleHashTable hashtable,
 extern TupleHashEntry LookupTupleHashEntryHash(TupleHashTable hashtable,
 											   TupleTableSlot *slot,
 											   bool *isnew, uint32 hash);
+extern bool ReplaceTupleHashEntryIfBetter(TupleHashTable hashtable,
+										  TupleHashEntry entry,
+										  TupleTableSlot *newslot,
+										  TupleTableSlot *firstslot,
+										  SortSupport sortKeys,
+										  int numSortCols);
 extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 TupleTableSlot *slot,
 										 ExprState *eqcomp,
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 1e1be9666ae..e9f0cddec32 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -319,6 +319,8 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	TupleTableSlot *hash_firstTupleSlot; /* slot for comparing stored tuples */
+	SortSupport  sortKeys;		/* sort support for distinct ON comparisons */
 } AggStatePerHashData;
 
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0debcd193ab..6beecfcdfb3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -226,6 +226,8 @@ typedef struct Query
 
 	List	   *distinctClause; /* a list of SortGroupClause's */
 
+	List	   *distinctSortClause; /* a list of SortGroupClause's for inline DISTINCT ON ORDER BY */
+
 	List	   *sortClause;		/* a list of SortGroupClause's */
 
 	Node	   *limitOffset;	/* # of result tuples to skip (int8 expr) */
@@ -2269,6 +2271,7 @@ typedef struct SelectStmt
 	struct SelectStmt *larg;	/* left child */
 	struct SelectStmt *rarg;	/* right child */
 	/* Eventually add fields for CORRESPONDING spec here */
+	List	   *distinctSortClause; /* inline DISTINCT ON ORDER BY clause (list of SortBy's) */
 } SelectStmt;
 
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 8615db98a55..d798a631a29 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2596,6 +2596,7 @@ typedef struct AggPath
 	Cardinality numGroups;		/* estimated number of groups in input */
 	uint64		transitionSpace;	/* for pass-by-ref transition data */
 	List	   *groupClause;	/* a list of SortGroupClause's */
+	List	   *distinctSortClause; /* a list of SortGroupClause's for inline DISTINCT ON ORDER BY */
 	List	   *qual;			/* quals (HAVING quals), if any */
 } AggPath;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 9e538f3c44a..b687edb3a9f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1242,6 +1242,13 @@ typedef struct Agg
 	/* grouping sets to use */
 	List	   *groupingSets;
 
+	/* sort keys for inline DISTINCT ON ORDER BY (if any) */
+	int			numSortCols;
+	AttrNumber *sortColIdx pg_node_attr(array_size(numSortCols));
+	Oid		   *sortOperators pg_node_attr(array_size(numSortCols));
+	Oid		   *sortCollations pg_node_attr(array_size(numSortCols));
+	bool	   *sortNullsFirst pg_node_attr(array_size(numSortCols));
+
 	/* chained Agg/Sort nodes */
 	List	   *chain;
 } Agg;
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index da2d9b384b5..d514eb0c3ba 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -269,6 +269,17 @@ extern AggPath *create_agg_path(PlannerInfo *root,
 								List *qual,
 								const AggClauseCosts *aggcosts,
 								double numGroups);
+extern AggPath *create_agg_path_ext(PlannerInfo *root,
+									RelOptInfo *rel,
+									Path *subpath,
+									PathTarget *target,
+									AggStrategy aggstrategy,
+									AggSplit aggsplit,
+									List *groupClause,
+									List *qual,
+									const AggClauseCosts *aggcosts,
+									double numGroups,
+									List *distinctSortClause);
 extern GroupingSetsPath *create_groupingsets_path(PlannerInfo *root,
 												  RelOptInfo *rel,
 												  Path *subpath,
diff --git a/src/test/regress/expected/select_distinct_on.out b/src/test/regress/expected/select_distinct_on.out
index 75b1e7d300f..4c24a8b7fdb 100644
--- a/src/test/regress/expected/select_distinct_on.out
+++ b/src/test/regress/expected/select_distinct_on.out
@@ -246,3 +246,78 @@ SELECT DISTINCT ON (y, x) x, y FROM (select * from distinct_on_tbl order by x, z
 
 RESET enable_hashagg;
 DROP TABLE distinct_on_tbl;
+--
+-- Test SELECT DISTINCT ON with inline ORDER BY
+--
+CREATE TABLE distinct_inline_tbl (a int, b int);
+INSERT INTO distinct_inline_tbl VALUES (1, 10), (1, 20), (2, 5), (2, 15);
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+                 QUERY PLAN                  
+---------------------------------------------
+ Unique
+   ->  Sort
+         Sort Key: a, b DESC
+         ->  Seq Scan on distinct_inline_tbl
+(4 rows)
+
+SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+ a | b  
+---+----
+ 1 | 20
+ 2 | 15
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+                 QUERY PLAN                  
+---------------------------------------------
+ Unique
+   ->  Sort
+         Sort Key: a, b
+         ->  Seq Scan on distinct_inline_tbl
+(4 rows)
+
+SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+ a | b  
+---+----
+ 1 | 10
+ 2 |  5
+(2 rows)
+
+-- Test with HashAgg
+SET enable_sort TO OFF;
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl;
+                 QUERY PLAN                  
+---------------------------------------------
+ Unique
+   ->  Sort
+         Disabled: true
+         Sort Key: a, b DESC
+         ->  Seq Scan on distinct_inline_tbl
+(5 rows)
+
+SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+ a | b  
+---+----
+ 1 | 20
+ 2 | 15
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl;
+                 QUERY PLAN                  
+---------------------------------------------
+ Unique
+   ->  Sort
+         Disabled: true
+         Sort Key: a, b
+         ->  Seq Scan on distinct_inline_tbl
+(5 rows)
+
+SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+ a | b  
+---+----
+ 1 | 10
+ 2 |  5
+(2 rows)
+
+RESET enable_sort;
+DROP TABLE distinct_inline_tbl;
diff --git a/src/test/regress/sql/select_distinct_on.sql b/src/test/regress/sql/select_distinct_on.sql
index 8680749e49a..df844119938 100644
--- a/src/test/regress/sql/select_distinct_on.sql
+++ b/src/test/regress/sql/select_distinct_on.sql
@@ -82,3 +82,25 @@ SELECT DISTINCT ON (y, x) x, y FROM (select * from distinct_on_tbl order by x, z
 RESET enable_hashagg;
 
 DROP TABLE distinct_on_tbl;
+
+--
+-- Test SELECT DISTINCT ON with inline ORDER BY
+--
+CREATE TABLE distinct_inline_tbl (a int, b int);
+INSERT INTO distinct_inline_tbl VALUES (1, 10), (1, 20), (2, 5), (2, 15);
+
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+
+-- Test with HashAgg
+SET enable_sort TO OFF;
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl;
+SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a;
+EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl;
+SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a;
+RESET enable_sort;
+
+DROP TABLE distinct_inline_tbl;
+
-- 
2.56.0.176.g3575e65682-goog

