Hi hackers,

Memory allocated for copied pass-by-reference Datums was not accounted
against work_mem because tuplesort_putdatum() passed a hardcoded tuplen
of 0 to tuplesort_puttuple_common(). Function free_sort_tuple() adjusts
the accounting by the amount actually allocated, so freeing such a
tuple subtracts an amount that was never added.

This was introduced in 6ed83d5fa55, which switched non-bounded sorts to
bump contexts. That commit correctly changed the other tuplesort_put*()
functions to compute the size, leaving only this one passing hardcoded
0.

So currently:

1) Bounded datum sorts are misreported.  With work_mem = 4MB:

    EXPLAIN ANALYZE SELECT md5(i::text) AS hash 
      FROM  generate_series(1,100000) i 
      ORDER BY hash LIMIT 5;

    master:  Sort Method: still in progress  Memory: 0kB
    patched: Sort Method: top-N heapsort  Memory: 25kB

2) work_mem is not enforced against the tuple data, and hold more data
than allowed before spilling  With work_mem = 4MB:

    EXPLAIN ANALYZE SELECT md5(i::text) AS hash 
      FROM generate_series(1,100000) i 
      ORDER BY hash;

    master:  Sort Method: quicksort  Memory: 3073kB
    patched: Sort Method: external merge  Disk: 3920kB


The attached patch computes tuplen the way the tuplesort_put*()
variants do.

Thanks,
Mario
From c68763005ae539bb4b8c145c420ed119c62f5282 Mon Sep 17 00:00:00 2001
From: Mario Karuza <[email protected]>
Date: Thu, 10 Sep 2026 22:30:56 +0200
Subject: [PATCH v1] Fix memory accounting for datum sorts of pass-by-reference
 types

Memory allocated for copied pass-by-reference Datums was not accounted
against work_mem because tuplesort_putdatum() passed a hardcoded
tuplen of 0 to tuplesort_puttuple_common(). free_sort_tuple() correctly
adjusts the accounting by the amount actually allocated for the tuple,
so freeing one subtracts an amount that was never added.

Compute tuplen in tuplesort_putdatum() consistently with the other
tuplesort_put*() variants. This ensures that pass-by-reference datum
data is properly accounted.
---
 src/backend/utils/sort/tuplesortvariants.c | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/sort/tuplesortvariants.c b/src/backend/utils/sort/tuplesortvariants.c
index 95b9ff9d39a..c648c78455a 100644
--- a/src/backend/utils/sort/tuplesortvariants.c
+++ b/src/backend/utils/sort/tuplesortvariants.c
@@ -939,6 +939,7 @@ tuplesort_putdatum(Tuplesortstate *state, Datum val, bool isNull)
 	MemoryContext oldcontext = MemoryContextSwitchTo(base->tuplecontext);
 	TuplesortDatumArg *arg = (TuplesortDatumArg *) base->arg;
 	SortTuple	stup;
+	Size		tuplen;
 
 	/*
 	 * Pass-by-value types or null values are just stored directly in
@@ -961,17 +962,26 @@ tuplesort_putdatum(Tuplesortstate *state, Datum val, bool isNull)
 		stup.datum1 = !isNull ? val : (Datum) 0;
 		stup.isnull1 = isNull;
 		stup.tuple = NULL;		/* no separate storage */
+		tuplen = 0;
 	}
 	else
 	{
 		stup.isnull1 = false;
 		stup.datum1 = datumCopy(val, false, arg->datumTypeLen);
 		stup.tuple = DatumGetPointer(stup.datum1);
+
+		/* GetMemoryChunkSpace is not supported for bump contexts */
+		if (TupleSortUseBumpTupleCxt(base->sortopt))
+			tuplen = MAXALIGN(datumGetSize(PointerGetDatum(stup.tuple),
+										   false,
+										   arg->datumTypeLen));
+		else
+			tuplen = GetMemoryChunkSpace(stup.tuple);
 	}
 
 	tuplesort_puttuple_common(state, &stup,
 							  base->tuples &&
-							  base->sortKeys->abbrev_converter && !isNull, 0);
+							  base->sortKeys->abbrev_converter && !isNull, tuplen);
 
 	MemoryContextSwitchTo(oldcontext);
 }
-- 
2.55.0

Reply via email to