From 62e967ee84e3ae471b0cd93cef7ca03b69a2ca20 Mon Sep 17 00:00:00 2001
From: zhenglong li <smartkeyerror@gmail.com>
Date: Wed, 28 Oct 2026 15:47:12 +0800
Subject: [PATCH v3 1/2] Ignore sort direction when matching GROUP BY keys to
 input pathkeys

get_useful_group_keys_orderings() tries to reorder the GROUP BY keys to
match the ordering of the input path, so that a presorted input such as
an index scan can be used for the grouping step without an extra sort.
However, both group_keys_reorder_by_pathkeys() and the GROUP BY check in
truncate_useless_pathkeys() match pathkeys by pointer, which requires the
input ordering to agree with the GROUP BY keys in sort direction and
NULLS ordering as well.

That is more than grouping needs.  Grouping only requires equal values to
be adjacent, which any ordering by the same equivalence class and operator
family guarantees, whichever its direction.  The sort direction of a GROUP
BY key is merely taken from a matching ORDER BY item by the parser, to
make it likely that one sort can serve both steps.  So when the ORDER BY
sorts in a direction the input path doesn't provide, as in

    CREATE INDEX ON t (b, c);
    SELECT b, c, count(*) FROM t GROUP BY b, c ORDER BY b, c DESC;

the grouping keys are b ASC, c DESC, and the index ordering b ASC, c ASC
is useful only for its first key.  We then sort the whole input by b, c
DESC incrementally, rather than grouping straight off the index and
sorting only the (typically far fewer) groups for the ORDER BY.

Teach group_keys_reorder_by_pathkeys() to accept an input pathkey that
matches a GROUP BY pathkey in equivalence class and operator family,
regardless of its sort direction and NULLS ordering.  In that case the
reordered GROUP BY keys use the input pathkey, along with a copy of the
GROUP BY clause whose sort operator and NULLS ordering are adjusted to
agree with it.  Likewise, make truncate_useless_pathkeys() consider such
pathkeys useful for GROUP BY, so that they survive to be matched.  The
aggregate pathkeys that may follow the grouping keys in group_pathkeys
are still matched exactly, since an aggregate's ORDER BY does depend on
the sort direction.

Do the same for the partial aggregation that eager aggregation pushes
below a join.  generate_grouped_paths() required its input to be sorted
exactly by the grouping pathkeys, so once the GROUP BY keys took a
direction the input path doesn't provide, the partial aggregation had
to sort its whole input, and then lost out to a plan without it, as the
final aggregation can now group straight off the input.  Partial
aggregation compares the grouping keys only for equality, and its output
keeps the ordering of its input, so let it take the sort direction from
the input path, using the new function adjust_group_pathkeys_to_input().

If the operator family lacks the sort operator for the direction of the
input pathkey, which is possible only with an operator class that does
not follow the btree rules, stop matching there rather than raise an
error, since the GROUP BY keys can still be sorted in their own
direction.

As before, this ordering is only offered as an additional candidate, so
it is chosen only if it is cheaper.

Adjust the assertions in get_useful_group_keys_orderings() accordingly,
as the reordered clauses and pathkeys no longer need to be identical to
the original ones, only to cover the same grouping keys.
---
 src/backend/optimizer/path/allpaths.c         |  26 +-
 src/backend/optimizer/path/pathkeys.c         | 234 ++++++++++++++++--
 src/include/optimizer/paths.h                 |   2 +
 src/test/regress/expected/aggregates.out      |  81 ++++++
 src/test/regress/expected/eager_aggregate.out |  57 +++++
 src/test/regress/sql/aggregates.sql           |  42 ++++
 src/test/regress/sql/eager_aggregate.sql      |  28 +++
 7 files changed, 450 insertions(+), 20 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index d52a6d40505..1b9ec2920d9 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -3608,6 +3608,7 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 		{
 			Path	   *input_path = (Path *) lfirst(lc);
 			Path	   *path;
+			List	   *input_group_pathkeys;
 			bool		is_sorted;
 			int			presorted_keys;
 
@@ -3619,7 +3620,16 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 				input_path != cheapest_total_path)
 				continue;
 
-			is_sorted = pathkeys_count_contained_in(group_pathkeys,
+			/*
+			 * Partial aggregation compares the grouping keys only for
+			 * equality, so the input can be sorted on them in any direction.
+			 * Take the direction from the input path where it can provide
+			 * the grouping order.
+			 */
+			input_group_pathkeys =
+				adjust_group_pathkeys_to_input(group_pathkeys,
+											   input_path->pathkeys);
+			is_sorted = pathkeys_count_contained_in(input_group_pathkeys,
 													input_path->pathkeys,
 													&presorted_keys);
 
@@ -3653,13 +3663,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 					path = (Path *) create_sort_path(root,
 													 grouped_rel,
 													 path,
-													 group_pathkeys,
+													 input_group_pathkeys,
 													 -1.0);
 				else
 					path = (Path *) create_incremental_sort_path(root,
 																 grouped_rel,
 																 path,
-																 group_pathkeys,
+																 input_group_pathkeys,
 																 presorted_keys,
 																 -1.0);
 			}
@@ -3692,10 +3702,14 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 		{
 			Path	   *input_path = (Path *) lfirst(lc);
 			Path	   *path;
+			List	   *input_group_pathkeys;
 			bool		is_sorted;
 			int			presorted_keys;
 
-			is_sorted = pathkeys_count_contained_in(group_pathkeys,
+			input_group_pathkeys =
+				adjust_group_pathkeys_to_input(group_pathkeys,
+											   input_path->pathkeys);
+			is_sorted = pathkeys_count_contained_in(input_group_pathkeys,
 													input_path->pathkeys,
 													&presorted_keys);
 
@@ -3729,13 +3743,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 					path = (Path *) create_sort_path(root,
 													 grouped_rel,
 													 path,
-													 group_pathkeys,
+													 input_group_pathkeys,
 													 -1.0);
 				else
 					path = (Path *) create_incremental_sort_path(root,
 																 grouped_rel,
 																 path,
-																 group_pathkeys,
+																 input_group_pathkeys,
 																 presorted_keys,
 																 -1.0);
 			}
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index d6a305aedac..6860c21d82b 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -353,6 +353,35 @@ pathkeys_contained_in(List *keys1, List *keys2)
 	return false;
 }
 
+/*
+ * find_grouping_pathkey
+ *		Find a GROUP BY pathkey that 'pathkey' can stand in for.
+ *
+ * Searches the first 'num_groupby_pathkeys' items of 'group_pathkeys' for a
+ * pathkey with the same equivalence class and operator family as 'pathkey',
+ * ignoring sort direction and NULLS ordering.  Returns NULL if there is none.
+ * Pathkeys are canonical, so there can be at most one.
+ */
+static PathKey *
+find_grouping_pathkey(List *group_pathkeys, int num_groupby_pathkeys,
+					  PathKey *pathkey)
+{
+	ListCell   *lc;
+
+	foreach(lc, group_pathkeys)
+	{
+		PathKey    *gpk = (PathKey *) lfirst(lc);
+
+		if (foreach_current_index(lc) >= num_groupby_pathkeys)
+			break;
+		if (gpk->pk_eclass == pathkey->pk_eclass &&
+			gpk->pk_opfamily == pathkey->pk_opfamily)
+			return gpk;
+	}
+
+	return NULL;
+}
+
 /*
  * group_keys_reorder_by_pathkeys
  *		Reorder GROUP BY pathkeys and clauses to match the input pathkeys.
@@ -364,6 +393,13 @@ pathkeys_contained_in(List *keys1, List *keys2)
  * 'num_groupby_pathkeys' is the number of first '*group_pathkeys' items to
  *		search matching pathkeys.
  *
+ * An input pathkey matches a GROUP BY pathkey if both have the same
+ * equivalence class and operator family, even if they differ in sort
+ * direction or NULLS ordering.  Grouping only requires that equal values be
+ * adjacent, which the input ordering then guarantees, so in that case we use
+ * the input pathkey in place of the GROUP BY pathkey and a copy of the GROUP
+ * BY clause with its sort ordering adjusted to agree with it.
+ *
  * Returns the number of GROUP BY keys with a matching pathkey.
  */
 static int
