Add PRODUCT() aggregate function

Started by Jeevan Chalke3 months ago25 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:t248615
psql -h localhost -U postgres

Built from patchset v7 (message #7), September 17, 2026 at 07:09 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 t248615_7 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 t248615_7 && git checkout t248615_7

Patchset v7 (message #7) is on t248615_7

Jump to latest
#1Jeevan Chalke
jeevan.chalke@enterprisedb.com

Hello Hackers,

PostgreSQL has aggregates for summing a set of values (sum()) but not for
multiplying them. Computing the product of a column is a fairly common
request -- e.g. compounding growth/return factors, combining independent
probabilities, computing factorial-like or geometric quantities -- and today
it requires either a custom aggregate or the exp(sum(ln(...))) trick, which
does not work for zero or negative inputs and loses precision.

This idea was proposed by Peter Eisentraut, and the attached patch
implements
a built-in PRODUCT() aggregate.

*What it does*
-----

PRODUCT() returns the product of all non-null input values. It is defined
for
int2, int4, int8, float4, float8 and numeric input, and always returns
numeric.

A few examples:

CREATE TABLE t (g int, v int);
INSERT INTO t VALUES (1,2),(1,3),(1,4),(2,5),(2,-6),(2,0);

SELECT product(v) FROM t;
product
---------
0

SELECT g, product(v) FROM t WHERE v <> 0 GROUP BY g ORDER BY g;
g | product
---+---------
1 | 24
2 | -30

Like sum(), PRODUCT() ignores NULL inputs and returns NULL for an empty
input
set (or a group consisting only of NULLs).

*Design / implementation notes*
-----

* The result type and the internal transition state are both numeric,
regardless of the input type. Using numeric for the running product
avoids
overflow in the intermediate state for the integer and floating-point
variants, where a product grows much faster than a sum. (A sufficiently
large product can of course still overflow numeric and raise an error.)

* For numeric input, the transition and combine functions are simply the
existing numeric_mul(). For the other input types, small non-strict
transition functions (int2_product_accum, int4_product_accum,
int8_product_accum, float4_product_accum, float8_product_accum) promote
the
input to numeric and then call numeric_mul().

* PRODUCT() supports Partial Mode (parallel aggregation), using
numeric_mul()
as the combine function. Because the transition type is numeric rather
than
internal, no serialization/deserialization functions are needed.

* No inverse transition (moving-aggregate) function is provided. An inverse
for a product would require division, which is unreliable or undefined
when
any input is zero (and lossy in general), so as a window aggregate over a
moving frame PRODUCT() falls back to recomputing the frame.

*Open questions*
-----

* Naming. I went with PRODUCT(); other systems and discussions have used
names like PROD or MUL. Happy to change it if there is a consensus.

* Return type. Always returning numeric is the safe choice for overflow,
but
it does mean product(double precision) returns numeric rather than a float
.
An alternative would be to return float8 for the floating-point inputs. I
leaned towards numeric for consistency and to avoid overflow surprises;
feedback welcome.

* Type coverage. The patch covers the standard numeric input types. money
and interval were intentionally left out, since a product of those types
has
no clear meaning.

*Testing / docs*
-----

The patch adds regression tests for all input types and the relevant edge
cases
(NULLs, DISTINCT, FILTER, zero/negative inputs, Infinity/NaN, overflow,
parallel
aggregation, and window usage), along with documentation updates.

Thoughts and review feedback are very welcome.

Thanks

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

Attachments:

v1-0001-Add-PRODUCT-aggregate-function.patchapplication/octet-stream; name=v1-0001-Add-PRODUCT-aggregate-function.patchDownload+1024-2
#2Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Jeevan Chalke (#1)
Re: Add PRODUCT() aggregate function

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is defined for
int2, int4, int8, float4, float8 and numeric input, and always returns numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

Regards,
Dean

#3Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Dean Rasheed (#2)
Re: Add PRODUCT() aggregate function

On Tue, 23 Jun 2026 at 09:37, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is defined for
int2, int4, int8, float4, float8 and numeric input, and always returns numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

... and perhaps make the float8 version return float8.

Regards,
Dean

#4Jim Jones
jim.jones@uni-muenster.de
In reply to: Dean Rasheed (#2)
Re: Add PRODUCT() aggregate function

Hi Jeevan

On 23/06/2026 10:37, Dean Rasheed wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is defined for
int2, int4, int8, float4, float8 and numeric input, and always returns numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

+1

I've tested the patch in many different scenarios and all results look
fine -- valgrind also didn't report anything :)

The test coverage is comprehensive! For the sake of completeness I'd add
numeric tests for NaN and Infitinty with positive numeric values in the
set, e.g:

postgres=# WITH j (v) AS (VALUES
('NaN'::numeric),('Infinity'::numeric),(3.14))
SELECT product(v) FROM j;
product
---------
NaN
(1 row)

Other than that and the point mentioned by Dean I have nothing to add at
this point.

Thanks for the patch.

Best, Jim

#5Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Dean Rasheed (#3)
Re: Add PRODUCT() aggregate function

On Tue, Jun 23, 2026 at 2:13 PM Dean Rasheed <dean.a.rasheed@gmail.com>
wrote:

On Tue, 23 Jun 2026 at 09:37, Dean Rasheed <dean.a.rasheed@gmail.com>
wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is

defined for

int2, int4, int8, float4, float8 and numeric input, and always returns

numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

Thank you, Dean, for looking at this.

I appreciate the suggestion, but I don't think the implicit-casting
approach
works well here, for the following reasons:

1. The integer types (int2/int4/int8) have implicit casts to both float8
and
numeric. Since float8 is the preferred type in the numeric type category,
the function resolution machinery would select the product(float8) variant.
That has two consequences: an extra cast function has to be executed per
row,
and, more importantly, integer inputs would be accumulated as float8.
For a large product that yields a lossy result in exponent form, whereas
accumulating in numeric gives an exact answer.

2. This approach is also consistent with the existing aggregates —
sum(), avg(), min()/max(), etc. all define per-type variants rather than
relying on implicit input casting. The patch follows that established
pattern rather than introducing a new one.

3. There is also a small performance penalty to the casting approach:
the per-row execution of the cast function itself, in addition to the
resolution issue noted in (1).

I ran a quick test to demonstrate point (1). For large products, the float8
implementation returns a lossy, exponential result like 9.9999970000003e+20,

whereas numeric returns the exact value of 999999700000029999999.

I would be happy to share the test script if it's helpful.

... and perhaps make the float8 version return float8.

Yes, I already noted this under "Open questions" in my first email. We can
certainly make the float4 and float8 variants return float8 instead of
numeric. Let's see what others think before making that adjustment.

Thanks

Regards,
Dean

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#6Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Jim Jones (#4)
Re: Add PRODUCT() aggregate function

On Tue, Jun 23, 2026 at 4:32 PM Jim Jones <jim.jones@uni-muenster.de> wrote:

Hi Jeevan

On 23/06/2026 10:37, Dean Rasheed wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is

defined for

int2, int4, int8, float4, float8 and numeric input, and always returns

numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

+1

I've tested the patch in many different scenarios and all results look
fine -- valgrind also didn't report anything :)

The test coverage is comprehensive! For the sake of completeness I'd add
numeric tests for NaN and Infitinty with positive numeric values in the
set, e.g:

postgres=# WITH j (v) AS (VALUES
('NaN'::numeric),('Infinity'::numeric),(3.14))
SELECT product(v) FROM j;
product
---------
NaN
(1 row)

Other than that and the point mentioned by Dean I have nothing to add at
this point.

Thanks, Jim, for the thorough testing.

I'll include that test case in the next version of the patch.

Thanks for the patch.

Best, Jim

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#7Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Jeevan Chalke (#6)
Re: Add PRODUCT() aggregate function

Hello,

CFbot flagged this for a rebase. The conflicts were due to the catalog
version bump, so I've dropped it here and noted in the commit message
that the committer should bump catversion at commit time to avoid
recurring conflicts.

Also added tests as suggested by Jim.

Thanks

On Tue, Jun 23, 2026 at 5:26 PM Jeevan Chalke <
jeevan.chalke@enterprisedb.com> wrote:

On Tue, Jun 23, 2026 at 4:32 PM Jim Jones <jim.jones@uni-muenster.de>
wrote:

Hi Jeevan

On 23/06/2026 10:37, Dean Rasheed wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is

defined for

int2, int4, int8, float4, float8 and numeric input, and always returns

numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

+1

I've tested the patch in many different scenarios and all results look
fine -- valgrind also didn't report anything :)

The test coverage is comprehensive! For the sake of completeness I'd add
numeric tests for NaN and Infitinty with positive numeric values in the
set, e.g:

postgres=# WITH j (v) AS (VALUES
('NaN'::numeric),('Infinity'::numeric),(3.14))
SELECT product(v) FROM j;
product
---------
NaN
(1 row)

