From 576a5f36cc60b6c3c147ec9fad23c3765467b198 Mon Sep 17 00:00:00 2001
From: Zhong ShiHao <zhong950419@gmail.com>
Date: Thu, 27 Aug 2026 22:14:14 -0400
Subject: [PATCH v1 1/2] Add a planner support function for
 generate_subscripts()

Previously the planner had no way to estimate how many rows
generate_subscripts() returns, so it always used the function's
prorows value of 1000.  Its close sibling unnest() has computed a
proper estimate from its array argument since planner support
functions were introduced in v12.

Add generate_subscripts_support(), which handles SupportRequestRows
as follows:

* If any argument is a constant NULL, report zero rows: the function
  is strict.

* If both the array and the dimension number are plan-time constants,
  report the length of the requested dimension exactly.

* If only the dimension number is known and it is 1, use
  estimate_array_length(), which can consult the array column's
  statistics.  For one-dimensional arrays, by far the common case,
  the total element count it reports equals the length of dimension 1.

* Otherwise report nothing, and the planner falls back on prorows as
  before.
---
 src/backend/utils/adt/arrayfuncs.c        | 111 ++++++++++++++
 src/include/catalog/pg_proc.dat           |   9 +-
 src/test/regress/expected/planner_est.out | 167 ++++++++++++++++++++++
 src/test/regress/sql/planner_est.sql      |  95 ++++++++++++
 4 files changed, 380 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index ef66182b047..c183ce9ea86 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -6000,6 +6000,117 @@ generate_subscripts_nodir(PG_FUNCTION_ARGS)
 	return generate_subscripts(fcinfo);
 }
 
