From f53ab7c1a6bd594b1a559048eadc2d7d5049ff63 Mon Sep 17 00:00:00 2001 From: Zhong ShiHao Date: Sun, 30 Aug 2026 22:13:39 -0400 Subject: [PATCH] Add row estimate tests for the older SRF support functions The support functions for unnest() and the integer variants of generate_series() predate planner_est.sql and have no regression tests for their row estimates. Add some, following the existing sections. --- src/test/regress/expected/planner_est.out | 162 ++++++++++++++++++++++ src/test/regress/sql/planner_est.sql | 102 ++++++++++++++ 2 files changed, 264 insertions(+) diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out index 5e892ef6728..3a6e7bd90f7 100644 --- a/src/test/regress/expected/planner_est.out +++ b/src/test/regress/expected/planner_est.out @@ -118,6 +118,85 @@ false, true, false, true); -- the support function. SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); ERROR: step size cannot equal zero +-- +-- Test the SupportRequestRows support functions for the integer variants of +-- generate_series() +-- +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) +(1 row) + +-- As above but with non-default step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25, 2) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13.00 loops=1) +(1 row) + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25, 1, -1) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) +(1 row) + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25, 1) g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +-- Ensure a constant NULL argument estimates no rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, NULL::int4) g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +-- Ensure we get the default row estimate when step size is zero. We expect +-- generate_series() to throw the error rather than the support function. +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25, 0) g(s);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1000 width=N) +(1 row) + +SELECT * FROM generate_series(1, 25, 0) g(s); +ERROR: step size cannot equal zero +-- As above for the bigint variant +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1::int8, 25::int8, 3::int8) g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=9 width=N) (actual rows=9.00 loops=1) +(1 row) + +-- Ensure a very large range does not overflow the row estimate +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1::int8, 10000000000::int8) g(s);$$, +false, true, false, true); + explain_mask_costs +-------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=10000000000 width=N) +(1 row) + -- -- Test the SupportRequestRows support function for generate_series_numeric() -- @@ -377,6 +456,89 @@ false, true, false, true); -> Result (cost=N..N rows=1 width=N) (4 rows) +-- +-- Test the SupportRequestRows support function for unnest(anyarray) +-- +-- Ensure the row estimate matches the actual rows for a constant array +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{1,2,3,4,5}'::int[]) u(e);$$, +true, true, false, true); + explain_mask_costs +---------------------------------------------------------------------------------- + Function Scan on unnest u (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1) +(1 row) + +-- A multi-dimensional array is estimated as its total number of elements +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{{1,2,3},{4,5,6}}'::int[]) u(e);$$, +true, true, false, true); + explain_mask_costs +---------------------------------------------------------------------------------- + Function Scan on unnest u (cost=N..N rows=6 width=N) (actual rows=6.00 loops=1) +(1 row) + +-- Ensure empty and NULL arrays estimate 1 row after clamping +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{}'::int[]) u(e);$$, +true, true, false, true); + explain_mask_costs +---------------------------------------------------------------------------------- + Function Scan on unnest u (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +SELECT explain_mask_costs($$ +SELECT * FROM unnest(NULL::int[]) u(e);$$, +true, true, false, true); + explain_mask_costs +---------------------------------------------------------------------------------- + Function Scan on unnest u (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +-- An ArrayExpr is estimated from its element count +SELECT explain_mask_costs($$ +SELECT * FROM unnest(ARRAY[1, 2, (SELECT 3)]) u(e);$$, +true, true, false, true); + explain_mask_costs +---------------------------------------------------------------------------------- + Function Scan on unnest u (cost=N..N rows=3 width=N) (actual rows=3.00 loops=1) + InitPlan expr_1 + -> Result (cost=N..N rows=1 width=N) (actual rows=1.00 loops=1) +(3 rows) + +-- Ensure a Var array is estimated from the DECHIST statistics. The arrays +-- hold distinct elements only, keeping the average independent of sampling. +CREATE TEMP TABLE unnest_table_1 AS + SELECT array_agg(g) AS a FROM generate_series(1, 100) i, + LATERAL generate_series(1, 7) g GROUP BY i; +ANALYZE unnest_table_1; +-- Disable Memoize to keep the plan shape stable +SET enable_memoize = off; +SELECT explain_mask_costs($$ +SELECT * FROM unnest_table_1 t, LATERAL unnest(t.a) u(e);$$, +true, true, false, true); + explain_mask_costs +----------------------------------------------------------------------------------------------- + Nested Loop (cost=N..N rows=700 width=N) (actual rows=700.00 loops=1) + -> Seq Scan on unnest_table_1 t (cost=N..N rows=100 width=N) (actual rows=100.00 loops=1) + -> Function Scan on unnest u (cost=N..N rows=7 width=N) (actual rows=7.00 loops=100) +(3 rows) + +-- Without column statistics the default estimate of 10 applies. Analyze +-- only "id" so that the array column has none but the row count is stable. +CREATE TEMP TABLE unnest_table_2 (id int, a int[]); +INSERT INTO unnest_table_2 SELECT i, ARRAY[1, 2, 3] FROM generate_series(1, 5) i; +ANALYZE unnest_table_2 (id); +SELECT explain_mask_costs($$ +SELECT * FROM unnest_table_2 t, LATERAL unnest(t.a) u(e);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Nested Loop (cost=N..N rows=50 width=N) (actual rows=15.00 loops=1) + -> Seq Scan on unnest_table_2 t (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1) + -> Function Scan on unnest u (cost=N..N rows=10 width=N) (actual rows=3.00 loops=5) +(3 rows) + +RESET enable_memoize; -- Verify that scalarineqsel() works on "char" columns CREATE TEMP TABLE char_table_1 AS SELECT i::"char" AS c FROM generate_series(64,96) i; diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql index b9d2dbc4a4b..4ae35ce7e62 100644 --- a/src/test/regress/sql/planner_est.sql +++ b/src/test/regress/sql/planner_est.sql @@ -93,6 +93,54 @@ false, true, false, true); -- the support function. SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); +-- +-- Test the SupportRequestRows support functions for the integer variants of +-- generate_series() +-- + +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25) g(s);$$, +true, true, false, true); + +-- As above but with non-default step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25, 2) g(s);$$, +true, true, false, true); + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25, 1, -1) g(s);$$, +true, true, false, true); + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25, 1) g(s);$$, +true, true, false, true); + +-- Ensure a constant NULL argument estimates no rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, NULL::int4) g(s);$$, +true, true, false, true); + +-- Ensure we get the default row estimate when step size is zero. We expect +-- generate_series() to throw the error rather than the support function. +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1, 25, 0) g(s);$$, +false, true, false, true); + +SELECT * FROM generate_series(1, 25, 0) g(s); + +-- As above for the bigint variant +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1::int8, 25::int8, 3::int8) g(s);$$, +true, true, false, true); + +-- Ensure a very large range does not overflow the row estimate +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1::int8, 10000000000::int8) g(s);$$, +false, true, false, true); + -- -- Test the SupportRequestRows support function for generate_series_numeric() -- @@ -242,6 +290,60 @@ SELECT explain_mask_costs($$ SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$, false, true, false, true); +-- +-- Test the SupportRequestRows support function for unnest(anyarray) +-- + +-- Ensure the row estimate matches the actual rows for a constant array +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{1,2,3,4,5}'::int[]) u(e);$$, +true, true, false, true); + +-- A multi-dimensional array is estimated as its total number of elements +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{{1,2,3},{4,5,6}}'::int[]) u(e);$$, +true, true, false, true); + +-- Ensure empty and NULL arrays estimate 1 row after clamping +SELECT explain_mask_costs($$ +SELECT * FROM unnest('{}'::int[]) u(e);$$, +true, true, false, true); + +SELECT explain_mask_costs($$ +SELECT * FROM unnest(NULL::int[]) u(e);$$, +true, true, false, true); + +-- An ArrayExpr is estimated from its element count +SELECT explain_mask_costs($$ +SELECT * FROM unnest(ARRAY[1, 2, (SELECT 3)]) u(e);$$, +true, true, false, true); + +-- Ensure a Var array is estimated from the DECHIST statistics. The arrays +-- hold distinct elements only, keeping the average independent of sampling. +CREATE TEMP TABLE unnest_table_1 AS + SELECT array_agg(g) AS a FROM generate_series(1, 100) i, + LATERAL generate_series(1, 7) g GROUP BY i; +ANALYZE unnest_table_1; + +-- Disable Memoize to keep the plan shape stable +SET enable_memoize = off; + +SELECT explain_mask_costs($$ +SELECT * FROM unnest_table_1 t, LATERAL unnest(t.a) u(e);$$, +true, true, false, true); + +-- Without column statistics the default estimate of 10 applies. Analyze +-- only "id" so that the array column has none but the row count is stable. +CREATE TEMP TABLE unnest_table_2 (id int, a int[]); +INSERT INTO unnest_table_2 SELECT i, ARRAY[1, 2, 3] FROM generate_series(1, 5) i; +ANALYZE unnest_table_2 (id); + +SELECT explain_mask_costs($$ +SELECT * FROM unnest_table_2 t, LATERAL unnest(t.a) u(e);$$, +true, true, false, true); + +RESET enable_memoize; + -- Verify that scalarineqsel() works on "char" columns CREATE TEMP TABLE char_table_1 AS SELECT i::"char" AS c FROM generate_series(64,96) i; -- 2.37.1 (Apple Git-137.1)