[PATCH] Planner support function for generate_subscripts()

Started by shihao zhong17 days ago6 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253611
psql -h localhost -U postgres

Built from patchset v6 (message #6), September 16, 2026 at 02:13 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253611_6 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253611_6 && git checkout t253611_6

Patchset v6 (message #6) is on t253611_6

Jump to latest
#1shihao zhong
zhong950419@gmail.com

Hi hackers,
generate_subscripts() has no planner support function, so its row
estimate is always the prorows value of 1000, no matter what the
arguments are. unnest() has been estimating its row count from the
array argument since v12. The attached 0001 does the same for
generate_subscripts().

One difference from unnest() is that generate_subscripts() returns
one row per subscript of the requested dimension, not one row per
element. An exact answer is therefore only possible when the array
is a plan-time constant.

The support function handles three cases:

1. If any argument is a constant NULL, it reports zero rows, since the
function is strict.
2. If both the array and the dimension number are
constants, it reports the exact length of that dimension.
3. If only the dimension number is known and it is 1, it uses
estimate_array_length().

That works because for one-dimensional arrays, the element count
equals the length of dimension 1. In all other cases it declines and
prorows applies as before.

This can change plans for the better. Joining five subscripts
against an indexed table:
Hash Join (cost=637.00..649.63 rows=1000 width=45)
Hash Cond: (s.s = items.id)
-> Function Scan on generate_subscripts s (rows=1000) (actual rows=5)
-> Hash
-> Seq Scan on items (rows=20000)
becomes
Nested Loop (cost=0.29..41.58 rows=5 width=45)
-> Function Scan on generate_subscripts s (rows=5) (actual rows=5)
-> Index Scan using items_pkey on items

A note on the statistics path. estimate_array_length() uses the
DECHIST average, which counts distinct elements, so arrays with many
duplicate or NULL elements get underestimated. unnest() behaves the
same way. Fixing that centrally looks like separate work. The new
regression tests use arrays of distinct elements to keep the expected
output deterministic.

0002 lowers prorows from 1000 to 100. After 0001, prorows is only
reached when the dimension number is unknown at plan time, or when a
higher dimension of a non-constant array is requested. 100 matches
what unnest() uses. I kept it as a separate patch so it can be taken
or dropped on its own.

CatVersion bump is required.

Thanks,
Shihao

Attachments:

t253611_1
v1-0002-Lower-generate_subscripts-s-prorows-estimate-to-m.patchapplication/octet-stream; name=v1-0002-Lower-generate_subscripts-s-prorows-estimate-to-m.patchDownload+12-10
v1-0001-Add-a-planner-support-function-for-generate_subsc.patchapplication/octet-stream; name=v1-0001-Add-a-planner-support-function-for-generate_subsc.patchDownload+380-3
#2Priyanka S
developerette@gmail.com
In reply to: shihao zhong (#1)
Re: [PATCH] Planner support function for generate_subscripts()

Hi,

I've applied the patch and done some debugging to understand the behaviour.
The approach looks good and all the cases work correctly for me. Please
find below some minor review comments. This is my first code review for
postgres, so please excuse me if I have given any wrong inputs.

1) It would be good to add a line to the description saying that
generate_subscripts() returns 0 for dimensions which do not exist in the
array. This seems like an important case which should be mentioned
specifically.

2) Missing NULL-initialisation of these vars, plus unnecessary newlines.

File: src/backend/utils/adt/arrayfuncs.c
+                       Node       *arg1,
+                                          *arg2,
+                                          *arg3;

3) Would be good to change the comment to something more descriptive like
'Check that this is a FuncExpr'. Or delete it, whichever you prefer.

File: src/backend/utils/adt/arrayfuncs.c

+ if (is_funcclause(req->node)) /* be paranoid */

4) The declaration and assignment should be combined into a single line.

File: src/backend/utils/adt/arrayfuncs.c
+                                       ArrayType  *arr;
+                                       ...
+                                       arr = DatumGetArrayTypeP(((Const *)
arg1)->constvalue);

to

+ ArrayType *arr =
DatumGetArrayTypeP(((Const *) arg1)->constvalue);

5) Could you please add a test case for a dimension greater than 2? I've
tested this patch using a 4-dimensional array and it works. Here are my
query outputs, if you'd like to use them.

pgsql=# explain SELECT * FROM
generate_subscripts('{{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}},
{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}}}'::int[],
1);
QUERY PLAN
------------------------------------------------------------------------
Function Scan on generate_subscripts (cost=0.00..0.02 rows=2 width=4)
(1 row)

pgsql=# explain SELECT * FROM
generate_subscripts('{{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}},
{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}}}'::int[],
2);
QUERY PLAN
------------------------------------------------------------------------
Function Scan on generate_subscripts (cost=0.00..0.03 rows=3 width=4)
(1 row)

