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