@@ -374,6 +410,8 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
 	List	   *new_group_pathkeys = NIL,
 			   *new_group_clauses = NIL;
 	List	   *grouping_pathkeys;
+	List	   *replaced_pathkeys = NIL,
+			   *replaced_clauses = NIL;
 	ListCell   *lc;
 	int			n;
 
@@ -403,18 +441,24 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
 	foreach(lc, pathkeys)
 	{
 		PathKey    *pathkey = (PathKey *) lfirst(lc);
+		PathKey    *group_pathkey;
 		SortGroupClause *sgc;
+		SortGroupClause *new_sgc;
 
 		/*
-		 * Pathkeys are built in a way that allows simply comparing pointers.
-		 * Give up if we can't find the matching pointer.  Also give up if
+		 * Give up if we've already matched all the grouping pathkeys, or if
 		 * there is no sortclause reference for some reason.
 		 */
 		if (foreach_current_index(lc) >= num_groupby_pathkeys ||
-			!list_member_ptr(grouping_pathkeys, pathkey) ||
 			pathkey->pk_eclass->ec_sortref == 0)
 			break;
 
+		/* Give up if there's no grouping pathkey this one can stand in for */
+		group_pathkey = find_grouping_pathkey(grouping_pathkeys,
+											  num_groupby_pathkeys, pathkey);
+		if (group_pathkey == NULL)
+			break;
+
 		/*
 		 * Since 1349d27 pathkey coming from underlying node can be in the
 		 * root->group_pathkeys but not in the processed_groupClause. So, we
@@ -432,23 +476,117 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
 		 */
 		Assert(OidIsValid(sgc->sortop));
 
+		/*
+		 * If the input pathkey's ordering differs from that of the GROUP BY
+		 * pathkey, make a copy of the clause that agrees with the input
+		 * pathkey.
+		 */
+		new_sgc = sgc;
+		if (pathkey != group_pathkey)
+		{
+			bool		reverse_sort = (pathkey->pk_cmptype == COMPARE_GT);
+			Oid			sortop = sgc->sortop;
+
+			if (sgc->reverse_sort != reverse_sort)
+			{
+				Oid			lefttype,
+							righttype;
+
+				op_input_types(sgc->sortop, &lefttype, &righttype);
+				sortop = get_opfamily_member_for_cmptype(pathkey->pk_opfamily,
+														 lefttype,
+														 righttype,
+														 pathkey->pk_cmptype);
+
+				/*
+				 * Give up if the operator family lacks the operator to sort
+				 * in the input pathkey's direction.  That is possible only
+				 * with an operator class that doesn't follow the btree rules,
+				 * but we needn't fail on it, as the GROUP BY keys can still
+				 * be sorted in their own direction.
+				 */
+				if (!OidIsValid(sortop))
+					break;
+			}
+
+			new_sgc = copyObject(sgc);
+			new_sgc->sortop = sortop;
+			new_sgc->reverse_sort = reverse_sort;
+			new_sgc->nulls_first = pathkey->pk_nulls_first;
+		}
+
+		replaced_pathkeys = lappend(replaced_pathkeys, group_pathkey);
+		replaced_clauses = lappend(replaced_clauses, sgc);
 		new_group_pathkeys = lappend(new_group_pathkeys, pathkey);
-		new_group_clauses = lappend(new_group_clauses, sgc);
+		new_group_clauses = lappend(new_group_clauses, new_sgc);
 	}
 
 	/* remember the number of pathkeys with a matching GROUP BY key */
 	n = list_length(new_group_pathkeys);
 
 	/* append the remaining group pathkeys (will be treated as not sorted) */