Other than that and the point mentioned by Dean I have nothing to add at
this point.

Thanks, Jim, for the thorough testing.

I'll include that test case in the next version of the patch.

Thanks for the patch.

Best, Jim

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

Attachments:

t248615_7
v2-0001-Add-PRODUCT-aggregate-function.patchapplication/octet-stream; name=v2-0001-Add-PRODUCT-aggregate-function.patchDownload+1073-1
#8Vaibhav Dalvi
vaibhav.dalvi@enterprisedb.com
In reply to: Jeevan Chalke (#7)
Re: Add PRODUCT() aggregate function

Hi Jeevan,

Nice feature; I tested it locally and it works correctly. NULL
handling, parallel aggregate (combine), and the moving-window.
Fallback to recalculation are all fine, no correctness bug was found.
I only have the following point with a short description.

*There is no fast path for the common case; it always goes through Numeric:*
For int2/int4/int8/float4/float8, every row undergoes a full
arbitrary-precision
Numeric conversion plus numeric_mul, even when the running product
would easily fit in int64/int128 for most rows. This file already has a
pattern
for exactly this problem (int8 SUM uses int128 internally, only promoting to
numeric on real overflow). I think PRODUCT(int4)/PRODUCT(int2) over a
large table will be much slower per row than SUM for the same data, because
of this.

So, if possible, consider using the same native-then-promote-on-overflow
approach here.

Thanks,
Vaibhav Dalvi
EnterpriseDB

On Fri, Jun 26, 2026 at 11:24 AM Jeevan Chalke <
jeevan.chalke@enterprisedb.com> wrote:

Show quoted text

Hello,

CFbot flagged this for a rebase. The conflicts were due to the catalog
version bump, so I've dropped it here and noted in the commit message
that the committer should bump catversion at commit time to avoid
recurring conflicts.

Also added tests as suggested by Jim.

Thanks

On Tue, Jun 23, 2026 at 5:26 PM Jeevan Chalke <
jeevan.chalke@enterprisedb.com> wrote:

On Tue, Jun 23, 2026 at 4:32 PM Jim Jones <jim.jones@uni-muenster.de>
wrote:

Hi Jeevan

On 23/06/2026 10:37, Dean Rasheed wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is

defined for

int2, int4, int8, float4, float8 and numeric input, and always

returns numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

+1

I've tested the patch in many different scenarios and all results look
fine -- valgrind also didn't report anything :)

The test coverage is comprehensive! For the sake of completeness I'd add
numeric tests for NaN and Infitinty with positive numeric values in the
set, e.g:

postgres=# WITH j (v) AS (VALUES
('NaN'::numeric),('Infinity'::numeric),(3.14))
SELECT product(v) FROM j;
product
---------
NaN
(1 row)