+/*
+ * Planner support function for generate_subscripts(anyarray, int [, bool])
+ *
+ * Unlike unnest(), generate_subscripts() returns one row per subscript of
+ * the *requested dimension*, not one row per array element.  Those are the
+ * same thing for a one-dimensional array, but in general we need the length
+ * of dimension "dim", which we can only determine when the array itself is
+ * available as a constant.
+ */
+Datum
+generate_subscripts_support(PG_FUNCTION_ARGS)
+{
+	Node	   *rawreq = (Node *) PG_GETARG_POINTER(0);
+	Node	   *ret = NULL;
+
+	if (IsA(rawreq, SupportRequestRows))
+	{
+		/* Try to estimate the number of rows returned */
+		SupportRequestRows *req = (SupportRequestRows *) rawreq;
+
+		if (is_funcclause(req->node))	/* be paranoid */
+		{
+			List	   *args = ((FuncExpr *) req->node)->args;
+			Node	   *arg1,
+					   *arg2,
+					   *arg3;
+
+			/* We can use estimated argument values here */
+			arg1 = estimate_expression_value(req->root, linitial(args));
+			arg2 = estimate_expression_value(req->root, lsecond(args));
+			if (list_length(args) >= 3)
+				arg3 = estimate_expression_value(req->root, lthird(args));
+			else
+				arg3 = NULL;
+
+			/*
+			 * The function is strict, so a constant NULL in any argument
+			 * position means that no rows will be returned.  Otherwise we
+			 * need to know the dimension number to say anything at all.
+			 */
+			if ((IsA(arg1, Const) &&
+				 ((Const *) arg1)->constisnull) ||
+				(IsA(arg2, Const) &&
+				 ((Const *) arg2)->constisnull) ||
+				(arg3 != NULL && IsA(arg3, Const) &&
+				 ((Const *) arg3)->constisnull))
+			{
+				req->rows = 0;
+				ret = (Node *) req;
+			}
+			else if (IsA(arg2, Const))
+			{
+				int32		reqdim = DatumGetInt32(((Const *) arg2)->constvalue);
+
+				if (reqdim <= 0)
+				{
+					/* generate_subscripts() returns no rows for such dims */
+					req->rows = 0;
+					ret = (Node *) req;
+				}
+				else if (IsA(arg1, Const))
+				{
+					ArrayType  *arr;
+
+					/*
+					 * We know the array, so we can report the length of the
+					 * requested dimension exactly.  Note that the dimension's
+					 * lower bound does not enter into it.  A dimension the
+					 * array doesn't have yields no rows, which also covers
+					 * empty arrays, whose ndim is zero.
+					 *
+					 * Unlike estimate_array_length(), we don't bother to look
+					 * through array coercions here; if one is in the way we
+					 * simply fall through to the cases below.
+					 */
+					arr = DatumGetArrayTypeP(((Const *) arg1)->constvalue);
+
+					if (reqdim > ARR_NDIM(arr))
+						req->rows = 0;	/* no such dimension */
+					else
+						req->rows = ARR_DIMS(arr)[reqdim - 1];
+					ret = (Node *) req;
+				}
+				else if (reqdim == 1)
+				{
+					/*
+					 * All we have is estimate_array_length(), which counts
+					 * every element rather than the length of one dimension.
+					 * Those agree for 1-D arrays, and since dimension 1 was
+					 * requested it's fair to suppose that's what we have.  If
+					 * not we'll overestimate, but no per-dimension statistics
+					 * exist that could do better.
+					 */
+					req->rows = estimate_array_length(req->root, arg1);
+					ret = (Node *) req;
+				}
+
+				/*
+				 * Otherwise a dimension above the first was requested for an
+				 * array that isn't a constant.  We know nothing about the
+				 * array's dimensionality, let alone the length of the
+				 * requested dimension, so leave ret as NULL to make the
+				 * caller fall back on prorows.
+				 */
+			}
+		}
+	}
+
+	PG_RETURN_POINTER(ret);
+}
+
 /*
  * array_fill_with_lower_bounds
  *		Create and fill array with defined lower bounds.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6979c7d1161..e35379afd33 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1711,13 +1711,18 @@
   proargtypes => 'anycompatiblearray anycompatible',
   prosrc => 'array_positions' },
 { oid => '1191', descr => 'array subscripts generator',
-  proname => 'generate_subscripts', prorows => '1000', proretset => 't',
+  proname => 'generate_subscripts', prorows => '1000',
+  prosupport => 'generate_subscripts_support', proretset => 't',
   prorettype => 'int4', proargtypes => 'anyarray int4 bool',
   prosrc => 'generate_subscripts' },
 { oid => '1192', descr => 'array subscripts generator',
-  proname => 'generate_subscripts', prorows => '1000', proretset => 't',
+  proname => 'generate_subscripts', prorows => '1000',
+  prosupport => 'generate_subscripts_support', proretset => 't',
   prorettype => 'int4', proargtypes => 'anyarray int4',
   prosrc => 'generate_subscripts_nodir' },
+{ oid => '9419', descr => 'planner support for generate_subscripts',
+  proname => 'generate_subscripts_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'generate_subscripts_support' },
 { oid => '1193', descr => 'array constructor with value',
   proname => 'array_fill', proisstrict => 'f', prorettype => 'anyarray',
   proargtypes => 'anyelement _int4', prosrc => 'array_fill' },
diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out
index 236cb274a78..91305e360f8 100644
--- a/src/test/regress/expected/planner_est.out
+++ b/src/test/regress/expected/planner_est.out
@@ -183,6 +183,173 @@ false, true, false, true);
  Function Scan on generate_series g  (cost=N..N rows=1000 width=N)
 (1 row)
 
+--
+-- Test the SupportRequestRows support function for generate_subscripts()
+--
+-- A constant array gives an exact estimate for the requested dimension
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30,40,50}'::int[], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1)
+(1 row)
+
+-- As above but for the 3-argument form; "reverse" cannot change the row count
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30,40,50}'::int[], 1, true) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1)
+(1 row)
+
+-- Ensure a non-zero lower bound does not affect the estimate
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('[5:9]={10,20,30,40,50}'::int[], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1)
+(1 row)
+
+-- Ensure each dimension of a multi-dimensional array is estimated separately
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=2 width=N) (actual rows=2.00 loops=1)
+(1 row)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 2) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=3 width=N) (actual rows=3.00 loops=1)
+(1 row)
+
+-- Ensure cases which return no rows estimate 1 row after clamping.  Try an
+-- out-of-range dimension, a dimension below the first, and an empty array.
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 3) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], 0) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{}'::int[], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+-- Ensure a constant NULL in any argument position estimates no rows, since
+-- generate_subscripts() is strict
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(NULL::int[], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], NULL) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], 1, NULL) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+(1 row)
+
+-- An ArrayExpr is estimated by way of estimate_array_length()
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], 1) g(s);$$,
+true, true, false, true);
+                                      explain_mask_costs                                       
+-----------------------------------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (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 we get the default row estimate when the dimension number isn't a
+-- constant, and when a dimension above the first is requested for an array
+-- which isn't a constant.  Neither case can be estimated.
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], (SELECT 1)) g(s);$$,
+false, true, false, true);
+                          explain_mask_costs                           
+-----------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1000 width=N)
+   InitPlan expr_1
+     ->  Result  (cost=N..N rows=1 width=N)
+   InitPlan expr_2
+     ->  Result  (cost=N..N rows=1 width=N)
+(5 rows)
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], 2) g(s);$$,
+false, true, false, true);
+                          explain_mask_costs                           
+-----------------------------------------------------------------------
+ Function Scan on generate_subscripts g  (cost=N..N rows=1000 width=N)
+   InitPlan expr_1
+     ->  Result  (cost=N..N rows=1 width=N)
+(3 rows)
+
+-- Ensure a Var array is estimated from the DECHIST statistics.  Every array
+-- below holds exactly 7 distinct elements, so the average distinct element
+-- count does not depend on which rows ANALYZE happens to sample.
+CREATE TEMP TABLE subscript_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 subscript_table_1;
+-- Memoize is disabled here only to keep the plan shape stable; the node's
+-- capacity and hit percentage are not masked by explain_mask_costs().
+SET enable_memoize = off;
+SELECT explain_mask_costs($$
+SELECT * FROM subscript_table_1 t, LATERAL generate_subscripts(t.a, 1) g(s);$$,
+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 subscript_table_1 t  (cost=N..N rows=100 width=N) (actual rows=100.00 loops=1)
+   ->  Function Scan on generate_subscripts g  (cost=N..N rows=7 width=N) (actual rows=7.00 loops=100)
+(3 rows)
+
+-- As above, but a dimension above the first still falls back on prorows
+SELECT explain_mask_costs($$
+SELECT * FROM subscript_table_1 t, LATERAL generate_subscripts(t.a, 2) g(s);$$,
+false, true, false, true);
+                             explain_mask_costs                              
+-----------------------------------------------------------------------------
+ Nested Loop  (cost=N..N rows=100000 width=N)
+   ->  Seq Scan on subscript_table_1 t  (cost=N..N rows=100 width=N)
+   ->  Function Scan on generate_subscripts g  (cost=N..N rows=1000 width=N)
+(3 rows)
+
+RESET enable_memoize;
 --
 -- Test ScalarArrayOpExpr row estimates for <> ALL for arrays with NULLs.  We
 -- expect the planner to estimate 1 row will match in both of the following
diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql
index 2b696a4e4e5..b9d2dbc4a4b 100644
--- a/src/test/regress/sql/planner_est.sql
+++ b/src/test/regress/sql/planner_est.sql
@@ -131,6 +131,101 @@ SELECT explain_mask_costs($$
 SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$,
 false, true, false, true);
 
+--
+-- Test the SupportRequestRows support function for generate_subscripts()
+--
+
+-- A constant array gives an exact estimate for the requested dimension
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30,40,50}'::int[], 1) g(s);$$,
+true, true, false, true);
+
+-- As above but for the 3-argument form; "reverse" cannot change the row count
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30,40,50}'::int[], 1, true) g(s);$$,
+true, true, false, true);
+
+-- Ensure a non-zero lower bound does not affect the estimate
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('[5:9]={10,20,30,40,50}'::int[], 1) g(s);$$,
+true, true, false, true);
+
+-- Ensure each dimension of a multi-dimensional array is estimated separately
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 1) g(s);$$,
+true, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 2) g(s);$$,
+true, true, false, true);
+
+-- Ensure cases which return no rows estimate 1 row after clamping.  Try an
+-- out-of-range dimension, a dimension below the first, and an empty array.
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{{1,2,3},{4,5,6}}'::int[], 3) g(s);$$,
+true, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], 0) g(s);$$,
+true, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{}'::int[], 1) g(s);$$,
+true, true, false, true);
+
+-- Ensure a constant NULL in any argument position estimates no rows, since
+-- generate_subscripts() is strict
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(NULL::int[], 1) g(s);$$,
+true, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], NULL) g(s);$$,
+true, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts('{10,20,30}'::int[], 1, NULL) g(s);$$,
+true, true, false, true);
+
+-- An ArrayExpr is estimated by way of estimate_array_length()
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], 1) g(s);$$,
+true, true, false, true);
+
+-- Ensure we get the default row estimate when the dimension number isn't a
+-- constant, and when a dimension above the first is requested for an array
+-- which isn't a constant.  Neither case can be estimated.
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], (SELECT 1)) g(s);$$,
+false, true, false, true);
+
+SELECT explain_mask_costs($$
+SELECT * FROM generate_subscripts(ARRAY[1, 2, (SELECT 3)], 2) g(s);$$,
+false, true, false, true);
+
+-- Ensure a Var array is estimated from the DECHIST statistics.  Every array
+-- below holds exactly 7 distinct elements, so the average distinct element
+-- count does not depend on which rows ANALYZE happens to sample.
+CREATE TEMP TABLE subscript_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 subscript_table_1;
+
+-- Memoize is disabled here only to keep the plan shape stable; the node's
+-- capacity and hit percentage are not masked by explain_mask_costs().
+SET enable_memoize = off;
+
+SELECT explain_mask_costs($$
+SELECT * FROM subscript_table_1 t, LATERAL generate_subscripts(t.a, 1) g(s);$$,
+true, true, false, true);
+
+-- As above, but a dimension above the first still falls back on prorows
+SELECT explain_mask_costs($$
+SELECT * FROM subscript_table_1 t, LATERAL generate_subscripts(t.a, 2) g(s);$$,
+false, true, false, true);
+
+RESET enable_memoize;
+
 --
 -- Test ScalarArrayOpExpr row estimates for <> ALL for arrays with NULLs.  We
 -- expect the planner to estimate 1 row will match in both of the following
-- 
2.37.1 (Apple Git-137.1)