-	*group_pathkeys = list_concat_unique_ptr(new_group_pathkeys,
-											 *group_pathkeys);
-	*group_clauses = list_concat_unique_ptr(new_group_clauses,
-											*group_clauses);
+	foreach(lc, *group_pathkeys)
+	{
+		PathKey    *pathkey = (PathKey *) lfirst(lc);
+
+		if (!list_member_ptr(replaced_pathkeys, pathkey))
+			new_group_pathkeys = list_append_unique_ptr(new_group_pathkeys,
+														pathkey);
+	}
+	foreach(lc, *group_clauses)
+	{
+		SortGroupClause *sgc = (SortGroupClause *) lfirst(lc);
+
+		if (!list_member_ptr(replaced_clauses, sgc))
+			new_group_clauses = lappend(new_group_clauses, sgc);
+	}
+	*group_pathkeys = new_group_pathkeys;
+	*group_clauses = new_group_clauses;
 
 	list_free(grouping_pathkeys);
+	list_free(replaced_pathkeys);
+	list_free(replaced_clauses);
 	return n;
 }
 
+/*
+ * adjust_group_pathkeys_to_input
+ *		Make grouping pathkeys agree in sort direction with the input pathkeys.
+ *
+ * Returns a copy of 'group_pathkeys' in which each leading pathkey that has
+ * the same equivalence class and operator family as the pathkey at the same
+ * position of 'pathkeys' is replaced by the latter, even if they differ in
+ * sort direction or NULLS ordering.  Grouping only requires that equal values
+ * be adjacent, so sorting by the result is as good for grouping as sorting by
+ * 'group_pathkeys', and an input path sorted by 'pathkeys' may satisfy it
+ * where it would not satisfy 'group_pathkeys'.
+ *
+ * This is meant for grouping steps that don't care about the sort operators
+ * of their grouping clauses, such as partial aggregation, which compares the
+ * grouping keys only for equality and leaves the final aggregation to provide
+ * any ordering the query requires.
+ */
+List *
+adjust_group_pathkeys_to_input(List *group_pathkeys, List *pathkeys)
+{
+	List	   *result = list_copy(group_pathkeys);
+	ListCell   *lc1,
+			   *lc2;
+
+	forboth(lc1, result, lc2, pathkeys)
+	{
+		PathKey    *group_pathkey = (PathKey *) lfirst(lc1);
+		PathKey    *pathkey = (PathKey *) lfirst(lc2);
+
+		if (group_pathkey->pk_eclass != pathkey->pk_eclass ||
+			group_pathkey->pk_opfamily != pathkey->pk_opfamily)
+			break;
+		lfirst(lc1) = pathkey;
+	}
+
+	return result;
+}
+
 /*
  * get_useful_group_keys_orderings
  *		Determine which orderings of GROUP BY keys are potentially interesting.
@@ -461,7 +599,9 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
  *
  * - GROUP BY keys as ordered by preprocess_groupclause() to match target
  *   ORDER BY clause (as much as possible),
- * - GROUP BY keys reordered to match 'path' ordering (as much as possible).
+ * - GROUP BY keys reordered to match 'path' ordering (as much as possible),
+ *   taking on the sort direction and NULLS ordering of 'path' where they
+ *   differ from those of the GROUP BY keys.
  */
 List *
 get_useful_group_keys_orderings(PlannerInfo *root, Path *path)
@@ -533,8 +673,39 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path)
 
 			Assert(list_length(info->clauses) == list_length(pinfo->clauses));
 			Assert(list_length(info->pathkeys) == list_length(pinfo->pathkeys));
-			Assert(list_difference(info->clauses, pinfo->clauses) == NIL);
-			Assert(list_difference_ptr(info->pathkeys, pinfo->pathkeys) == NIL);
+
+			/*
+			 * The clauses and pathkeys may differ from the original ones in
+			 * sort ordering, but they must cover the same GROUP BY items and
+			 * equivalence classes.
+			 */
+			foreach(lc1, info->clauses)
+			{
+				SortGroupClause *sgc = lfirst_node(SortGroupClause, lc1);
+				SortGroupClause *psgc;
+
+				psgc = get_sortgroupref_clause(sgc->tleSortGroupRef,
+											   pinfo->clauses);
+				Assert(sgc->eqop == psgc->eqop);
+			}
+			foreach(lc1, info->pathkeys)
+			{
+				PathKey    *pk = lfirst_node(PathKey, lc1);
+				bool		found = false;
+
+				foreach(lc2, pinfo->pathkeys)
+				{
+					PathKey    *ppk = lfirst_node(PathKey, lc2);
+
+					if (ppk->pk_eclass == pk->pk_eclass &&
+						ppk->pk_opfamily == pk->pk_opfamily)
+					{
+						found = true;
+						break;
+					}
+				}
+				Assert(found);
+			}
 
 			forboth(lc1, info->clauses, lc2, info->pathkeys)
 			{
@@ -542,6 +713,8 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path)
 				PathKey    *pk = lfirst_node(PathKey, lc2);
 
 				Assert(pk->pk_eclass->ec_sortref == sgc->tleSortGroupRef);
