Re: Add PRODUCT() aggregate function - Mailing list pgsql-hackers
| From | Vaibhav Dalvi |
|---|---|
| Subject | Re: Add PRODUCT() aggregate function |
| Date | |
| Msg-id | CA+vB=AEcvBbkkFUh+DCm9jMqr-T-_T0v=M1uGL1xD1tGSnCv+A@mail.gmail.com Whole thread |
| In response to | Re: Add PRODUCT() aggregate function (Jim Jones <jim.jones@uni-muenster.de>) |
| Responses |
Re: Add PRODUCT() aggregate function
|
| List | pgsql-hackers |
Hi Jeevan,
Thanks for the explanation and the pro() example, it is convincing.
I hadn't considered this properly earlier. for SUM the overflow
depends only on number of rows, but for PRODUCT it depends on the
values itself, so it will overflow in very few steps whenever values
are more than 1. So native fast path will help only for columns
having mostly 0, 1 or -1, not for large tables in general. I also
checked int128.h and you are right, there is no existing function
for overflow-checked "int128 *= int64" type of multiply, so this
needs new code, not reuse of the SUM(int8) pattern. So agree, fine to
take this up later as a follow-up, no need to block on it.
About your question on float8 vs numeric for float variants - I
would prefer float8. sum(float4)/sum(float8) already return
float4/float8, not numeric, so PRODUCT() staying same for float
types will be more consistent. It also avoids the overflow-primitive
problem for floats, since float just becomes Infinity instead of
erroring out. For int2/int4/int8 numeric is fine as it is.
Thanks,
Vaibhav Dalvi
EnterpriseDB
I hadn't considered this properly earlier. for SUM the overflow
depends only on number of rows, but for PRODUCT it depends on the
values itself, so it will overflow in very few steps whenever values
are more than 1. So native fast path will help only for columns
having mostly 0, 1 or -1, not for large tables in general. I also
checked int128.h and you are right, there is no existing function
for overflow-checked "int128 *= int64" type of multiply, so this
needs new code, not reuse of the SUM(int8) pattern. So agree, fine to
take this up later as a follow-up, no need to block on it.
About your question on float8 vs numeric for float variants - I
would prefer float8. sum(float4)/sum(float8) already return
float4/float8, not numeric, so PRODUCT() staying same for float
types will be more consistent. It also avoids the overflow-primitive
problem for floats, since float just becomes Infinity instead of
erroring out. For int2/int4/int8 numeric is fine as it is.
Thanks,
Vaibhav Dalvi
EnterpriseDB
On Thu, Sep 10, 2026 at 7:26 PM Jeevan Chalke <jeevan.chalke@enterprisedb.com> wrote:
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-precisionNumeric conversion plus numeric_mul, even when the running productwould easily fit in int64/int128 for most rows. This file already has a patternfor 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 alarge table will be much slower per row than SUM for the same data, becauseof 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
EnterpriseDBOn 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.ThanksOn 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------
pgsql-hackers by date:
