#6404·paradedb

Parallel Base Scan workers stopped reporting queries in `EXPLAIN ANALYZE`

Author: mdashtiCreated Sep 18, 2026Updated Sep 18, 2026

What?

Since #6348, EXPLAIN ANALYZE on a parallel Base Scan shows query_count: 0 for every worker, and Queries counts only the leader.

sql
SET max_parallel_workers_per_gather = 2;
SET parallel_leader_participation = off;
SET enable_indexscan TO off;

CREATE TABLE tel_probe (id SERIAL8 PRIMARY KEY, uuid UUID, age INTEGER, rating INTEGER);
CREATE INDEX tel_probe_idx ON tel_probe USING paradedb (id, uuid, age)
WITH (
    text_fields = '{"uuid": {"tokenizer": {"type": "keyword"}, "fast": true}}',
    numeric_fields = '{"age": {"fast": true}}'
);

-- One segment per insert.
SET paradedb.global_mutable_segment_rows TO 0;
INSERT INTO tel_probe (uuid, age, rating)
SELECT rpad(lpad((i * 7919)::text, 10, '0'), 32, '0')::uuid, 20, 4 FROM generate_series(1, 6) i;
INSERT INTO tel_probe (uuid, age, rating)
SELECT rpad(lpad((i * 7919)::text, 10, '0'), 32, '0')::uuid, 20, 4 FROM generate_series(7, 12) i;
INSERT INTO tel_probe (uuid, age, rating)
SELECT rpad(lpad((i * 7919)::text, 10, '0'), 32, '0')::uuid, 20, 4 FROM generate_series(13, 18) i;
RESET paradedb.global_mutable_segment_rows;
ANALYZE tel_probe;

EXPLAIN (ANALYZE, VERBOSE, COSTS OFF, TIMING OFF)
SELECT age FROM tel_probe WHERE rating = 4 AND age @@@ '20' ORDER BY uuid LIMIT 5;

On main (PG18), both workers claim segments but report no queries:

Workers Launched: 2
...
Parallel Workers: {"0":{"query_count":0,"claimed_segments":[<1 segment>]},"1":{"query_count":0,"claimed_segments":[<2 segments>]}}
   Queries: 0

With the take() from #6348 reverted, it's query_count: 1 for each worker and Queries: 2.

Why?

EXPLAIN ANALYZE is how we check that the workers did the work. With every count at 0, we can't tell. No test covers worker telemetry, so CI stayed green.

How?

shutdown_custom_scan evaluates scan_state.parallel.take() before && parallel.is_leader(), so it clears the handle in workers too. A worker's ExecutorRun calls ExecShutdownNode before ExecutorEnd. So when end_custom_scan runs, its take() returns None and publish_telemetry never runs.

Publish from the worker at shutdown and keep the take(), so a second shutdown stays a no-op (#6374):

rust
fn shutdown_custom_scan(state: &mut CustomScanStateWrapper<Self>) {
    let scan_state = state.custom_state_mut();
    if let Some(parallel) = scan_state.parallel.take() {
        if parallel.is_leader() {
            parallel.finalize_explain(&mut scan_state.telemetry);
        } else {
            parallel.publish_telemetry(&scan_state.telemetry);
        }
    }
}

Then drop the worker branch in end_custom_scan. In a worker, ExecShutdownNode runs before dest->rShutdown detaches the tuple queue. So a leader that read every row already sees the counts when its own shutdown runs. A regress test with parallel_leader_participation = off and EXPLAIN (ANALYZE, VERBOSE) would cover it.