Other than that and the point mentioned by Dean I have nothing to add at
this point.

Thanks, Jim, for the thorough testing.

I'll include that test case in the next version of the patch.

Thanks for the patch.

Best, Jim

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#9Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Vaibhav Dalvi (#8)
Re: Add PRODUCT() aggregate function

Thank you, Vaibhav, for the review and the comment. Really appreciate it.

When I started working on this, I did look at Int128AggState and wondered
whether the same trick could be used for integer products. It can't, at
least not without new infrastructure.

Int128AggState avoids ever needing an overflow check because the values it
accumulates stay bounded well within 128 bits for any realistically sized
table: sum(int8) only accumulates via plain int128 addition, and each int64
input is at most 2^63, so sumX can't overflow until you've summed roughly
2^64 rows -- no real table gets remotely close to that.

PRODUCT has the opposite problem: it's the multiply itself that can
overflow, and there's currently no overflow-checked 128-bit multiply
primitive in int128.h to build on. Adding one -- plus the serialize/
deserialize/combine plumbing an internal transition type would need --
felt like overkill for an initial feature. I'd rather land PRODUCT() as
proposed and treat this as a follow-up optimization once it's in use.

On Thu, Sep 10, 2026 at 4:39 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:

Hi Jeevan,

Nice feature; I tested it locally and it works correctly. NULL
handling, parallel aggregate (combine), and the moving-window.
Fallback to recalculation are all fine, no correctness bug was found.
I only have the following point with a short description.

