From 6438c769744aa31a105b969cb0c08b81cf492146 Mon Sep 17 00:00:00 2001
From: Chengpeng Yan <chengpeng_yan@outlook.com>
Date: Mon, 3 Aug 2026 21:51:47 +0800
Subject: [PATCH v2] Use lazy abbreviation for bounded heap admission

Keep abbreviated keys for tuples resident in a bounded heap, but defer
abbreviating new tuples until they pass the heap boundary.  Cache the
heap root's full leading key for admission comparisons and fall back to
the full tuple tiebreak when needed.

This avoids converting tuples that are immediately discarded and
avoids restoring the whole heap when bounded sorting begins.  Sorting
semantics and SQL-visible behavior are unchanged.
---
 src/backend/utils/sort/tuplesort.c | 68 ++++++++++++++++++++++++------
 1 file changed, 54 insertions(+), 14 deletions(-)

diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c
index c0e7527b9ca..c7478257980 100644
--- a/src/backend/utils/sort/tuplesort.c
+++ b/src/backend/utils/sort/tuplesort.c
@@ -213,6 +213,7 @@ struct Tuplesortstate
 	 * H.  In state SORTEDONTAPE, the array is not used.
 	 */
 	SortTuple  *memtuples;		/* array of SortTuple structs */
+	Datum		boundedRootDatum1;	/* full key; may point into current root */
 	int			memtupcount;	/* number of tuples currently present */
 	int			memtupsize;		/* allocated length of memtuples array */
 	bool		growmemtuples;	/* memtuples' growth still underway? */
@@ -460,6 +461,7 @@ static void mergeonerun(Tuplesortstate *state);
 static void beginmerge(Tuplesortstate *state);
 static bool mergereadnext(Tuplesortstate *state, LogicalTape *srcTape, SortTuple *stup);
 static void dumptuples(Tuplesortstate *state, bool alltuples);
+static void cache_bounded_heap_root_key(Tuplesortstate *state);
 static void make_bounded_heap(Tuplesortstate *state);
 static void sort_bounded_heap(Tuplesortstate *state);
 static void tuplesort_sort_memtuples(Tuplesortstate *state);
@@ -759,19 +761,6 @@ tuplesort_set_bound(Tuplesortstate *state, int64 bound)
 
 	state->bounded = true;
 	state->bound = (int) bound;
-
-	/*
-	 * Bounded sorts are not an effective target for abbreviated key
-	 * optimization.  Disable by setting state to be consistent with no
-	 * abbreviation support.
-	 */
-	state->base.sortKeys->abbrev_converter = NULL;
-	if (state->base.sortKeys->abbrev_full_comparator)
-		state->base.sortKeys->comparator = state->base.sortKeys->abbrev_full_comparator;
-
-	/* Not strictly necessary, but be tidy */
-	state->base.sortKeys->abbrev_abort = NULL;
-	state->base.sortKeys->abbrev_full_comparator = NULL;
 }
 
 /*
@@ -1067,6 +1056,7 @@ tuplesort_puttuple_common(Tuplesortstate *state, SortTuple *tuple,
 						  bool useAbbrev, Size tuplen)
 {
 	MemoryContext oldcontext = MemoryContextSwitchTo(state->base.sortcontext);
+	int			compare;
 
 	Assert(!LEADER(state));
 
@@ -1085,6 +1075,16 @@ tuplesort_puttuple_common(Tuplesortstate *state, SortTuple *tuple,
 		 * abbreviated keys).
 		 */
 	}
+	else if (state->status == TSS_BOUNDED)
+	{
+		/*
+		 * Abbreviate after the tuple passes the heap boundary.
+		 *
+		 * TODO: Keep evaluating abbreviation for tuples that enter the
+		 * bounded heap, and restore its resident keys if abbreviation proves
+		 * ineffective.
+		 */
+	}
 	else if (!consider_abort_common(state))
 	{
 		/* Store abbreviated key representation */
@@ -1178,7 +1178,28 @@ tuplesort_puttuple_common(Tuplesortstate *state, SortTuple *tuple,
 			 * with just this one comparison.  Note that because we currently
 			 * have the sort direction reversed, we must check for <= not >=.
 			 */
-			if (COMPARETUP(state, tuple, &state->memtuples[0]) <= 0)
+			if (state->base.sortKeys->abbrev_converter != NULL)
+			{
+				/* Incoming datum1 is still authoritative. */
+				compare = ApplySortAbbrevFullComparator(tuple->datum1,
+														tuple->isnull1,
+														state->boundedRootDatum1,
+														state->memtuples[0].isnull1,
+														state->base.sortKeys);
+
+				/*
+				 * TODO: If the leading keys are authoritatively equal,
+				 * continue the tiebreak at the second sort key instead of
+				 * comparing the leading key again.
+				 */
+				if (compare == 0)
+					compare = state->base.comparetup_tiebreak(tuple,
+															  &state->memtuples[0], state);
+			}
+			else
+				compare = COMPARETUP(state, tuple, &state->memtuples[0]);
+
+			if (compare <= 0)
 			{
 				/* new tuple <= top of the heap, so we can discard it */
 				free_sort_tuple(state, tuple);
@@ -1186,9 +1207,16 @@ tuplesort_puttuple_common(Tuplesortstate *state, SortTuple *tuple,
 			}
 			else
 			{
+				/* Abbreviate only tuples that will enter the heap. */
+				if (useAbbrev)
+					tuple->datum1 = state->base.sortKeys->abbrev_converter(tuple->datum1,
+																		   state->base.sortKeys);
+
 				/* discard top of heap, replacing it with the new tuple */
 				free_sort_tuple(state, &state->memtuples[0]);
 				tuplesort_heap_replace_top(state, tuple);
+				if (state->base.sortKeys->abbrev_converter != NULL)
+					cache_bounded_heap_root_key(state);
 			}
 			break;
 
@@ -2471,6 +2499,16 @@ tuplesort_space_type_name(TuplesortSpaceType t)
  * Heap manipulation routines, per Knuth's Algorithm 5.2.3H.
  */
 
+static void
+cache_bounded_heap_root_key(Tuplesortstate *state)
+{
+	SortTuple	root = state->memtuples[0];
+
+	Assert(state->base.sortKeys->abbrev_converter != NULL);
+	REMOVEABBREV(state, &root, 1);
+	state->boundedRootDatum1 = root.datum1;
+}
+
 /*
  * Convert the existing unordered array of SortTuples to a bounded heap,
  * discarding all but the smallest "state->bound" tuples.
@@ -2524,6 +2562,8 @@ make_bounded_heap(Tuplesortstate *state)
 
 	Assert(state->memtupcount == state->bound);
 	state->status = TSS_BOUNDED;
+	if (state->base.sortKeys->abbrev_converter != NULL)
+		cache_bounded_heap_root_key(state);
 }
 
 /*
-- 
2.50.1 (Apple Git-155)

