From b2356f1c856036caadfc443a902c62f7d9b34ca4 Mon Sep 17 00:00:00 2001 From: Mehmet YILMAZ Date: Mon, 10 Nov 2025 10:43:11 +0300 Subject: [PATCH] PG18: Make EXPLAIN ANALYZE output stable by routing through explain_filter and hiding footers (#8325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ; ``` with: ```sql \pset footer off SELECT public.explain_filter('EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) '); \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: ` line before normalizing numeric fields, preventing the “N” version of the same line from sneaking in. * **Keep suite-wide normalizer intact** --- src/test/regress/bin/normalize.sed | 3 -- .../expected/local_shard_execution.out | 46 ++++++++++--------- .../local_shard_execution_replicated.out | 44 +++++++++--------- .../regress/expected/multi_test_helpers.out | 5 ++ .../regress/sql/local_shard_execution.sql | 9 +++- .../sql/local_shard_execution_replicated.sql | 8 +++- src/test/regress/sql/multi_test_helpers.sql | 5 ++ 7 files changed, 70 insertions(+), 50 deletions(-) diff --git a/src/test/regress/bin/normalize.sed b/src/test/regress/bin/normalize.sed index e3df83fa4..f209a2dc8 100644 --- a/src/test/regress/bin/normalize.sed +++ b/src/test/regress/bin/normalize.sed @@ -376,9 +376,6 @@ s/\/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 diff --git a/src/test/regress/expected/local_shard_execution.out b/src/test/regress/expected/local_shard_execution.out index 3348db63a..09a88c6c0 100644 --- a/src/test/regress/expected/local_shard_execution.out +++ b/src/test/regress/expected/local_shard_execution.out @@ -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 diff --git a/src/test/regress/expected/local_shard_execution_replicated.out b/src/test/regress/expected/local_shard_execution_replicated.out index 835df717d..ca85fdb4e 100644 --- a/src/test/regress/expected/local_shard_execution_replicated.out +++ b/src/test/regress/expected/local_shard_execution_replicated.out @@ -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 diff --git a/src/test/regress/expected/multi_test_helpers.out b/src/test/regress/expected/multi_test_helpers.out index 957a3d11b..00c4a61d7 100644 --- a/src/test/regress/expected/multi_test_helpers.out +++ b/src/test/regress/expected/multi_test_helpers.out @@ -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 diff --git a/src/test/regress/sql/local_shard_execution.sql b/src/test/regress/sql/local_shard_execution.sql index 0ba2f9e38..688896f56 100644 --- a/src/test/regress/sql/local_shard_execution.sql +++ b/src/test/regress/sql/local_shard_execution.sql @@ -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; diff --git a/src/test/regress/sql/local_shard_execution_replicated.sql b/src/test/regress/sql/local_shard_execution_replicated.sql index 0740d58da..45ed426ce 100644 --- a/src/test/regress/sql/local_shard_execution_replicated.sql +++ b/src/test/regress/sql/local_shard_execution_replicated.sql @@ -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; diff --git a/src/test/regress/sql/multi_test_helpers.sql b/src/test/regress/sql/multi_test_helpers.sql index 10242692c..e605e7e90 100644 --- a/src/test/regress/sql/multi_test_helpers.sql +++ b/src/test/regress/sql/multi_test_helpers.sql @@ -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