HashAggregate loses worker spill stats on rescan
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254264psql -h localhost -U postgresBuilt from patchset v1 (message #1), October 26, 2026 at 07:01 PM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t254264_1 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t254264_1 && git checkout t254264_1Patchset v1 (message #1) is on t254264_1
Hi,
HashAgg seems to lose the worker's spill stats when Gather is rescanned.
In the example below, Worker 0 spills on the first scan, but EXPLAIN shows
one batch and no disk usage after the second:
CREATE TABLE hashagg_stats AS
SELECT value AS item, value % 10000 AS bucket
FROM generate_series(1, 1000000) AS value;
ALTER TABLE hashagg_stats SET (parallel_workers = 2);
ANALYZE hashagg_stats;
SET max_parallel_workers_per_gather = 2;
SET max_parallel_workers = 1;
SET parallel_leader_participation = off;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET min_parallel_table_scan_size = 0;
SET work_mem = '64kB';
SET hash_mem_multiplier = 2;
SET enable_sort = off;
EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT limits.maximum,
(SELECT count(*)
FROM (SELECT bucket, count(*) FROM hashagg_stats
WHERE item <= (SELECT limits.maximum)
GROUP BY bucket) AS grouped)
FROM (VALUES (1000000), (10)) AS limits(maximum);
AFAICS, ExecEndAgg() clobbers the shared slot on each worker exit, rather
than combining the stats from successive scans.
Patch attached to keep the memory/disk peaks and add the spill batches.
I think we want to count the initial batch only once, as in the
non-parallel case. Otherwise, wouldn't even a non-spilling rescan add
another batch?
Regards,
Ayush
On Sun, 18 Oct 2026 at 22:55, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
wrote:
Hi,
HashAgg seems to lose the worker's spill stats when Gather is rescanned.
In the example below, Worker 0 spills on the first scan, but EXPLAIN shows
one batch and no disk usage after the second:CREATE TABLE hashagg_stats AS
SELECT value AS item, value % 10000 AS bucket
FROM generate_series(1, 1000000) AS value;
ALTER TABLE hashagg_stats SET (parallel_workers = 2);
ANALYZE hashagg_stats;SET max_parallel_workers_per_gather = 2;
SET max_parallel_workers = 1;
SET parallel_leader_participation = off;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET min_parallel_table_scan_size = 0;
SET work_mem = '64kB';
SET hash_mem_multiplier = 2;
SET enable_sort = off;EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT limits.maximum,
(SELECT count(*)
FROM (SELECT bucket, count(*) FROM hashagg_stats
WHERE item <= (SELECT limits.maximum)
GROUP BY bucket) AS grouped)
FROM (VALUES (1000000), (10)) AS limits(maximum);AFAICS, ExecEndAgg() clobbers the shared slot on each worker exit, rather
than combining the stats from successive scans.Patch attached to keep the memory/disk peaks and add the spill batches.
I think we want to count the initial batch only once, as in the
non-parallel case. Otherwise, wouldn't even a non-spilling rescan add
another batch?Regards,
Ayush
Hi Ayush,
Thanks for the report and patch.
I'm going to reproduce the issue and review the changes. I had a couple of
questions about the batch-counting logic:
-
Is the assumption that hash_batches_used is always at least 1 when
ExecEndAgg() records the worker's statistics guaranteed by the
surrounding code?
-
Could the si->hash_batches_used == 0 check be affected by any rescan or
worker-reuse scenario that would cause the initial batch to be counted more
than once?
I'll also check how the existing tests cover parallel HashAggregate rescans
and whether a regression test would be appropriate.
Regards,
Osama Abdul Qader
Hi,
On Tue, 20 Oct 2026 at 01:49, Osama Abdul Qader
<osamaabdulqaderpostgres@gmail.com> wrote:
On Sun, 18 Oct 2026 at 22:55, Ayush Tiwari <ayushtiwari.slg01@gmail.com> wrote:
Hi,
HashAgg seems to lose the worker's spill stats when Gather is rescanned.
In the example below, Worker 0 spills on the first scan, but EXPLAIN shows
one batch and no disk usage after the second:CREATE TABLE hashagg_stats AS
SELECT value AS item, value % 10000 AS bucket
FROM generate_series(1, 1000000) AS value;
ALTER TABLE hashagg_stats SET (parallel_workers = 2);
ANALYZE hashagg_stats;SET max_parallel_workers_per_gather = 2;
SET max_parallel_workers = 1;
SET parallel_leader_participation = off;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET min_parallel_table_scan_size = 0;
SET work_mem = '64kB';
SET hash_mem_multiplier = 2;
SET enable_sort = off;EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT limits.maximum,
(SELECT count(*)
FROM (SELECT bucket, count(*) FROM hashagg_stats
WHERE item <= (SELECT limits.maximum)
GROUP BY bucket) AS grouped)
FROM (VALUES (1000000), (10)) AS limits(maximum);AFAICS, ExecEndAgg() clobbers the shared slot on each worker exit, rather
than combining the stats from successive scans.Patch attached to keep the memory/disk peaks and add the spill batches.
I think we want to count the initial batch only once, as in the
non-parallel case. Otherwise, wouldn't even a non-spilling rescan add
another batch?Regards,
AyushIs the assumption that hash_batches_used is always at least 1 when
ExecEndAgg() records the worker's statistics guaranteed by the
surrounding code?
AFAICS yes for hashed/mixed aggs. ExecInitAgg() starts it at 1,
hashagg_spill_finish() increments it, and ExecReScanAgg() leaves it alone.
Could the si->hash_batches_used == 0 check be affected by any rescan or
worker-reuse scenario that would cause the initial batch to be counted
more than once?
I don't see one so far. The slot is zeroed by ExecAggInitializeDSM(),
but not on a Gather rescan, so the next worker should just add to the
existing count.
Regards,
Ayush