+				Assert(sgc->reverse_sort == (pk->pk_cmptype == COMPARE_GT));
+				Assert(sgc->nulls_first == pk->pk_nulls_first);
 			}
 		}
 	}
@@ -2190,6 +2363,38 @@ count_common_leading_pathkeys_unordered(List *keys1, List *keys2)
 	return ncommon;
 }
 
+/*
+ * count_common_leading_group_pathkeys
+ *		Returns the number of leading PathKeys in 'pathkeys' which are useful
+ *		for GROUP BY.
+ *
+ * This is like count_common_leading_pathkeys_unordered() applied to
+ * root->group_pathkeys, except that a pathkey matching a grouping key in
+ * everything but sort direction and NULLS ordering counts as useful too.
+ */
+static int
+count_common_leading_group_pathkeys(PlannerInfo *root, List *pathkeys)
+{
+	int			ncommon = 0;
+
+	/* No point in searching pathkeys when there are no GROUP BY pathkeys */
+	if (root->group_pathkeys == NIL)
+		return 0;
+
+	foreach_node(PathKey, pathkey, pathkeys)
+	{
+		if (!list_member_ptr(root->group_pathkeys, pathkey) &&
+			find_grouping_pathkey(root->group_pathkeys,
+								  root->num_groupby_pathkeys,
+								  pathkey) == NULL)
+			break;
+
+		ncommon++;
+	}
+
+	return ncommon;
+}
+
 /*
  * truncate_useless_pathkeys
  *		Shorten the given PathKey List to just the useful PathKeys.  If all
@@ -2238,10 +2443,11 @@ truncate_useless_pathkeys(PlannerInfo *root,
 	 * Check if these pathkeys are useful for GROUP BY or DISTINCT.  The order
 	 * of the pathkeys does not matter here as Unique and GroupAggregate for
 	 * these operations can take advantage of Paths presorted by any of the
-	 * GROUP BY/DISTINCT pathkeys.
+	 * GROUP BY/DISTINCT pathkeys.  For GROUP BY, the sort direction and NULLS
+	 * ordering of the grouping keys don't matter either, as
+	 * group_keys_reorder_by_pathkeys() can make them follow the input path.
 	 */
-	nuseful2 = count_common_leading_pathkeys_unordered(root->group_pathkeys,
-													   pathkeys);
+	nuseful2 = count_common_leading_group_pathkeys(root, pathkeys);
 	if (nuseful2 == ntotal)
 		return pathkeys;
 
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 64f87da1674..a47e4ee54df 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -226,6 +226,8 @@ typedef enum
 extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
 extern bool pathkeys_contained_in(List *keys1, List *keys2);
 extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common);
+extern List *adjust_group_pathkeys_to_input(List *group_pathkeys,
+											List *pathkeys);
 extern List *get_useful_group_keys_orderings(PlannerInfo *root, Path *path);
 extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 											Relids required_outer,
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 7d07619956f..b4afb005ea0 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -3428,6 +3428,87 @@ SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z;
                ->  Index Scan using btg_x_y_idx on btg
 (8 rows)
 
