BUG #19882: Parallel HashAggregate loses worker spill statistics after Gather rescan

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 1548140642(at)qq(dot)com
Subject: BUG #19882: Parallel HashAggregate loses worker spill statistics after Gather rescan
Date: 2026-11-11 13:53:46
Message-ID: 19882-1ff869bb8e72439e@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19882
Logged by: Milo
Email address: 1548140642(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux/amd64
Description:

When a rescannable parallel subtree executes a spilling Partial
HashAggregate and then executes a much smaller scan, EXPLAIN ANALYZE reports
only the last generation's per-worker spill statistics. In the recorded
case, Worker 0 initially used 33 batches and 22288kB of disk, but the
two-pass plan reports one batch and no disk usage for that worker.

Per-worker instrumentation should accumulate statistics across worker
generations that reuse the same slot. The query results remain correct; the
defect is in EXPLAIN's instrumentation.

How to repeat:

Save the following as reproducer.sql and run `psql -X -f reproducer.sql`:

```sql
DROP TABLE IF EXISTS hashagg_data;
CREATE TABLE hashagg_data AS
SELECT value AS item, value % 10000 AS bucket
FROM generate_series(1, 1000000) AS value;
ALTER TABLE hashagg_data SET (parallel_workers = 2);
ANALYZE hashagg_data;

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;

SELECT 'BASELINE_BEGIN';
EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT count(*)
FROM
(SELECT bucket, count(*)
FROM hashagg_data
WHERE item <= 1000000
GROUP BY bucket) AS grouped;
SELECT 'BASELINE_END';

SELECT 'RESCAN_BEGIN';
EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT limits.maximum,
(SELECT count(*)
FROM
(SELECT bucket, count(*)
FROM hashagg_data
WHERE item <= (SELECT limits.maximum)
GROUP BY bucket) AS grouped)
FROM (VALUES (1000000), (10)) AS limits(maximum);
SELECT 'RESCAN_END';

DROP TABLE hashagg_data;
```

The baseline contains:

```
-> Partial HashAggregate (actual rows=10000.00 loops=1)
Worker 0: actual rows=10000.00 loops=1
Batches: 33 Memory Usage: 177kB Disk Usage: 22288kB
```

The rescanned subtree instead contains:

```
-> Gather (actual rows=5005.00 loops=2)
Workers Launched: 1
-> Partial HashAggregate (actual rows=5005.00 loops=2)
Worker 0: actual rows=5005.00 loops=2
Batches: 1 Memory Usage: 65kB
```

The `loops=2` value establishes that the subtree ran twice. Because the
first execution spilled, cumulative worker instrumentation should retain a
batch count greater than one and positive disk usage rather than showing
only the final small execution's values.

The issue was reproduced on PostgreSQL 18.6 on Linux/amd64. This occurs when
a rescannable parallel subtree runs a spilling partial HashAggregate and
later reuses the worker slot for a smaller scan.

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-11-11 14:04:19 BUG #19883: Partition pruning errors on SubPlan inside = ANY array
Previous Message Corey Huinker 2026-11-11 13:46:56 Re: BUG #19857: Stats restore rejects empty arrays, losing range and array stats