From 080407d94ff6d87ecbb1f757c26b34959dc6d19d Mon Sep 17 00:00:00 2001 From: Lukas Fittl Date: Sun, 5 Apr 2026 03:39:46 -0700 Subject: [PATCH 1/2] Parallel Bitmap Heap Scan: Fix EXPLAIN reporting of "Heap Blocks" Fix the missing accumulation of "Heap Blocks" from parallel query workers to the leader, causing EXPLAIN (ANALYZE) to only show the leader statistics, significantly undercounting the true value. Additionally, add a regression test covering EXPLAIN (ANALYZE) of a Parallel Bitmap Heap Scan, which previously was not tested at all. Author: Lukas Fittl Reviewed-by: Discussion --- src/backend/commands/explain.c | 33 +++++++++++++++++++++------ src/test/regress/expected/explain.out | 33 +++++++++++++++++++++++++++ src/test/regress/sql/explain.sql | 31 +++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index e4b70166b0e50..b01a2e1e14928 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3924,26 +3924,45 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) static void show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es) { + uint64 exact_pages; + uint64 lossy_pages; + if (!es->analyze) return; + /* Start with leader's stats */ + exact_pages = planstate->stats.exact_pages; + lossy_pages = planstate->stats.lossy_pages; + + /* Accumulate worker stats into node-level totals */ + if (planstate->sinstrument != NULL) + { + for (int n = 0; n < planstate->sinstrument->num_workers; n++) + { + BitmapHeapScanInstrumentation *si = &planstate->sinstrument->sinstrument[n]; + + exact_pages += si->exact_pages; + lossy_pages += si->lossy_pages; + } + } + if (es->format != EXPLAIN_FORMAT_TEXT) { ExplainPropertyUInteger("Exact Heap Blocks", NULL, - planstate->stats.exact_pages, es); + exact_pages, es); ExplainPropertyUInteger("Lossy Heap Blocks", NULL, - planstate->stats.lossy_pages, es); + lossy_pages, es); } else { - if (planstate->stats.exact_pages > 0 || planstate->stats.lossy_pages > 0) + if (exact_pages > 0 || lossy_pages > 0) { ExplainIndentText(es); appendStringInfoString(es->str, "Heap Blocks:"); - if (planstate->stats.exact_pages > 0) - appendStringInfo(es->str, " exact=" UINT64_FORMAT, planstate->stats.exact_pages); - if (planstate->stats.lossy_pages > 0) - appendStringInfo(es->str, " lossy=" UINT64_FORMAT, planstate->stats.lossy_pages); + if (exact_pages > 0) + appendStringInfo(es->str, " exact=" UINT64_FORMAT, exact_pages); + if (lossy_pages > 0) + appendStringInfo(es->str, " lossy=" UINT64_FORMAT, lossy_pages); appendStringInfoChar(es->str, '\n'); } } diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out index 7c1f26b182cb0..58c5a512d74de 100644 --- a/src/test/regress/expected/explain.out +++ b/src/test/regress/expected/explain.out @@ -822,3 +822,36 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove (9 rows) reset work_mem; +-- Test parallel bitmap heap scan reports per-worker heap block stats. +CREATE FUNCTION check_parallel_bitmap_heap_scan() RETURNS boolean AS $$ +DECLARE + plan_json json; + node json; +BEGIN + SET LOCAL enable_seqscan = off; + SET LOCAL enable_indexscan = off; + SET LOCAL parallel_setup_cost = 0; + SET LOCAL parallel_tuple_cost = 0; + SET LOCAL min_parallel_table_scan_size = 0; + SET LOCAL min_parallel_index_scan_size = 0; + SET LOCAL max_parallel_workers_per_gather = 2; + SET LOCAL parallel_leader_participation = off; + + EXECUTE 'EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, FORMAT JSON) + SELECT count(*) FROM tenk1 WHERE hundred > 1' INTO plan_json; + + node := plan_json->0->'Plan'; + WHILE node->'Plans' IS NOT NULL AND node->>'Node Type' != 'Bitmap Heap Scan' LOOP + node := node->'Plans'->0; + END LOOP; + + RETURN COALESCE((node->>'Exact Heap Blocks')::int, 0) > 0; +END; +$$ LANGUAGE plpgsql; +SELECT check_parallel_bitmap_heap_scan() AS parallel_bitmap_instrumentation; + parallel_bitmap_instrumentation +--------------------------------- + t +(1 row) + +DROP FUNCTION check_parallel_bitmap_heap_scan; diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql index ebdab42604beb..bac97522053f6 100644 --- a/src/test/regress/sql/explain.sql +++ b/src/test/regress/sql/explain.sql @@ -188,3 +188,34 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove -- Test tuplestore storage usage in Window aggregate (memory and disk case, final result is disk) select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over(partition by m) from (SELECT n < 3 as m, n from generate_series(1,2500) a(n))'); reset work_mem; + +-- Test parallel bitmap heap scan reports per-worker heap block stats. +CREATE FUNCTION check_parallel_bitmap_heap_scan() RETURNS boolean AS $$ +DECLARE + plan_json json; + node json; +BEGIN + SET LOCAL enable_seqscan = off; + SET LOCAL enable_indexscan = off; + SET LOCAL parallel_setup_cost = 0; + SET LOCAL parallel_tuple_cost = 0; + SET LOCAL min_parallel_table_scan_size = 0; + SET LOCAL min_parallel_index_scan_size = 0; + SET LOCAL max_parallel_workers_per_gather = 2; + SET LOCAL parallel_leader_participation = off; + + EXECUTE 'EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, FORMAT JSON) + SELECT count(*) FROM tenk1 WHERE hundred > 1' INTO plan_json; + + node := plan_json->0->'Plan'; + WHILE node->'Plans' IS NOT NULL AND node->>'Node Type' != 'Bitmap Heap Scan' LOOP + node := node->'Plans'->0; + END LOOP; + + RETURN COALESCE((node->>'Exact Heap Blocks')::int, 0) > 0; +END; +$$ LANGUAGE plpgsql; + +SELECT check_parallel_bitmap_heap_scan() AS parallel_bitmap_instrumentation; + +DROP FUNCTION check_parallel_bitmap_heap_scan; From 7f39a58e9e845709b897276e1b86384bd7f60c3a Mon Sep 17 00:00:00 2001 From: Lukas Fittl Date: Sun, 5 Apr 2026 03:48:22 -0700 Subject: [PATCH 2/2] Add regression test coverage for EXPLAIN of Parallel Index Only Scans The functions dealing with copying back parallel worker instrumentation such as ExecIndexOnlyScanRetrieveInstrumentation were not exercised at all in the regression tests, leading to a gap in coverage. Add a query that verifies we correctly copy back "Index Searches" for EXPLAIN ANALYZE of a Parallel Index Only Scan. Reported-by: Andres Freund Author: Lukas Fittl Discussion: --- src/test/regress/expected/explain.out | 34 +++++++++++++++++++++++++++ src/test/regress/sql/explain.sql | 32 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out index 58c5a512d74de..b307e810ca561 100644 --- a/src/test/regress/expected/explain.out +++ b/src/test/regress/expected/explain.out @@ -855,3 +855,37 @@ SELECT check_parallel_bitmap_heap_scan() AS parallel_bitmap_instrumentation; (1 row) DROP FUNCTION check_parallel_bitmap_heap_scan; +-- Test parallel index-only scan reports per-worker index search stats. +CREATE FUNCTION check_parallel_indexonly_scan() RETURNS boolean AS $$ +DECLARE + plan_json json; + node json; +BEGIN + SET LOCAL enable_seqscan = off; + SET LOCAL enable_bitmapscan = off; + SET LOCAL parallel_setup_cost = 0; + SET LOCAL parallel_tuple_cost = 0; + SET LOCAL min_parallel_index_scan_size = 0; + SET LOCAL min_parallel_table_scan_size = 0; + SET LOCAL max_parallel_workers_per_gather = 2; + SET LOCAL parallel_leader_participation = off; + + EXECUTE 'EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, FORMAT JSON) + SELECT count(*) FROM tenk1 WHERE thousand > 95' INTO plan_json; + + -- Drill down to the Index Only Scan node + node := plan_json->0->'Plan'; + WHILE node->'Plans' IS NOT NULL AND node->>'Node Type' != 'Index Only Scan' LOOP + node := node->'Plans'->0; + END LOOP; + + RETURN COALESCE((node->>'Index Searches')::int, 0) > 0; +END; +$$ LANGUAGE plpgsql; +SELECT check_parallel_indexonly_scan() AS parallel_indexonly_instrumentation; + parallel_indexonly_instrumentation +------------------------------------ + t +(1 row) + +DROP FUNCTION check_parallel_indexonly_scan; diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql index bac97522053f6..3a13fa6ca69a4 100644 --- a/src/test/regress/sql/explain.sql +++ b/src/test/regress/sql/explain.sql @@ -219,3 +219,35 @@ $$ LANGUAGE plpgsql; SELECT check_parallel_bitmap_heap_scan() AS parallel_bitmap_instrumentation; DROP FUNCTION check_parallel_bitmap_heap_scan; + +-- Test parallel index-only scan reports per-worker index search stats. +CREATE FUNCTION check_parallel_indexonly_scan() RETURNS boolean AS $$ +DECLARE + plan_json json; + node json; +BEGIN + SET LOCAL enable_seqscan = off; + SET LOCAL enable_bitmapscan = off; + SET LOCAL parallel_setup_cost = 0; + SET LOCAL parallel_tuple_cost = 0; + SET LOCAL min_parallel_index_scan_size = 0; + SET LOCAL min_parallel_table_scan_size = 0; + SET LOCAL max_parallel_workers_per_gather = 2; + SET LOCAL parallel_leader_participation = off; + + EXECUTE 'EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, FORMAT JSON) + SELECT count(*) FROM tenk1 WHERE thousand > 95' INTO plan_json; + + -- Drill down to the Index Only Scan node + node := plan_json->0->'Plan'; + WHILE node->'Plans' IS NOT NULL AND node->>'Node Type' != 'Index Only Scan' LOOP + node := node->'Plans'->0; + END LOOP; + + RETURN COALESCE((node->>'Index Searches')::int, 0) > 0; +END; +$$ LANGUAGE plpgsql; + +SELECT check_parallel_indexonly_scan() AS parallel_indexonly_instrumentation; + +DROP FUNCTION check_parallel_indexonly_scan;