*There is no fast path for the common case; it always goes through
Numeric:*
For int2/int4/int8/float4/float8, every row undergoes a full
arbitrary-precision
Numeric conversion plus numeric_mul, even when the running product
would easily fit in int64/int128 for most rows. This file already has a
pattern
for exactly this problem (int8 SUM uses int128 internally, only promoting
to
numeric on real overflow).

I did a quick test to see how fast that "fits in int64" window closes,
multiplying the same number in a loop:

create or replace function pro(a int, b int) returns bigint as $$
declare
p bigint default 1;
begin
for i in 1 .. a loop
p := p * b;
end loop;
return p;
end; $$ language plpgsql;

# select pro(100, 2);
ERROR: bigint out of range

# select pro(5, 32767);
ERROR: bigint out of range

Multiplying 2 by itself overflows bigint well before 100 iterations (2^63
is the limit), and multiplying by the max smallint value overflows in just
5. Since PRODUCT() grows multiplicatively, the bigint/int128 range gets
exhausted very quickly for realistic inputs -- which is why I promoted to
numeric from the start rather than trying to stay native.

I think PRODUCT(int4)/PRODUCT(int2) over a
large table will be much slower per row than SUM for the same data,
because
of this.

So, if possible, consider using the same native-then-promote-on-overflow
approach here.

The same reasoning applies to the float variants. That said, I'm open to
returning float8 for those instead, despite its narrower range than
numeric, if reviewers prefer that.

Thanks,

Thanks,
Vaibhav Dalvi
EnterpriseDB

On Fri, Jun 26, 2026 at 11:24 AM Jeevan Chalke <
jeevan.chalke@enterprisedb.com> wrote:

Hello,

CFbot flagged this for a rebase. The conflicts were due to the catalog
version bump, so I've dropped it here and noted in the commit message
that the committer should bump catversion at commit time to avoid
recurring conflicts.

Also added tests as suggested by Jim.

Thanks

On Tue, Jun 23, 2026 at 5:26 PM Jeevan Chalke <
jeevan.chalke@enterprisedb.com> wrote:

On Tue, Jun 23, 2026 at 4:32 PM Jim Jones <jim.jones@uni-muenster.de>
wrote:

Hi Jeevan

On 23/06/2026 10:37, Dean Rasheed wrote:

On Tue, 23 Jun 2026 at 08:49, Jeevan Chalke
<jeevan.chalke@enterprisedb.com> wrote:

PRODUCT() returns the product of all non-null input values. It is

defined for

int2, int4, int8, float4, float8 and numeric input, and always

returns numeric.

I don't think that you need to define it for all those types. I
suspect that you could just define it for numeric and float8, and let
implicit casting do the rest.

+1

I've tested the patch in many different scenarios and all results look
fine -- valgrind also didn't report anything :)

The test coverage is comprehensive! For the sake of completeness I'd add
numeric tests for NaN and Infitinty with positive numeric values in the
set, e.g:

postgres=# WITH j (v) AS (VALUES
('NaN'::numeric),('Infinity'::numeric),(3.14))
SELECT product(v) FROM j;
product
---------
NaN
(1 row)

Other than that and the point mentioned by Dean I have nothing to add at
this point.

Thanks, Jim, for the thorough testing.

I'll include that test case in the next version of the patch.

Thanks for the patch.

