From 8267ac672bd6a7dd5f56b6df303bdb4d81bc419a Mon Sep 17 00:00:00 2001 From: Jeevan Chalke Date: Tue, 23 Jun 2026 11:28:10 +0530 Subject: [PATCH v1] Add PRODUCT() aggregate function This commit introduces the PRODUCT() aggregate to compute the product of all input values in a set. The following input types are supported: int2, int4, int8, float4, float8, and numeric. The internal transition state uses the numeric type to prevent overflow during multiplication. For non-numeric inputs, dedicated accumulator functions (*_product_accum) handle the promotion to numeric before invoking numeric_mul for the transition steps. Because division by zero (or near-zero) makes inverse operations unreliable or undefined, this implementation does not provide inverse transition functions. Moving aggregates will instead fall back to the standard recalculation behavior. Includes regression tests for all supported types, including edge cases involving NULLs, along with relevant documentation updates. Also, bump CATALOG_VERSION_NO due to the new catalog entries. Proposed-by: Peter Eisentraut Jeevan Chalke --- doc/src/sgml/func/func-aggregate.sgml | 42 ++ src/backend/utils/adt/numeric.c | 129 ++++++ src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_aggregate.dat | 14 + src/include/catalog/pg_proc.dat | 34 ++ src/test/regress/expected/aggregates.out | 526 +++++++++++++++++++++++ src/test/regress/expected/window.out | 67 +++ src/test/regress/sql/aggregates.sql | 183 ++++++++ src/test/regress/sql/window.sql | 28 ++ 9 files changed, 1024 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml index 8b5eaeb2e94..75cbee97148 100644 --- a/doc/src/sgml/func/func-aggregate.sgml +++ b/doc/src/sgml/func/func-aggregate.sgml @@ -534,6 +534,40 @@ Yes + + + + product + + product ( smallint ) + numeric + + + product ( integer ) + numeric + + + product ( bigint ) + numeric + + + product ( numeric ) + numeric + + + product ( real ) + numeric + + + product ( double precision ) + numeric + + + Computes the product of the non-null input values. + + Yes + + @@ -667,6 +701,14 @@ substitute zero or an empty array for null when necessary. + + The product aggregate always returns a + numeric result, regardless of the argument type, and + accumulates its running product as numeric to avoid + intermediate overflow. A sufficiently large product can still + overflow the numeric type and raise an error. + + The aggregate functions array_agg, json_agg, jsonb_agg, diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c index cb23dfe9b95..e1fc22e80ed 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -12161,3 +12161,132 @@ accum_sum_combine(NumericSumAccum *accum, NumericSumAccum *accum2) free_var(&tmp_var); } + +/* PRODUCT aggregate supporting functions */ + +/* state transition function for PRODUCT(int2) aggregate */ +Datum +int2_product_accum(PG_FUNCTION_ARGS) +{ + Datum dvalue; + Datum result; + + /* Need to handle NULLs as this is a non-strict function */ + + if (PG_ARGISNULL(0) && PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + if (PG_ARGISNULL(1)) + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); + + dvalue = NumericGetDatum(int64_to_numeric(PG_GETARG_INT16(1))); + + if (PG_ARGISNULL(0)) + PG_RETURN_DATUM(dvalue); + + result = DirectFunctionCall2(numeric_mul, PG_GETARG_DATUM(0), dvalue); + + return result; +} + +/* state transition function for PRODUCT(int4) aggregate */ +Datum +int4_product_accum(PG_FUNCTION_ARGS) +{ + Datum dvalue; + Datum result; + + /* Need to handle NULLs as this is a non-strict function */ + + if (PG_ARGISNULL(0) && PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + if (PG_ARGISNULL(1)) + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); + + dvalue = NumericGetDatum(int64_to_numeric(PG_GETARG_INT32(1))); + + if (PG_ARGISNULL(0)) + PG_RETURN_DATUM(dvalue); + + result = DirectFunctionCall2(numeric_mul, PG_GETARG_DATUM(0), dvalue); + + return result; +} + +/* state transition function for PRODUCT(int8) aggregate */ +Datum +int8_product_accum(PG_FUNCTION_ARGS) +{ + Datum dvalue; + Datum result; + + /* Need to handle NULLs as this is a non-strict function */ + + if (PG_ARGISNULL(0) && PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + if (PG_ARGISNULL(1)) + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); + + dvalue = NumericGetDatum(int64_to_numeric(PG_GETARG_INT64(1))); + + if (PG_ARGISNULL(0)) + PG_RETURN_DATUM(dvalue); + + result = DirectFunctionCall2(numeric_mul, PG_GETARG_DATUM(0), dvalue); + + return result; +} + +/* state transition function for PRODUCT(float4) aggregate */ +Datum +float4_product_accum(PG_FUNCTION_ARGS) +{ + Datum dvalue; + Datum result; + + /* Need to handle NULLs as this is a non-strict function */ + + if (PG_ARGISNULL(0) && PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + if (PG_ARGISNULL(1)) + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); + + dvalue = DirectFunctionCall1(float4_numeric, + Float4GetDatum(PG_GETARG_FLOAT4(1))); + + if (PG_ARGISNULL(0)) + PG_RETURN_DATUM(dvalue); + + result = DirectFunctionCall2(numeric_mul, PG_GETARG_DATUM(0), dvalue); + + return result; +} + +/* state transition function for PRODUCT(float8) aggregate */ +Datum +float8_product_accum(PG_FUNCTION_ARGS) +{ + Datum dvalue; + Datum result; + + /* Need to handle NULLs as this is a non-strict function */ + + if (PG_ARGISNULL(0) && PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + if (PG_ARGISNULL(1)) + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); + + dvalue = DirectFunctionCall1(float8_numeric, + Float8GetDatum(PG_GETARG_FLOAT8(1))); + + if (PG_ARGISNULL(0)) + PG_RETURN_DATUM(dvalue); + + result = DirectFunctionCall2(numeric_mul, PG_GETARG_DATUM(0), dvalue); + + return result; +} diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index c4e94a3a09e..b682c565d5d 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606091 +#define CATALOG_VERSION_NO 202606191 #endif diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat index 5fd38fd4f23..5f223d89233 100644 --- a/src/include/catalog/pg_aggregate.dat +++ b/src/include/catalog/pg_aggregate.dat @@ -91,6 +91,20 @@ aggtransspace => '128', aggmtranstype => 'internal', aggmtransspace => '128' }, +# product +{ aggfnoid => 'product(int2)', aggtransfn => 'int2_product_accum', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, +{ aggfnoid => 'product(int4)', aggtransfn => 'int4_product_accum', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, +{ aggfnoid => 'product(int8)', aggtransfn => 'int8_product_accum', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, +{ aggfnoid => 'product(float4)', aggtransfn => 'float4_product_accum', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, +{ aggfnoid => 'product(float8)', aggtransfn => 'float8_product_accum', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, +{ aggfnoid => 'product(numeric)', aggtransfn => 'numeric_mul', + aggcombinefn => 'numeric_mul', aggtranstype => 'numeric' }, + # max { aggfnoid => 'max(int8)', aggtransfn => 'int8larger', aggcombinefn => 'int8larger', aggsortop => '>(int8,int8)', diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index be157a5fbe9..e26bdfb65f8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7034,6 +7034,40 @@ proname => 'sum', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', proargtypes => 'numeric', prosrc => 'aggregate_dummy' }, +{ oid => '4551', descr => 'product as numeric across all smallint input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'int2', prosrc => 'aggregate_dummy' }, +{ oid => '4552', descr => 'product as numeric across all integer input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'int4', prosrc => 'aggregate_dummy' }, +{ oid => '4553', descr => 'product as numeric across all bigint input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'int8', prosrc => 'aggregate_dummy' }, +{ oid => '4554', descr => 'product as numeric across all float4 input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'float4', prosrc => 'aggregate_dummy' }, +{ oid => '4555', descr => 'product as numeric across all float8 input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'float8', prosrc => 'aggregate_dummy' }, +{ oid => '4556', descr => 'product as numeric across all numeric input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric', prosrc => 'aggregate_dummy' }, +{ oid => '4557', descr => 'aggregate transition function', + proname => 'int2_product_accum', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric int2', prosrc => 'int2_product_accum' }, +{ oid => '4558', descr => 'aggregate transition function', + proname => 'int4_product_accum', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric int4', prosrc => 'int4_product_accum' }, +{ oid => '4559', descr => 'aggregate transition function', + proname => 'int8_product_accum', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric int8', prosrc => 'int8_product_accum' }, +{ oid => '4560', descr => 'aggregate transition function', + proname => 'float4_product_accum', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric float4', prosrc => 'float4_product_accum' }, +{ oid => '4561', descr => 'aggregate transition function', + proname => 'float8_product_accum', proisstrict => 'f', prorettype => 'numeric', + proargtypes => 'numeric float8', prosrc => 'float8_product_accum' }, + { oid => '2115', descr => 'maximum value of all bigint input values', proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'int8', proargtypes => 'int8', prosrc => 'aggregate_dummy' }, diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 89e051ee824..da1a742bd96 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -3983,3 +3983,529 @@ drop table agg_hash_1; drop table agg_hash_2; drop table agg_hash_3; drop table agg_hash_4; +-- Error context is inconsistent. Suppress it. +\set VERBOSITY terse +-- PRODUCT(numeric) +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_numeric (a int, val numeric); +SELECT product(val) FROM product_numeric; -- over empty table + product +--------- + +(1 row) + +INSERT INTO product_numeric SELECT i, i FROM generate_series(1, 10) i; +SELECT product(val) FROM product_numeric GROUP BY a%2 ORDER BY 1; + product +--------- + 945 + 3840 +(2 rows) + +SELECT product(val) FROM product_numeric; + product +--------- + 3628800 +(1 row) + +-- Test with NULL values +CREATE TABLE product_numeric_nulls (val numeric); +INSERT INTO product_numeric_nulls VALUES (NULL), (NULL); +SELECT product(val) FROM product_numeric_nulls; -- over all NULLs + product +--------- + +(1 row) + +-- Test with mixed values, NULL + values +INSERT INTO product_numeric_nulls VALUES (4), (2), (2), (4); +SELECT product(val) FROM product_numeric_nulls GROUP BY val%2 ORDER BY 1 NULLS LAST; + product +--------- + 64 + +(2 rows) + +SELECT product(val) FILTER (WHERE val > 2) FROM product_numeric_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 16 + + +(3 rows) + +SELECT product(DISTINCT val) FROM product_numeric_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2 + 4 + +(3 rows) + +SELECT product(val) FROM product_numeric_nulls; + product +--------- + 64 +(1 row) + +-- Test with zero and negative values. A zero makes the product zero, and the +-- sign follows the parity of the negative inputs. Exercise the numeric, +-- integer and float transition functions. +CREATE TABLE product_signs (a int, val numeric); +INSERT INTO product_signs VALUES (1, -2), (1, 3), (1, -4), (2, 5), (2, -6), (3, 0), (3, 7); +SELECT product(val) FROM product_signs; -- a zero forces 0 + product +--------- + 0 +(1 row) + +SELECT product(val) FROM product_signs WHERE val <> 0; -- three negatives -> -5040 + product +--------- + -5040 +(1 row) + +SELECT a, product(val) FROM product_signs GROUP BY a ORDER BY a; + a | product +---+--------- + 1 | 24 + 2 | -30 + 3 | 0 +(3 rows) + +SELECT product(val::int) FROM product_signs; -- 0 + product +--------- + 0 +(1 row) + +SELECT product(val::int) FROM product_signs WHERE val <> 0; + product +--------- + -5040 +(1 row) + +SELECT product(val::float8) FROM product_signs; -- 0 + product +--------- + 0 +(1 row) + +-- Test parallel aggregation with PARTIAL product(). Force a parallel plan on +-- a small table and pin the worker count so the plan does not depend on the +-- table or block size. +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_numeric_parallel (a int, val numeric); +INSERT INTO product_numeric_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_numeric_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_numeric_parallel GROUP BY a%2 ORDER BY 1; + QUERY PLAN +------------------------------------------------------------------------------ + Sort + Output: (product(val)), ((a % 2)) + Sort Key: (product(product_numeric_parallel.val)) + -> Finalize HashAggregate + Output: product(val), ((a % 2)) + Group Key: ((product_numeric_parallel.a % 2)) + -> Gather + Output: ((a % 2)), (PARTIAL product(val)) + Workers Planned: 2 + -> Partial HashAggregate + Output: ((a % 2)), PARTIAL product(val) + Group Key: (product_numeric_parallel.a % 2) + -> Parallel Seq Scan on public.product_numeric_parallel + Output: (a % 2), val +(14 rows) + +-- The parallel-combined result must match the serial product (8 for the even group) +SELECT product(val) FROM product_numeric_parallel GROUP BY a%2 ORDER BY 1; + product +--------- + 1 + 8 +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- A numeric product can overflow; verify the error is reported +CREATE TABLE product_numeric_overflow (val numeric); +INSERT INTO product_numeric_overflow VALUES ('1e100000'), ('1e100000'); +SELECT product(val) FROM product_numeric_overflow; -- overflow error +ERROR: value overflows numeric format +-- Test with special NUMERIC values +CREATE TABLE product_numeric_special (val numeric); +INSERT INTO product_numeric_special VALUES ('Infinity'), ('NAN'); +SELECT product(val) FROM product_numeric_special GROUP BY val ORDER BY 1; + product +---------- + Infinity + NaN +(2 rows) + +SELECT product(val) FROM product_numeric_special GROUP BY val%2 ORDER BY 1; + product +--------- + NaN +(1 row) + +-- PRODUCT(smallint), PRODUCT(int4), and PRODUCT(bigint) +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_integers (a int, val int); +SELECT product(val::smallint) FROM product_integers; -- over empty table + product +--------- + +(1 row) + +SELECT product(val::int) FROM product_integers; -- over empty table + product +--------- + +(1 row) + +SELECT product(val::bigint) FROM product_integers; -- over empty table + product +--------- + +(1 row) + +INSERT INTO product_integers SELECT i, i FROM generate_series(1, 10) i; +SELECT product(val::smallint) FROM product_integers GROUP BY a%2 ORDER BY 1; + product +--------- + 945 + 3840 +(2 rows) + +SELECT product(val::int) FROM product_integers GROUP BY a%2 ORDER BY 1; + product +--------- + 945 + 3840 +(2 rows) + +SELECT product(val::bigint) FROM product_integers GROUP BY a%2 ORDER BY 1; + product +--------- + 945 + 3840 +(2 rows) + +SELECT product(val::smallint) FROM product_integers; + product +--------- + 3628800 +(1 row) + +SELECT product(val::int) FROM product_integers; + product +--------- + 3628800 +(1 row) + +SELECT product(val::bigint) FROM product_integers; + product +--------- + 3628800 +(1 row) + +-- Test with NULL values +CREATE TABLE product_integers_nulls (val int); +INSERT INTO product_integers_nulls VALUES (NULL), (NULL); +SELECT product(val::smallint) FROM product_integers_nulls; -- over all NULLs + product +--------- + +(1 row) + +SELECT product(val::int) FROM product_integers_nulls; -- over all NULLs + product +--------- + +(1 row) + +SELECT product(val::bigint) FROM product_integers_nulls; -- over all NULLs + product +--------- + +(1 row) + +-- Test with mixed values, NULL + values +INSERT INTO product_integers_nulls VALUES (4), (2), (2), (4); +SELECT product(val::smallint) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 16 + + +(3 rows) + +SELECT product(DISTINCT val::smallint) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2 + 4 + +(3 rows) + +SELECT product(val::smallint) FROM product_integers_nulls; + product +--------- + 64 +(1 row) + +SELECT product(val::int) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 16 + + +(3 rows) + +SELECT product(DISTINCT val::int) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2 + 4 + +(3 rows) + +SELECT product(val::int) FROM product_integers_nulls; + product +--------- + 64 +(1 row) + +SELECT product(val::bigint) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 16 + + +(3 rows) + +SELECT product(DISTINCT val::bigint) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2 + 4 + +(3 rows) + +SELECT product(val::bigint) FROM product_integers_nulls; + product +--------- + 64 +(1 row) + +-- Test parallel aggregation with PARTIAL product() +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_integers_parallel (a int, val int); +INSERT INTO product_integers_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_integers_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_integers_parallel GROUP BY a%2 ORDER BY 1; + QUERY PLAN +------------------------------------------------------------------------------- + Sort + Output: (product(val)), ((a % 2)) + Sort Key: (product(product_integers_parallel.val)) + -> Finalize HashAggregate + Output: product(val), ((a % 2)) + Group Key: ((product_integers_parallel.a % 2)) + -> Gather + Output: ((a % 2)), (PARTIAL product(val)) + Workers Planned: 2 + -> Partial HashAggregate + Output: ((a % 2)), PARTIAL product(val) + Group Key: (product_integers_parallel.a % 2) + -> Parallel Seq Scan on public.product_integers_parallel + Output: (a % 2), val +(14 rows) + +SELECT product(val) FROM product_integers_parallel GROUP BY a%2 ORDER BY 1; + product +--------- + 1 + 8 +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- PRODUCT(float4) and PRODUCT(float8) +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_floats (a int, val float4); +SELECT product(val::float4) FROM product_floats; -- over empty table + product +--------- + +(1 row) + +SELECT product(val::float8) FROM product_floats; -- over empty table + product +--------- + +(1 row) + +INSERT INTO product_floats SELECT i, i + 0.1 FROM generate_series(1, 10) i; +SELECT product(val::float4) FROM product_floats GROUP BY a%2 ORDER BY 1; + product +------------ + 1123.63251 + 4296.74301 +(2 rows) + +SELECT product(val::float8)::numeric(20,5) FROM product_floats GROUP BY a%2 ORDER BY 1; + product +------------ + 1123.63251 + 4296.74301 +(2 rows) + +SELECT product(val::float4) FROM product_floats; + product +-------------------- + 4827960.1331512551 +(1 row) + +SELECT product(val::float8)::numeric(20,5) FROM product_floats; + product +--------------- + 4827960.13921 +(1 row) + +-- Test with NULL values +CREATE TABLE product_floats_nulls (val float4); +INSERT INTO product_floats_nulls VALUES (NULL), (NULL); +SELECT product(val::float4) FROM product_floats_nulls; -- over all NULLs + product +--------- + +(1 row) + +SELECT product(val::float8) FROM product_floats_nulls; -- over all NULLs + product +--------- + +(1 row) + +-- Test with mixed values, NULL + values +INSERT INTO product_floats_nulls VALUES (4.5), (2.4), (2.4), (4.5); +SELECT product(val::float4) FILTER (WHERE val > 2) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 5.76 + 20.25 + +(3 rows) + +SELECT product(DISTINCT val::float4) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2.4 + 4.5 + +(3 rows) + +SELECT product(val::float4) FROM product_floats_nulls; + product +---------- + 116.6400 +(1 row) + +SELECT (product(val::float8) FILTER (WHERE val > 2))::numeric(10,3) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 5.760 + 20.250 + +(3 rows) + +SELECT product(DISTINCT val::float8)::numeric(10,3) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +--------- + 2.400 + 4.500 + +(3 rows) + +SELECT product(val::float8)::numeric(10,3) FROM product_floats_nulls; + product +--------- + 116.640 +(1 row) + +-- Test parallel aggregation with PARTIAL product() +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_floats_parallel (a int, val float4); +INSERT INTO product_floats_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_floats_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_floats_parallel GROUP BY a%2 ORDER BY 1; + QUERY PLAN +----------------------------------------------------------------------------- + Sort + Output: (product(val)), ((a % 2)) + Sort Key: (product(product_floats_parallel.val)) + -> Finalize HashAggregate + Output: product(val), ((a % 2)) + Group Key: ((product_floats_parallel.a % 2)) + -> Gather + Output: ((a % 2)), (PARTIAL product(val)) + Workers Planned: 2 + -> Partial HashAggregate + Output: ((a % 2)), PARTIAL product(val) + Group Key: (product_floats_parallel.a % 2) + -> Parallel Seq Scan on public.product_floats_parallel + Output: (a % 2), val +(14 rows) + +SELECT product(val) FROM product_floats_parallel GROUP BY a%2 ORDER BY 1; + product +--------- + 1 + 8 +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- Test with special FLOAT values +CREATE TABLE product_floats_special (val float); +INSERT INTO product_floats_special VALUES ('Infinity'), ('NAN'); +SELECT product(val) FROM product_floats_special GROUP BY val ORDER BY 1; + product +---------- + Infinity + NaN +(2 rows) + +DROP TABLE product_numeric; +DROP TABLE product_numeric_nulls; +DROP TABLE product_signs; +DROP TABLE product_numeric_parallel; +DROP TABLE product_numeric_overflow; +DROP TABLE product_numeric_special; +DROP TABLE product_integers; +DROP TABLE product_integers_nulls; +DROP TABLE product_integers_parallel; +DROP TABLE product_floats; +DROP TABLE product_floats_nulls; +DROP TABLE product_floats_parallel; +DROP TABLE product_floats_special; +\set VERBOSITY default diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index 59c3df8cf0a..525cff2a3d2 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -33,6 +33,30 @@ SELECT depname, empno, salary, sum(salary) OVER (PARTITION BY depname) FROM emps sales | 1 | 5000 | 14600 (10 rows) +-- product() as a window function, over the whole partition, for every +-- supported input type +SELECT depname, empno, salary, + product(salary::smallint) OVER (PARTITION BY depname) product_smallint, + product(salary::int) OVER (PARTITION BY depname) product_int, + product(salary::bigint) OVER (PARTITION BY depname) product_bigint, + product(salary::float4) OVER (PARTITION BY depname) product_float4, + product(salary::float8) OVER (PARTITION BY depname) product_float8, + product(salary::numeric) OVER (PARTITION BY depname) product_numeric + FROM empsalary ORDER BY depname, salary; + depname | empno | salary | product_smallint | product_int | product_bigint | product_float4 | product_float8 | product_numeric +-----------+-------+--------+---------------------+---------------------+---------------------+---------------------+---------------------+--------------------- + develop | 7 | 4200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 9 | 4500 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 11 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 10 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 8 | 6000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + personnel | 5 | 3500 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 + personnel | 2 | 3900 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 + sales | 3 | 4800 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 + sales | 4 | 4800 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 + sales | 1 | 5000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 +(10 rows) + SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary) FROM empsalary; depname | empno | salary | rank -----------+-------+--------+------ @@ -90,6 +114,49 @@ SELECT depname, empno, salary, sum(salary) OVER w FROM empsalary WINDOW w AS (PA sales | 4 | 4800 | 14600 (10 rows) +-- product() as a window function using a named WINDOW clause +SELECT depname, empno, salary, + product(salary::smallint) OVER w product_smallint, + product(salary::int) OVER w product_int, + product(salary::bigint) OVER w product_bigint, + product(salary::float4) OVER w product_float4, + product(salary::float8) OVER w product_float8, + product(salary::numeric) OVER w product_numeric + FROM empsalary WINDOW w AS (PARTITION BY depname); + depname | empno | salary | product_smallint | product_int | product_bigint | product_float4 | product_float8 | product_numeric +-----------+-------+--------+---------------------+---------------------+---------------------+---------------------+---------------------+--------------------- + develop | 11 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 7 | 4200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 9 | 4500 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 8 | 6000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + develop | 10 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 + personnel | 5 | 3500 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 + personnel | 2 | 3900 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 | 13650000 + sales | 3 | 4800 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 + sales | 1 | 5000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 + sales | 4 | 4800 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 | 115200000000 +(10 rows) + +-- product() has no inverse transition function, so a moving frame must +-- recompute the aggregate for every row. Exercise that path. +SELECT empno, salary, + product(salary::numeric) OVER (ORDER BY empno + ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS product_moving + FROM empsalary ORDER BY empno; + empno | salary | product_moving +-------+--------+---------------- + 1 | 5000 | 5000 + 2 | 3900 | 19500000 + 3 | 4800 | 18720000 + 4 | 4800 | 23040000 + 5 | 3500 | 16800000 + 7 | 4200 | 14700000 + 8 | 6000 | 25200000 + 9 | 4500 | 27000000 + 10 | 5200 | 23400000 + 11 | 5200 | 27040000 +(10 rows) + SELECT depname, empno, salary, rank() OVER w FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary) ORDER BY rank() OVER w; depname | empno | salary | rank -----------+-------+--------+------ diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 916383db927..75c61998757 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -1798,3 +1798,186 @@ drop table agg_hash_1; drop table agg_hash_2; drop table agg_hash_3; drop table agg_hash_4; + +-- Error context is inconsistent. Suppress it. +\set VERBOSITY terse + +-- PRODUCT(numeric) + +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_numeric (a int, val numeric); +SELECT product(val) FROM product_numeric; -- over empty table +INSERT INTO product_numeric SELECT i, i FROM generate_series(1, 10) i; +SELECT product(val) FROM product_numeric GROUP BY a%2 ORDER BY 1; +SELECT product(val) FROM product_numeric; + +-- Test with NULL values +CREATE TABLE product_numeric_nulls (val numeric); +INSERT INTO product_numeric_nulls VALUES (NULL), (NULL); +SELECT product(val) FROM product_numeric_nulls; -- over all NULLs + +-- Test with mixed values, NULL + values +INSERT INTO product_numeric_nulls VALUES (4), (2), (2), (4); +SELECT product(val) FROM product_numeric_nulls GROUP BY val%2 ORDER BY 1 NULLS LAST; +SELECT product(val) FILTER (WHERE val > 2) FROM product_numeric_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val) FROM product_numeric_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val) FROM product_numeric_nulls; + +-- Test with zero and negative values. A zero makes the product zero, and the +-- sign follows the parity of the negative inputs. Exercise the numeric, +-- integer and float transition functions. +CREATE TABLE product_signs (a int, val numeric); +INSERT INTO product_signs VALUES (1, -2), (1, 3), (1, -4), (2, 5), (2, -6), (3, 0), (3, 7); +SELECT product(val) FROM product_signs; -- a zero forces 0 +SELECT product(val) FROM product_signs WHERE val <> 0; -- three negatives -> -5040 +SELECT a, product(val) FROM product_signs GROUP BY a ORDER BY a; +SELECT product(val::int) FROM product_signs; -- 0 +SELECT product(val::int) FROM product_signs WHERE val <> 0; +SELECT product(val::float8) FROM product_signs; -- 0 + +-- Test parallel aggregation with PARTIAL product(). Force a parallel plan on +-- a small table and pin the worker count so the plan does not depend on the +-- table or block size. +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_numeric_parallel (a int, val numeric); +INSERT INTO product_numeric_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_numeric_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_numeric_parallel GROUP BY a%2 ORDER BY 1; +-- The parallel-combined result must match the serial product (8 for the even group) +SELECT product(val) FROM product_numeric_parallel GROUP BY a%2 ORDER BY 1; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- A numeric product can overflow; verify the error is reported +CREATE TABLE product_numeric_overflow (val numeric); +INSERT INTO product_numeric_overflow VALUES ('1e100000'), ('1e100000'); +SELECT product(val) FROM product_numeric_overflow; -- overflow error + +-- Test with special NUMERIC values +CREATE TABLE product_numeric_special (val numeric); +INSERT INTO product_numeric_special VALUES ('Infinity'), ('NAN'); +SELECT product(val) FROM product_numeric_special GROUP BY val ORDER BY 1; +SELECT product(val) FROM product_numeric_special GROUP BY val%2 ORDER BY 1; + + +-- PRODUCT(smallint), PRODUCT(int4), and PRODUCT(bigint) + +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_integers (a int, val int); +SELECT product(val::smallint) FROM product_integers; -- over empty table +SELECT product(val::int) FROM product_integers; -- over empty table +SELECT product(val::bigint) FROM product_integers; -- over empty table +INSERT INTO product_integers SELECT i, i FROM generate_series(1, 10) i; +SELECT product(val::smallint) FROM product_integers GROUP BY a%2 ORDER BY 1; +SELECT product(val::int) FROM product_integers GROUP BY a%2 ORDER BY 1; +SELECT product(val::bigint) FROM product_integers GROUP BY a%2 ORDER BY 1; +SELECT product(val::smallint) FROM product_integers; +SELECT product(val::int) FROM product_integers; +SELECT product(val::bigint) FROM product_integers; + +-- Test with NULL values +CREATE TABLE product_integers_nulls (val int); +INSERT INTO product_integers_nulls VALUES (NULL), (NULL); +SELECT product(val::smallint) FROM product_integers_nulls; -- over all NULLs +SELECT product(val::int) FROM product_integers_nulls; -- over all NULLs +SELECT product(val::bigint) FROM product_integers_nulls; -- over all NULLs + +-- Test with mixed values, NULL + values +INSERT INTO product_integers_nulls VALUES (4), (2), (2), (4); +SELECT product(val::smallint) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val::smallint) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val::smallint) FROM product_integers_nulls; +SELECT product(val::int) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val::int) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val::int) FROM product_integers_nulls; +SELECT product(val::bigint) FILTER (WHERE val > 2) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val::bigint) FROM product_integers_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val::bigint) FROM product_integers_nulls; + +-- Test parallel aggregation with PARTIAL product() +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_integers_parallel (a int, val int); +INSERT INTO product_integers_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_integers_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_integers_parallel GROUP BY a%2 ORDER BY 1; +SELECT product(val) FROM product_integers_parallel GROUP BY a%2 ORDER BY 1; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + + +-- PRODUCT(float4) and PRODUCT(float8) + +-- Test with regular GROUP BY and grouping over all rows +CREATE TABLE product_floats (a int, val float4); +SELECT product(val::float4) FROM product_floats; -- over empty table +SELECT product(val::float8) FROM product_floats; -- over empty table +INSERT INTO product_floats SELECT i, i + 0.1 FROM generate_series(1, 10) i; +SELECT product(val::float4) FROM product_floats GROUP BY a%2 ORDER BY 1; +SELECT product(val::float8)::numeric(20,5) FROM product_floats GROUP BY a%2 ORDER BY 1; +SELECT product(val::float4) FROM product_floats; +SELECT product(val::float8)::numeric(20,5) FROM product_floats; + +-- Test with NULL values +CREATE TABLE product_floats_nulls (val float4); +INSERT INTO product_floats_nulls VALUES (NULL), (NULL); +SELECT product(val::float4) FROM product_floats_nulls; -- over all NULLs +SELECT product(val::float8) FROM product_floats_nulls; -- over all NULLs + +-- Test with mixed values, NULL + values +INSERT INTO product_floats_nulls VALUES (4.5), (2.4), (2.4), (4.5); +SELECT product(val::float4) FILTER (WHERE val > 2) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val::float4) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val::float4) FROM product_floats_nulls; +SELECT (product(val::float8) FILTER (WHERE val > 2))::numeric(10,3) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(DISTINCT val::float8)::numeric(10,3) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; +SELECT product(val::float8)::numeric(10,3) FROM product_floats_nulls; + +-- Test parallel aggregation with PARTIAL product() +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +CREATE TABLE product_floats_parallel (a int, val float4); +INSERT INTO product_floats_parallel + SELECT i, CASE WHEN i % 1000 = 0 THEN 2 ELSE 1 END FROM generate_series(1, 3000) i; +ALTER TABLE product_floats_parallel SET (parallel_workers = 2); +EXPLAIN (VERBOSE, COSTS OFF) SELECT product(val) FROM product_floats_parallel GROUP BY a%2 ORDER BY 1; +SELECT product(val) FROM product_floats_parallel GROUP BY a%2 ORDER BY 1; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- Test with special FLOAT values +CREATE TABLE product_floats_special (val float); +INSERT INTO product_floats_special VALUES ('Infinity'), ('NAN'); +SELECT product(val) FROM product_floats_special GROUP BY val ORDER BY 1; + +DROP TABLE product_numeric; +DROP TABLE product_numeric_nulls; +DROP TABLE product_signs; +DROP TABLE product_numeric_parallel; +DROP TABLE product_numeric_overflow; +DROP TABLE product_numeric_special; +DROP TABLE product_integers; +DROP TABLE product_integers_nulls; +DROP TABLE product_integers_parallel; +DROP TABLE product_floats; +DROP TABLE product_floats_nulls; +DROP TABLE product_floats_parallel; +DROP TABLE product_floats_special; + + +\set VERBOSITY default diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 17261135dc3..9755dd7d3de 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -23,6 +23,17 @@ INSERT INTO empsalary VALUES SELECT depname, empno, salary, sum(salary) OVER (PARTITION BY depname) FROM empsalary ORDER BY depname, salary; +-- product() as a window function, over the whole partition, for every +-- supported input type +SELECT depname, empno, salary, + product(salary::smallint) OVER (PARTITION BY depname) product_smallint, + product(salary::int) OVER (PARTITION BY depname) product_int, + product(salary::bigint) OVER (PARTITION BY depname) product_bigint, + product(salary::float4) OVER (PARTITION BY depname) product_float4, + product(salary::float8) OVER (PARTITION BY depname) product_float8, + product(salary::numeric) OVER (PARTITION BY depname) product_numeric + FROM empsalary ORDER BY depname, salary; + SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary) FROM empsalary; -- with GROUP BY @@ -31,6 +42,23 @@ GROUP BY four, ten ORDER BY four, ten; SELECT depname, empno, salary, sum(salary) OVER w FROM empsalary WINDOW w AS (PARTITION BY depname); +-- product() as a window function using a named WINDOW clause +SELECT depname, empno, salary, + product(salary::smallint) OVER w product_smallint, + product(salary::int) OVER w product_int, + product(salary::bigint) OVER w product_bigint, + product(salary::float4) OVER w product_float4, + product(salary::float8) OVER w product_float8, + product(salary::numeric) OVER w product_numeric + FROM empsalary WINDOW w AS (PARTITION BY depname); + +-- product() has no inverse transition function, so a moving frame must +-- recompute the aggregate for every row. Exercise that path. +SELECT empno, salary, + product(salary::numeric) OVER (ORDER BY empno + ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS product_moving + FROM empsalary ORDER BY empno; + SELECT depname, empno, salary, rank() OVER w FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary) ORDER BY rank() OVER w; -- empty window specification -- 2.43.0