+-- Utilize the ordering of index scan even if it differs in sort direction
+-- from the ordering that the GROUP BY keys took from ORDER BY
+CREATE TABLE btg_dir AS
+  SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i;
+CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y);
+ANALYZE btg_dir;
+EXPLAIN (COSTS OFF)
+SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Incremental Sort
+   Sort Key: x, y DESC
+   Presorted Key: x
+   ->  GroupAggregate
+         Group Key: x, y
+         ->  Index Only Scan using btg_dir_x_y_idx on btg_dir
+(6 rows)
+
+SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6;
+ x | y | count 
+---+---+-------
+ 0 | 4 |    50
+ 0 | 3 |    50
+ 0 | 2 |    50
+ 0 | 1 |    50
+ 0 | 0 |    50
+ 1 | 4 |    50
+(6 rows)
+
+-- Likewise for a backward index scan that also provides the ordering
+-- required by the aggregate
+EXPLAIN (COSTS OFF)
+SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ GroupAggregate
+   Group Key: x
+   ->  Index Only Scan Backward using btg_dir_x_y_idx on btg_dir
+(3 rows)
+
+SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x;
+ x |  array_agg  
+---+-------------
+ 0 | {4,3,2,1,0}
+ 1 | {4,3,2,1,0}
+ 2 | {4,3,2,1,0}
+ 3 | {4,3,2,1,0}
+(4 rows)
+
+DROP TABLE btg_dir;
+-- An index scan in the direction that the operator class has no operator for
+-- must not cause an error, though such an operator class is not valid
+BEGIN;
+CREATE TYPE btg_color AS ENUM ('r', 'g', 'b');
+CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt';
+CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq';
+CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp';
+CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color,
+  FUNCTION = btg_color_lt);
+CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color,
+  FUNCTION = btg_color_eq);
+CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS
+  OPERATOR 1 <<<, OPERATOR 3 ===,
+  FUNCTION 1 btg_color_cmp(btg_color, btg_color);
+CREATE TABLE btg_color_tab AS
+  SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c
+  FROM generate_series(1, 300) AS i;
+CREATE INDEX ON btg_color_tab (c);
+ANALYZE btg_color_tab;
+SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c;
+ c | count 
+---+-------
+ r |   100
+ g |   100
+ b |   100
+(3 rows)
+
+ROLLBACK;
 -- Test the case where the number of incoming subtree path keys is more than
 -- the number of grouping keys.
 CREATE INDEX btg_y_x_w_idx ON btg(y, x, w);
diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out
index b420a125e1c..fb7ee9e56b7 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -600,6 +600,63 @@ GROUP BY s1.a;
 
 DROP TABLE eager_agg_s1;
 DROP TABLE eager_agg_s2;
+-- Ensure the partial aggregation can use an index scan that differs in sort
+-- direction from the ordering that the grouping keys took from ORDER BY
+CREATE TABLE eager_agg_d1 (a int, b int, c int);
+CREATE TABLE eager_agg_d2 (a int);
+INSERT INTO eager_agg_d1
+  SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i;
+INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i;
+CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b);
+ANALYZE eager_agg_d1, eager_agg_d2;
+SET enable_hashagg TO off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT d1.a, d1.b, sum(d1.c)
+  FROM eager_agg_d1 d1
+  JOIN eager_agg_d2 d2 ON d1.a = d2.a
+GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Limit
+   Output: d1.a, d1.b, (sum(d1.c))
+   ->  Incremental Sort
+         Output: d1.a, d1.b, (sum(d1.c))
+         Sort Key: d1.a, d1.b DESC
+         Presorted Key: d1.a
+         ->  Finalize GroupAggregate
+               Output: d1.a, d1.b, sum(d1.c)
+               Group Key: d1.a, d1.b
+               ->  Merge Join
+                     Output: d1.a, d1.b, (PARTIAL sum(d1.c))
+                     Merge Cond: (d1.a = d2.a)
+                     ->  Partial GroupAggregate
+                           Output: d1.a, d1.b, PARTIAL sum(d1.c)
+                           Group Key: d1.a, d1.b
+                           ->  Index Scan using eager_agg_d1_a_b_idx on public.eager_agg_d1 d1
+                                 Output: d1.a, d1.b, d1.c
+                     ->  Sort
+                           Output: d2.a
+                           Sort Key: d2.a
+                           ->  Seq Scan on public.eager_agg_d2 d2
+                                 Output: d2.a
+(22 rows)
+
+SELECT d1.a, d1.b, sum(d1.c)
+  FROM eager_agg_d1 d1
+  JOIN eager_agg_d2 d2 ON d1.a = d2.a
+GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5;
+ a | b  |  sum  
+---+----+-------
+ 0 | 99 | 54900
+ 0 | 98 | 54800
+ 0 | 97 | 54700
+ 0 | 96 | 54600
+ 0 | 95 | 54500
+(5 rows)
+
+RESET enable_hashagg;
+DROP TABLE eager_agg_d1;
+DROP TABLE eager_agg_d2;
 DROP TABLE eager_agg_t1;
 DROP TABLE eager_agg_t2;
 DROP TABLE eager_agg_t3;
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 91f8342166f..bd349c55867 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -1457,6 +1457,48 @@ SELECT count(*) FROM btg GROUP BY w, x, z, y ORDER BY y, x, z, w;
 EXPLAIN (COSTS OFF)
 SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z;
 
