From 6a4b57ba2e5ba0a5e28b952400c07932e84a62df Mon Sep 17 00:00:00 2001
From: TatsuyaKawata <kawatatatsuya0913@gmail.com>
Date: Sun, 2 Aug 2026 19:30:55 +0900
Subject: [PATCH] Don't disable abbreviated keys for bounded heap sorts

COMPARETUP already falls back to the real value on an abbreviated
tie, so bounded heap comparisons work fine without an eager restore
at switch time. Track the post-switch heap-survival rate instead,
and abandon abbreviation only once it stops paying off.
---
 src/backend/utils/sort/tuplesort.c | 69 ++++++++++++++++++++++++------
 1 file changed, 56 insertions(+), 13 deletions(-)

diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c
index c0e7527b9ca..0ca3beb78b1 100644
--- a/src/backend/utils/sort/tuplesort.c
+++ b/src/backend/utils/sort/tuplesort.c
@@ -328,6 +328,15 @@ struct Tuplesortstate
 	int64		abbrevNext;		/* Tuple # at which to next check
 								 * applicability */
 
+	/*
+	 * Tracks whether abbreviation is still worth its cost once we're
+	 * maintaining a bounded heap.  See tuplesort_puttuple_common().
+	 */
+	int64		boundedArrivals;	/* tuples seen since entering TSS_BOUNDED */
+	int64		boundedReplacements;	/* of those, how many replaced the heap root */
+	int64		boundedAbbrevNext;	/* boundedArrivals value at which to next
+									 * reconsider abbreviation's usefulness */
+
 	/*
 	 * Resource snapshot for time of sort start.
 	 */
@@ -759,19 +768,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;
 }
 
 /*
@@ -1189,6 +1185,42 @@ tuplesort_puttuple_common(Tuplesortstate *state, SortTuple *tuple,
 				/* discard top of heap, replacing it with the new tuple */
 				free_sort_tuple(state, &state->memtuples[0]);
 				tuplesort_heap_replace_top(state, tuple);
+				state->boundedReplacements++;
+			}
+
+			/*
+			 * Each arrival here is compared against the root once and
+			 * usually discarded, so abbreviation's cost isn't amortized
+			 * the way it is for an ordinary sort.  Periodically check the
+			 * survival rate and give up on abbreviation if it's low; the
+			 * restore is cheap since the heap holds only state->bound
+			 * tuples.
+			 */
+			if (state->base.sortKeys->abbrev_converter != NULL)
+			{
+				state->boundedArrivals++;
+				if (state->boundedArrivals >= state->boundedAbbrevNext)
+				{
+					state->boundedAbbrevNext *= 2;
+
+					/* less than 5% of arrivals since the switch survived */
+					if (state->boundedReplacements * 20 < state->boundedArrivals)
+					{
+						SortSupport sortKey = state->base.sortKeys;
+
+						if (trace_sort)
+							elog(LOG, "abandoning abbreviation in bounded heap after %lld arrivals (%lld replacements): %s",
+								 (long long) state->boundedArrivals,
+								 (long long) state->boundedReplacements,
+								 pg_rusage_show(&state->ru_start));
+
+						REMOVEABBREV(state, state->memtuples, state->memtupcount);
+						sortKey->comparator = sortKey->abbrev_full_comparator;
+						sortKey->abbrev_converter = NULL;
+						sortKey->abbrev_abort = NULL;
+						sortKey->abbrev_full_comparator = NULL;
+					}
+				}
 			}
 			break;
 
@@ -2491,6 +2523,17 @@ make_bounded_heap(Tuplesortstate *state)
 	Assert(tupcount >= state->bound);
 	Assert(SERIAL(state));
 
+	/*
+	 * Don't restore/disable abbreviation here: COMPARETUP already falls
+	 * back to the real value on an abbreviated tie (e.g.
+	 * comparetup_heap_tiebreak), so it works fine on abbreviated tuples.
+	 * tuplesort_puttuple_common() decides separately whether to abandon
+	 * abbreviation once we're maintaining the heap.
+	 */
+	state->boundedArrivals = 0;
+	state->boundedReplacements = 0;
+	state->boundedAbbrevNext = 1000;
+
 	/* Reverse sort direction so largest entry will be at root */
 	reversedirection(state);
 
-- 
2.34.1