Best, Jim

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#10Vik Fearing
vik@postgresfriends.org
In reply to: Jeevan Chalke (#1)
Re: Add PRODUCT() aggregate function

On 23/06/2026 09:48, Jeevan Chalke wrote:

* Naming.  I went with PRODUCT(); other systems and discussions have used
  names like PROD or MUL.  Happy to change it if there is a consensus.

Hi!

It should be called PRODUCT and it should accept the syntax

    PRODUCT(col, 1 ON EMPTY)

for when there are no non-nulls in the input.

This will be required by the next edition of the SQL standard. The exact
value "1" is required by the standard, but I think we should allow any
a_expr there.

--

Vik Fearing

#11Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Vik Fearing (#10)
Re: Add PRODUCT() aggregate function

Hello Vik,

On Thu, Sep 10, 2026 at 8:22 PM Vik Fearing <vik@postgresfriends.org> wrote:

On 23/06/2026 09:48, Jeevan Chalke wrote:

* Naming. I went with PRODUCT(); other systems and discussions have used
names like PROD or MUL. Happy to change it if there is a consensus.

Hi!

It should be called PRODUCT

Thanks for seconding the aggregate name PRODUCT.

and it should accept the syntax

PRODUCT(col, 1 ON EMPTY)

for when there are no non-nulls in the input.

This will be required by the next edition of the SQL standard. The exact
value "1" is required by the standard, but I think we should allow any
a_expr there.

Regarding the ON EMPTY clause, I have actually already proposed a patch for
that here:
/messages/by-id/CAM2+6=VS=fSKxfimW6Th9iu_xjbxOEAKg4eYwaa=SMg3X8pHaQ@mail.gmail.com

It is designed to be applicable to any aggregate function. Note that in its
current form, the patch accepts any arbitrary expression (constant) for the
return value, rather than restricting it to a hard-coded literal.

I would really appreciate it if you could take a look at those changes and
share your reviews, feedback, or suggestions on that thread.
Thanks,

--

Vik Fearing

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#12Tom Lane
tgl@sss.pgh.pa.us
In reply to: Vik Fearing (#10)
Re: Add PRODUCT() aggregate function

Vik Fearing <vik@postgresfriends.org> writes:

It should be called PRODUCT and it should accept the syntax
    PRODUCT(col, 1 ON EMPTY)
for when there are no non-nulls in the input.

Sigh. The committee really loves to invent randomly creative syntax,
don't they. At least this one won't force us to create any new
fully-reserved words.

This will be required by the next edition of the SQL standard. The exact
value "1" is required by the standard, but I think we should allow any
a_expr there.

I think that this may be trickier than it looks. You'd really want
that to act like a "direct" argument, ie evaluate once not once per
row. Also, if PRODUCT can use this, SUM could use it even more,
and probably other aggregates too (but likely values would be
different from "1"). Did they generalize at all, or is this a
PRODUCT-specific wart?

Anyway, I'd counsel thinking of this ON EMPTY business as an
orthogonal feature to PRODUCT. My real concern about PRODUCT
as such is that it seems enormously prone to overflow. It'd
make little sense to invent variants emitting anything except
numeric or float8.

regards, tom lane

#13Vik Fearing
vik@postgresfriends.org
In reply to: Tom Lane (#12)
Re: Add PRODUCT() aggregate function

On 10/09/2026 18:16, Tom Lane wrote:

Vik Fearing <vik@postgresfriends.org> writes:

It should be called PRODUCT and it should accept the syntax
    PRODUCT(col, 1 ON EMPTY)
for when there are no non-nulls in the input.

Sigh. The committee really loves to invent randomly creative syntax,
don't they. At least this one won't force us to create any new
fully-reserved words.

Yes.  I tried to fight back on it but I was overruled.

This will be required by the next edition of the SQL standard. The exact
value "1" is required by the standard, but I think we should allow any
a_expr there.

I think that this may be trickier than it looks. You'd really want
that to act like a "direct" argument, ie evaluate once not once per
row.

That's fair, but it isn't what happens for string_agg, for example,
which can have a different separator per value.

SELECT string_agg(col, sep)
FROM (VALUES ('a', ','), ('b', ':'), ('c', 'd')) AS v (col, sep);

Result: a:bdc

Also, if PRODUCT can use this, SUM could use it even more,
and probably other aggregates too (but likely values would be
different from "1"). Did they generalize at all, or is this a
PRODUCT-specific wart?

It's for both PRODUCT and SUM (1 and 0 respectively) and not generalized
beyond that.

I tried to at least make it IDENTITY ON EMPTY but that wasn't even
understood!

--

Vik Fearing

#14Tom Lane
tgl@sss.pgh.pa.us
In reply to: Vik Fearing (#13)
Re: Add PRODUCT() aggregate function

Vik Fearing <vik@postgresfriends.org> writes:

On 10/09/2026 18:16, Tom Lane wrote:

I think that this may be trickier than it looks. You'd really want
that to act like a "direct" argument, ie evaluate once not once per
row.

That's fair, but it isn't what happens for string_agg, for example,
which can have a different separator per value.

Sure, but that's not the same thing. You can do something credible
with a separator-per-value in string_agg, but it's nonsense to suppose
that ON EMPTY is a per-row value. If it were per-row, which value
would you use? I assume it applies even if there are zero input rows,
not only if there are some inputs but they happen to all be null.

The committee is evidently choosing to sidestep the
how-many-evaluations question by insisting on a constant value,
which may well be sufficient for all real-world cases. If we want
it to be "any a_expr" though, we have to think about that.

It's for both PRODUCT and SUM (1 and 0 respectively) and not generalized
beyond that.

OK, at least the SUM case occurred to them ;-). But I think for
our purposes we definitely want to allow it for any aggregate.

regards, tom lane

#15Isaac Morland
isaac.morland@gmail.com
In reply to: Vik Fearing (#13)
Re: Add PRODUCT() aggregate function

On Thu, 10 Sept 2026 at 19:29, Vik Fearing <vik@postgresfriends.org> wrote:

On 10/09/2026 18:16, Tom Lane wrote:

Vik Fearing <vik@postgresfriends.org> writes:

It should be called PRODUCT and it should accept the syntax
PRODUCT(col, 1 ON EMPTY)
for when there are no non-nulls in the input.

Sigh. The committee really loves to invent randomly creative syntax,
don't they. At least this one won't force us to create any new
fully-reserved words.

Yes. I tried to fight back on it but I was overruled.

Out of curiosity, was there any thought given to the idea that it should be
an attribute of the aggregate itself? The following seem to me to be pretty
hard to dispute: max -> -infinity; min -> infinity; sum -> 0; product -> 1;
string_agg -> ''; array_agg -> []. Of course there is legacy behaviour that
can't just be erased, but an option with just two choices defaulting to the
NULL result could handle that.

#16Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Tom Lane (#14)
Re: Add PRODUCT() aggregate function

On Fri, Sep 11, 2026 at 5:16 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Vik Fearing <vik@postgresfriends.org> writes:

On 10/09/2026 18:16, Tom Lane wrote:

I think that this may be trickier than it looks. You'd really want
that to act like a "direct" argument, ie evaluate once not once per
row.

That's fair, but it isn't what happens for string_agg, for example,
which can have a different separator per value.

Sure, but that's not the same thing. You can do something credible
with a separator-per-value in string_agg, but it's nonsense to suppose
that ON EMPTY is a per-row value. If it were per-row, which value
would you use? I assume it applies even if there are zero input rows,
not only if there are some inputs but they happen to all be null.

In my currently proposed patch (
/messages/by-id/CAM2+6=VS=fSKxfimW6Th9iu_xjbxOEAKg4eYwaa=SMg3X8pHaQ@mail.gmail.com),
the ON EMPTY value is strictly returned only when there are zero input
rows. Rows containing NULL are treated as valid rows and do not trigger the ON
EMPTY clause.

The committee is evidently choosing to sidestep the
how-many-evaluations question by insisting on a constant value,
which may well be sufficient for all real-world cases. If we want
it to be "any a_expr" though, we have to think about that.

It's for both PRODUCT and SUM (1 and 0 respectively) and not generalized
beyond that.

OK, at least the SUM case occurred to them ;-). But I think for
our purposes we definitely want to allow it for any aggregate.

Yes, the proposed patch supports this for all aggregate functions and is
not restricted to specific ones at the moment.

Additionally, while the grammar accepts an a_expr, the backend code
includes checks to ensure the provided expression is a constant value that
is type-coercible to the aggregate's result type.

I would be very happy to receive any feedback or comments on that thread as
well.

Thanks,

regards, tom lane

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeevan Chalke (#16)
Re: Add PRODUCT() aggregate function

Jeevan Chalke <jeevan.chalke@enterprisedb.com> writes:

In my currently proposed patch (
/messages/by-id/CAM2+6=VS=fSKxfimW6Th9iu_xjbxOEAKg4eYwaa=SMg3X8pHaQ@mail.gmail.com),
the ON EMPTY value is strictly returned only when there are zero input
rows. Rows containing NULL are treated as valid rows and do not trigger the ON
EMPTY clause.

[ ... not having read the patch ... ] There is a critical distinction
here between strict and non-strict aggregates. My interpretation of
how this should work is that ON EMPTY should trigger if zero rows were
fed to the aggregate's transition function. A row containing NULL is
valid input if the transition function is non-strict, otherwise it is
not.

What I gather from Vik's comments is that the SQL committee only
formalized the behavior for strict aggregates (since both PRODUCT
and SUM ignore nulls). So we're somewhat out on a limb here for
the non-strict case, but I think we have to define that one as
being "null inputs count as inputs".

regards, tom lane

#18Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#17)
Re: Add PRODUCT() aggregate function

... btw, there is another interesting definitional question here.
AFAICS, "ON EMPTY" effectively is an override for the aggregate's
final function. Does it actually make sense when there is a final
function? Specifically, if the final function were willing to
provide non-null output for zero rows in, should we still override
it? SUM and PRODUCT don't provide a lot of guidance here.

I'm also wondering idly how this interacts with "inverse transition
functions" for aggregates used as window functions.

regards, tom lane

#19Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Tom Lane (#17)
Re: Add PRODUCT() aggregate function

On Fri, Sep 11, 2026 at 8:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Jeevan Chalke <jeevan.chalke@enterprisedb.com> writes:

In my currently proposed patch (

/messages/by-id/CAM2+6=VS=fSKxfimW6Th9iu_xjbxOEAKg4eYwaa=SMg3X8pHaQ@mail.gmail.com
),

the ON EMPTY value is strictly returned only when there are zero input
rows. Rows containing NULL are treated as valid rows and do not trigger

the ON

EMPTY clause.

[ ... not having read the patch ... ] There is a critical distinction
here between strict and non-strict aggregates. My interpretation of
how this should work is that ON EMPTY should trigger if zero rows were
fed to the aggregate's transition function. A row containing NULL is
valid input if the transition function is non-strict, otherwise it is
not.

What I gather from Vik's comments is that the SQL committee only
formalized the behavior for strict aggregates (since both PRODUCT
and SUM ignore nulls). So we're somewhat out on a limb here for
the non-strict case, but I think we have to define that one as
being "null inputs count as inputs".

Agree with the strict/non-strict point. But the SQL standard text for this
is
not available yet, so I am not sure what exact behaviour we should follow
here.

Do you or Vik have more details on what the committee is going with? That
will
help us decide the correct semantics instead of guessing.

Thanks

regards, tom lane

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#20Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Tom Lane (#18)
Re: Add PRODUCT() aggregate function

On Fri, Sep 11, 2026 at 8:16 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... btw, there is another interesting definitional question here.
AFAICS, "ON EMPTY" effectively is an override for the aggregate's
final function. Does it actually make sense when there is a final
function? Specifically, if the final function were willing to
provide non-null output for zero rows in, should we still override
it? SUM and PRODUCT don't provide a lot of guidance here.

The patch currently short-circuits and returns the ON EMPTY value anytime
zero
rows are processed, bypassing the aggregate's final function entirely. This
makes the most sense to me from a user perspective -- if someone explicitly
specifies ON EMPTY, they want that exact value to take precedence over the
aggregate's default empty-set behavior.

I'm also wondering idly how this interacts with "inverse transition
functions" for aggregates used as window functions.

I believe this scenario is already handled correctly. When a window frame
shrinks
and loses its last row, advance_windowaggregate_base() bypasses the inverse
transition function entirely. Instead, it deliberately falls back to
initialize_windowaggregate() to restore the true initial state. This
reinitialization cleanly resets our inputReceived flag to false, ensuring
that
a frame emptying out via incremental removal is accurately detected as
empty.

I'd be happy to discuss this further on the relevant thread.

Thanks

regards, tom lane

--
*Jeevan Chalke*
*Senior Principal Engineer, Engineering Manager*
*Product Development*

enterprisedb.com <https://www.enterprisedb.com&gt;

#21Vik Fearing
vik@postgresfriends.org
In reply to: Jeevan Chalke (#19)
#22Vik Fearing
vik@postgresfriends.org
In reply to: Jeevan Chalke (#1)
#23Vik Fearing
vik@postgresfriends.org
In reply to: Jeevan Chalke (#11)
#24Jeevan Chalke
jeevan.chalke@enterprisedb.com
In reply to: Vik Fearing (#23)
#25Vaibhav Dalvi
vaibhav.dalvi@enterprisedb.com
In reply to: Jeevan Chalke (#9)