pgsql=# explain SELECT * FROM
generate_subscripts('{{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}},
{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}}}'::int[],
3);
QUERY PLAN
------------------------------------------------------------------------
Function Scan on generate_subscripts (cost=0.00..0.04 rows=4 width=4)
(1 row)

pgsql=# explain SELECT * FROM
generate_subscripts('{{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}},
{{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}},{{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}}}'::int[],
4);
QUERY PLAN
------------------------------------------------------------------------
Function Scan on generate_subscripts (cost=0.00..0.05 rows=5 width=4)
(1 row)

6) I debugged estimate_array_length() to understand the non-const array
case. For multi-dimensional arrays, it seems to use a default value of 10
to match a variable 'scalararraysel'. I was wondering if
generate_subscripts() and unnest() prorows should match this default value
of 10 instead of being set to 100?

Thanks & regards,
Priyanka

On Mon, Aug 31, 2026 at 7:34 AM shihao zhong <zhong950419@gmail.com> wrote:

Show quoted text

Hi hackers,
generate_subscripts() has no planner support function, so its row
estimate is always the prorows value of 1000, no matter what the
arguments are. unnest() has been estimating its row count from the
array argument since v12. The attached 0001 does the same for
generate_subscripts().

One difference from unnest() is that generate_subscripts() returns
one row per subscript of the requested dimension, not one row per
element. An exact answer is therefore only possible when the array
is a plan-time constant.

The support function handles three cases:

1. If any argument is a constant NULL, it reports zero rows, since the
function is strict.
2. If both the array and the dimension number are
constants, it reports the exact length of that dimension.
3. If only the dimension number is known and it is 1, it uses
estimate_array_length().

That works because for one-dimensional arrays, the element count
equals the length of dimension 1. In all other cases it declines and
prorows applies as before.

This can change plans for the better. Joining five subscripts
against an indexed table:
Hash Join (cost=637.00..649.63 rows=1000 width=45)
Hash Cond: (s.s = items.id)
-> Function Scan on generate_subscripts s (rows=1000) (actual rows=5)
-> Hash
-> Seq Scan on items (rows=20000)
becomes
Nested Loop (cost=0.29..41.58 rows=5 width=45)
-> Function Scan on generate_subscripts s (rows=5) (actual rows=5)
-> Index Scan using items_pkey on items

A note on the statistics path. estimate_array_length() uses the
DECHIST average, which counts distinct elements, so arrays with many
duplicate or NULL elements get underestimated. unnest() behaves the
same way. Fixing that centrally looks like separate work. The new
regression tests use arrays of distinct elements to keep the expected
output deterministic.

0002 lowers prorows from 1000 to 100. After 0001, prorows is only
reached when the dimension number is unknown at plan time, or when a
higher dimension of a non-constant array is requested. 100 matches
what unnest() uses. I kept it as a separate patch so it can be taken
or dropped on its own.

CatVersion bump is required.

Thanks,
Shihao

#3shihao zhong
zhong950419@gmail.com
In reply to: Priyanka S (#2)
Re: [PATCH] Planner support function for generate_subscripts()

Hi Priyanka,

Thanks for the review, and for testing the patch yourself. v2 is
attached.

1) This is already in the docs. The text below the function table in
func-srf.sgml says: "Zero rows are returned for arrays that do not have
the requested dimension, or if any input is NULL." The entries in the
table itself are kept to a line or two, so I left it where it is.

2) I set arg3 to NULL. It is the only one that is not always given a
value. arg1 and arg2 are set on the next line, so setting them to NULL
first would be a wasted store. I did join their declarations and their
assignments, as you suggest in 4).

3) I left that comment as it is. The same comment sits on the same
test in six other support functions, so it is how we usually write it.

4) I kept those two lines apart. As one line it would be 87 columns
wide, which is too long. The comment also makes more sense right above
the assignment than above the declaration.

5) Added. I used a 3-D array where all three dimensions have a
different length, so a mistake in the dimension indexing could not
still print the right number. I used one case instead of four, since
they all run the same line of code. While there I dropped one of the
old cases: two of them checked the same rule, that we give up for a
dimension above the first when the array is not a constant.

6) You are right. I think the reason is even better than the one you
give.

prorows is only used when the support function gives up.
array_unnest_support() never gives up: it sets req->rows every time it
is asked. So unnest() never uses its prorows of 100. For an array it
knows nothing about, unnest() really estimates 10, which is the default
in estimate_array_length(). My reason for picking 100 was to match
unnest(), but I was matching a number that is never read.

v2-0002 uses 10 instead. Two cases still fall back on prorows, and 10
fits both:

* The dimension number is not known when we plan. At run time it is
almost always 1, and 10 is what the dimension-1 case gives for an
array with no statistics.

* A dimension above the first is asked for on an array that is not a
constant. One dimension of an array is never longer than the whole
array, so a large number is wrong here.

1000 fits neither case.

I kept 0002 as its own patch. It only changes a number in the catalog,
and 0001 does not need it. If people do not agree on the number, 0001
can still go in.

Thanks,
Shihao

Attachments:

t253611_3
v2-0001-Add-a-planner-support-function-for-generate_subsc.patchapplication/octet-stream; name=v2-0001-Add-a-planner-support-function-for-generate_subsc.patchDownload+382-2
v2-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchapplication/octet-stream; name=v2-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchDownload+9-7
#4Andrei Lepikhov
lepihov@gmail.com
In reply to: shihao zhong (#3)
Re: [PATCH] Planner support function for generate_subscripts()

On 08/09/2026 03:21, shihao zhong wrote:

* A dimension above the first is asked for on an array that is not a
  constant.  One dimension of an array is never longer than the whole
  array, so a large number is wrong here.

1000 fits neither case.

I kept 0002 as its own patch.  It only changes a number in the catalog,
and 0001 does not need it.  If people do not agree on the number, 0001
can still go in.

I'd say that detecting the estimate_array_length's default value for the rows
number and returning constant 1000 as used before might make life of engineers
easier and query plans more stable during upgrade.
Hence, maybe it makes sense to follow the way of group-by estimations -
introduce something like one more 'isdefault' parameter to the
estimate_array_length?

Basically, this code looks good. But I'd like to see more assertions: if
something will be changed at the functions itself it would more quickly detect
issues in the prosupport routine. For example:
- Assert(req->root != NULL);
- Type of arg2
- Potentially, no more than 3 arguments of the function
- Maybe hard oid check on F_GENERATE_SUBSCRIPTS / F_GENERATE_SUBSCRIPTS_NODIR

This topic also raises the question of the practical usability of the average
total number of elements in an array column. Your tests highlight a gap in
estimates for more than two dimensions.

regards, Andrei Lepikhov,
pgEdge

#5shihao zhong
zhong950419@gmail.com
In reply to: Andrei Lepikhov (#4)
Re: [PATCH] Planner support function for generate_subscripts()

Hi Andrei,
Thanks for the review. v3 attached.

But I'd like to see more assertions

I took all of these. One small change on the root check. The
comment above get_function_rows() says root can be NULL. So the
function now declines when root is NULL instead of asserting.
Without this check, estimate_expression_value() would crash.
The function also declines when req->funcid is not one of the two
generate_subscripts() entries, or when the call has fewer than two
arguments. These are runtime checks on purpose. Assertions go away
in production builds, and ALTER FUNCTION ... SUPPORT can attach this
function to anything. Once the funcid is checked, the parser
guarantees the argument count and types. So those two stay as plain
assertions.

maybe it makes sense to follow the way of group-by estimations -
introduce something like one more 'isdefault' parameter to the
estimate_array_length?

After 0002, the fallback and the default guess are both 10. So
detecting the default would not change any plan. If you would
rather keep prorows at 1000, an isdefault flag would be the right
tool, and I can rework 0002 that way. I still prefer 10. unnest()
has given 10 for an array with no statistics since v12, and matching
it keeps the two functions consistent.

Your tests highlight a gap in estimates for more than two
dimensions.

Yes. There are no statistics about single dimensions. The DECHIST
number is only an average of distinct element counts. So a higher
dimension of a non-constant array cannot be estimated today. Making
array_typanalyze collect such data would be a separate project.
Until then the patch declines instead of guessing.

Thanks,
Shihao

Attachments:

t253611_5
v3-0001-Add-a-planner-support-function-for-generate_subsc.patchapplication/octet-stream; name=v3-0001-Add-a-planner-support-function-for-generate_subsc.patchDownload+397-3
v3-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchapplication/octet-stream; name=v3-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchDownload+9-8
#6shihao zhong
zhong950419@gmail.com
In reply to: shihao zhong (#5)
Re: [PATCH] Planner support function for generate_subscripts()

Hi,

v3 broke the CompilerWarnings CI task. v4 attached fixes that; nothing
else has changed.

Thanks,
Shihao

Attachments:

t253611_6
v4-0001-Add-a-planner-support-function-for-generate_subsc.patchapplication/octet-stream; name=v4-0001-Add-a-planner-support-function-for-generate_subsc.patchDownload+397-3
v4-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchapplication/octet-stream; name=v4-0002-Lower-generate_subscripts-s-prorows-estimate-to-1.patchDownload+9-8