+-- Utilize the ordering of index scan even if it differs in sort direction
+-- from the ordering that the GROUP BY keys took from ORDER BY
+CREATE TABLE btg_dir AS
+  SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i;
+CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y);
+ANALYZE btg_dir;
+EXPLAIN (COSTS OFF)
+SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC;
+SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6;
+
+-- Likewise for a backward index scan that also provides the ordering
+-- required by the aggregate
+EXPLAIN (COSTS OFF)
+SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x;
+SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x;
+DROP TABLE btg_dir;
+
+-- An index scan in the direction that the operator class has no operator for
+-- must not cause an error, though such an operator class is not valid
+BEGIN;
+CREATE TYPE btg_color AS ENUM ('r', 'g', 'b');
+CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt';
+CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq';
+CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int
+  LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp';
+CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color,
+  FUNCTION = btg_color_lt);
+CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color,
+  FUNCTION = btg_color_eq);
+CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS
+  OPERATOR 1 <<<, OPERATOR 3 ===,
+  FUNCTION 1 btg_color_cmp(btg_color, btg_color);
+CREATE TABLE btg_color_tab AS
+  SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c
+  FROM generate_series(1, 300) AS i;
+CREATE INDEX ON btg_color_tab (c);
+ANALYZE btg_color_tab;
+SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c;
+ROLLBACK;
+
 -- Test the case where the number of incoming subtree path keys is more than
 -- the number of grouping keys.
 CREATE INDEX btg_y_x_w_idx ON btg(y, x, w);
diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql
index 7e3623aa6fd..347de9c6e5e 100644
--- a/src/test/regress/sql/eager_aggregate.sql
+++ b/src/test/regress/sql/eager_aggregate.sql
@@ -226,6 +226,34 @@ GROUP BY s1.a;
 DROP TABLE eager_agg_s1;
 DROP TABLE eager_agg_s2;
 
+-- Ensure the partial aggregation can use an index scan that differs in sort
+-- direction from the ordering that the grouping keys took from ORDER BY
+CREATE TABLE eager_agg_d1 (a int, b int, c int);
+CREATE TABLE eager_agg_d2 (a int);
+INSERT INTO eager_agg_d1
+  SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i;
+INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i;
+CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b);
+ANALYZE eager_agg_d1, eager_agg_d2;
+
+SET enable_hashagg TO off;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT d1.a, d1.b, sum(d1.c)
+  FROM eager_agg_d1 d1
+  JOIN eager_agg_d2 d2 ON d1.a = d2.a
+GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5;
+
+SELECT d1.a, d1.b, sum(d1.c)
+  FROM eager_agg_d1 d1
+  JOIN eager_agg_d2 d2 ON d1.a = d2.a
+GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5;
+
+RESET enable_hashagg;
+
+DROP TABLE eager_agg_d1;
+DROP TABLE eager_agg_d2;
+
 DROP TABLE eager_agg_t1;
 DROP TABLE eager_agg_t2;
 DROP TABLE eager_agg_t3;
-- 
2.50.1 (Apple Git-155)

