PG18: Make EXPLAIN ANALYZE output stable by routing through explain_filter and hiding footers (#8325)

PostgreSQL 18 adds a new line to text EXPLAIN with ANALYZE (`Index
Searches: N`). That extra line both creates noise and bumps psql’s `(N
rows)` footer. This PR keeps ANALYZE (so statements still execute) while
removing the version-specific churn in our regress outputs.

### What changed

* **Use `explain_filter(...)` instead of raw text EXPLAIN**

* In `local_shard_execution.sql` and
`local_shard_execution_replicated.sql`, replace direct:

    ```sql
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
<stmt>;
    ```

    with:

    ```sql
    \pset footer off
SELECT public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF,
TIMING OFF, BUFFERS OFF) <stmt>');
    \pset footer on
    ```
* Expected files updated accordingly to show the `explain_filter` output
block instead of raw EXPLAIN text.
* **Extend `explain_filter` to drop the PG18 line**

* Filter now removes any `Index Searches: <number>` line before
normalizing numeric fields, preventing the “N” version of the same line
from sneaking in.
* **Keep suite-wide normalizer intact**
pull/8326/head
Mehmet YILMAZ 2025-11-10 10:43:11 +03:00 committed by GitHub
parent daa69bec8f
commit b2356f1c85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 50 deletions

View File

@ -376,9 +376,6 @@ s/\<is referenced from table\>/is still referenced from table/g
# ignore any "find_in_path:" lines in test output
/DEBUG: find_in_path: trying .*/d
# PG18: EXPLAIN ANALYZE prints "Index Searches: N" for index scans — remove it
/^\s*Index Searches:\s*\d+\s*$/d
# EXPLAIN (PG18+): hide Materialize storage instrumentation
# this rule can be removed when PG18 is the minimum supported version
/^[ \t]*Storage:[ \t].*$/d

View File

@ -312,21 +312,22 @@ EXPLAIN (COSTS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
Filter: (age = 20)
(8 rows)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
QUERY PLAN
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20');
explain_filter
---------------------------------------------------------------------
Custom Scan (Citus Adaptive) (actual rows=1 loops=1)
Task Count: 1
Tuple data received from nodes: 14 bytes
Custom Scan (Citus Adaptive) (actual rows=N loops=N)
Task Count: N
Tuple data received from nodes: N bytes
Tasks Shown: All
-> Task
Tuple data received from node: 14 bytes
Node: host=localhost port=xxxxx dbname=regression
-> Index Scan using distributed_table_pkey_1470001 on distributed_table_1470001 distributed_table (actual rows=1 loops=1)
Index Cond: (key = 1)
Filter: (age = 20)
(10 rows)
Tuple data received from node: N bytes
Node: host=localhost port=N dbname=regression
-> Index Scan using distributed_table_pkey_1470001 on distributed_table_1470001 distributed_table (actual rows=N loops=N)
Index Cond: (key = N)
Filter: (age = N)
\pset footer on
EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
WITH r AS ( SELECT GREATEST(random(), 2) z,* FROM distributed_table)
SELECT 1 FROM r WHERE z < 3;
@ -368,21 +369,22 @@ EXPLAIN (COSTS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
Filter: (age = 20)
(9 rows)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
QUERY PLAN
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20');
explain_filter
---------------------------------------------------------------------
Custom Scan (Citus Adaptive) (actual rows=0 loops=1)
Task Count: 1
Custom Scan (Citus Adaptive) (actual rows=N loops=N)
Task Count: N
Tasks Shown: All
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Delete on distributed_table_1470001 distributed_table (actual rows=0 loops=1)
-> Index Scan using distributed_table_pkey_1470001 on distributed_table_1470001 distributed_table (actual rows=1 loops=1)
Index Cond: (key = 1)
Filter: (age = 20)
Trigger for constraint second_distributed_table_key_fkey_1470005: calls=1
(10 rows)
Node: host=localhost port=N dbname=regression
-> Delete on distributed_table_1470001 distributed_table (actual rows=N loops=N)
-> Index Scan using distributed_table_pkey_1470001 on distributed_table_1470001 distributed_table (actual rows=N loops=N)
Index Cond: (key = N)
Filter: (age = N)
Trigger for constraint second_distributed_table_key_fkey_1470005: calls=N
\pset footer on
-- show that EXPLAIN ANALYZE deleted the row and cascades deletes
SELECT * FROM distributed_table WHERE key = 1 AND age = 20 ORDER BY 1,2,3;
NOTICE: executing the command locally: SELECT key, value, age FROM local_shard_execution.distributed_table_1470001 distributed_table WHERE ((key OPERATOR(pg_catalog.=) 1) AND (age OPERATOR(pg_catalog.=) 20)) ORDER BY key, value, age

View File

@ -250,21 +250,22 @@ EXPLAIN (COSTS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
Filter: (age = 20)
(8 rows)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
QUERY PLAN
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20');
explain_filter
---------------------------------------------------------------------
Custom Scan (Citus Adaptive) (actual rows=1 loops=1)
Task Count: 1
Tuple data received from nodes: 14 bytes
Custom Scan (Citus Adaptive) (actual rows=N loops=N)
Task Count: N
Tuple data received from nodes: N bytes
Tasks Shown: All
-> Task
Tuple data received from node: 14 bytes
Node: host=localhost port=xxxxx dbname=regression
-> Index Scan using distributed_table_pkey_1500001 on distributed_table_1500001 distributed_table (actual rows=1 loops=1)
Index Cond: (key = 1)
Filter: (age = 20)
(10 rows)
Tuple data received from node: N bytes
Node: host=localhost port=N dbname=regression
-> Index Scan using distributed_table_pkey_1500001 on distributed_table_1500001 distributed_table (actual rows=N loops=N)
Index Cond: (key = N)
Filter: (age = N)
\pset footer on
EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
WITH r AS ( SELECT GREATEST(random(), 2) z,* FROM distributed_table)
SELECT 1 FROM r WHERE z < 3;
@ -306,20 +307,21 @@ EXPLAIN (COSTS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
Filter: (age = 20)
(9 rows)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
QUERY PLAN
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20');
explain_filter
---------------------------------------------------------------------
Custom Scan (Citus Adaptive) (actual rows=0 loops=1)
Task Count: 1
Custom Scan (Citus Adaptive) (actual rows=N loops=N)
Task Count: N
Tasks Shown: All
-> Task
Node: host=localhost port=xxxxx dbname=regression
-> Delete on distributed_table_1500001 distributed_table (actual rows=0 loops=1)
-> Index Scan using distributed_table_pkey_1500001 on distributed_table_1500001 distributed_table (actual rows=1 loops=1)
Index Cond: (key = 1)
Filter: (age = 20)
(9 rows)
Node: host=localhost port=N dbname=regression
-> Delete on distributed_table_1500001 distributed_table (actual rows=N loops=N)
-> Index Scan using distributed_table_pkey_1500001 on distributed_table_1500001 distributed_table (actual rows=N loops=N)
Index Cond: (key = N)
Filter: (age = N)
\pset footer on
-- show that EXPLAIN ANALYZE deleted the row
SELECT * FROM distributed_table WHERE key = 1 AND age = 20 ORDER BY 1,2,3;
NOTICE: executing the command locally: SELECT key, value, age FROM local_shard_execution_replicated.distributed_table_1500001 distributed_table WHERE ((key OPERATOR(pg_catalog.=) 1) AND (age OPERATOR(pg_catalog.=) 20)) ORDER BY key, value, age

View File

@ -732,6 +732,11 @@ declare
begin
for ln in execute $1
loop
-- PG18 extra line "Index Searches: N" — remove entirely
IF ln ~ '^[[:space:]]*Index[[:space:]]+Searches:[[:space:]]*[0-9]+[[:space:]]*$' THEN
CONTINUE;
END IF;
-- Replace any numeric word with just 'N'
ln := regexp_replace(ln, '-?\m\d+\M', 'N', 'g');
-- In sort output, the above won't match units-suffixed numbers

View File

@ -218,7 +218,9 @@ SET citus.enable_binary_protocol = TRUE;
-- though going through distributed execution
EXPLAIN (COSTS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20');
\pset footer on
EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
WITH r AS ( SELECT GREATEST(random(), 2) z,* FROM distributed_table)
@ -226,7 +228,10 @@ SELECT 1 FROM r WHERE z < 3;
EXPLAIN (COSTS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20');
\pset footer on
-- show that EXPLAIN ANALYZE deleted the row and cascades deletes
SELECT * FROM distributed_table WHERE key = 1 AND age = 20 ORDER BY 1,2,3;
SELECT * FROM second_distributed_table WHERE key = 1 ORDER BY 1,2;

View File

@ -183,7 +183,9 @@ SET citus.enable_binary_protocol = TRUE;
-- though going through distributed execution
EXPLAIN (COSTS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20;
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT * FROM distributed_table WHERE key = 1 AND age = 20');
\pset footer on
EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
WITH r AS ( SELECT GREATEST(random(), 2) z,* FROM distributed_table)
@ -191,7 +193,9 @@ SELECT 1 FROM r WHERE z < 3;
EXPLAIN (COSTS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20;
\pset footer off
select public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) DELETE FROM distributed_table WHERE key = 1 AND age = 20');
\pset footer on
-- show that EXPLAIN ANALYZE deleted the row
SELECT * FROM distributed_table WHERE key = 1 AND age = 20 ORDER BY 1,2,3;
SELECT * FROM second_distributed_table WHERE key = 1 ORDER BY 1,2;

View File

@ -763,6 +763,11 @@ declare
begin
for ln in execute $1
loop
-- PG18 extra line "Index Searches: N" — remove entirely
IF ln ~ '^[[:space:]]*Index[[:space:]]+Searches:[[:space:]]*[0-9]+[[:space:]]*$' THEN
CONTINUE;
END IF;
-- Replace any numeric word with just 'N'
ln := regexp_replace(ln, '-?\m\d+\M', 'N', 'g');
-- In sort output, the above won't match units-suffixed numbers