From b63a76eca8685cc8b46c089c7140ca12d2b8ec12 Mon Sep 17 00:00:00 2001 From: Jeevan Chalke Date: Thu, 24 Sep 2026 13:04:11 +0530 Subject: [PATCH v3] 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. For int2, int4, int8, and numeric input, the internal transition state uses the numeric type to prevent overflow during multiplication. Dedicated accumulator functions (*_product_accum) promote the integer inputs to numeric before invoking numeric_mul for the transition steps. For float4 and float8 input, the transition state is float8 (native, not numeric). product(float8) reuses the existing float8mul() directly as both the transition and combine function; product(float4) promotes its input to float8 via a small accumulator function that also delegates the actual multiplication to float8_mul(). 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. New catalog entries require a CATALOG_VERSION_NO bump. Omitted here to avoid conflicts with concurrent commits; committer should bump at commit time. Author: Jeevan Chalke Reviewed-by: Dean Rasheed Reviewed-by: Jim Jones Reviewed-by: Vaibhav Dalvi Proposed-by: Peter Eisentraut Discussion: https://postgr.es/m/CAM2%2B6%3DWG8gpOti%2B1-N_ra0mNcNrdhMjSJcDvcA7wzjLD-qKH3g%40mail.gmail.com --- doc/src/sgml/func/func-aggregate.sgml | 51 ++ src/backend/utils/adt/numeric.c | 110 +++++ src/include/catalog/pg_aggregate.dat | 14 + src/include/catalog/pg_proc.dat | 31 ++ src/test/regress/expected/aggregates.out | 578 +++++++++++++++++++++++ src/test/regress/expected/window.out | 67 +++ src/test/regress/sql/aggregates.sql | 204 ++++++++ src/test/regress/sql/window.sql | 28 ++ 8 files changed, 1083 insertions(+) diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml index 6a6a4d7e32e..bdb07a78ddd 100644 --- a/doc/src/sgml/func/func-aggregate.sgml +++ b/doc/src/sgml/func/func-aggregate.sgml @@ -536,6 +536,40 @@ Yes + + + + product + + product ( smallint ) + numeric + + + product ( integer ) + numeric + + + product ( bigint ) + numeric + + + product ( numeric ) + numeric + + + product ( real ) + double precision + + + product ( double precision ) + double precision + + + Computes the product of the non-null input values. + + Yes + + @@ -669,6 +703,23 @@ substitute zero or an empty array for null when necessary. + + For smallint, integer, bigint, + and numeric input, the product + aggregate returns numeric 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. For real + and double precision input, product + returns double precision and accumulates natively in + that type, so, like sum, a product of finite + values that would exceed the range of double precision + raises an error rather than silently becoming + Infinity; Infinity is only + produced (and propagated) when it is already present among the + input values. + + 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 8b2575b63e3..7ba4f7adad5 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -12195,3 +12195,113 @@ 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 + * + * Unlike the integer variants, the running product here is kept natively + * as float8 (the transition type), not promoted to Numeric. The actual + * multiplication is delegated to float8_mul(), which raises an overflow + * error for a finite*finite multiplication that would yield Infinity + * (matching sum(float4)/sum(float8), and product(float8), which for the + * same reason uses float8mul() directly rather than the bare "*" + * operator). Infinity is only produced when it is already present in + * the input. + */ +Datum +float4_product_accum(PG_FUNCTION_ARGS) +{ + float8 newval; + + /* 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_FLOAT8(PG_GETARG_FLOAT8(0)); + + newval = (float8) PG_GETARG_FLOAT4(1); + + if (PG_ARGISNULL(0)) + PG_RETURN_FLOAT8(newval); + + PG_RETURN_FLOAT8(float8_mul(PG_GETARG_FLOAT8(0), newval)); +} diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat index 7bce36ac9c4..adad2d7a5b2 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 => 'float8mul', aggtranstype => 'float8' }, +{ aggfnoid => 'product(float8)', aggtransfn => 'float8mul', + aggcombinefn => 'float8mul', aggtranstype => 'float8' }, +{ 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 f46427258e3..83d4699817a 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7105,6 +7105,37 @@ 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 double precision across all float4 input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'float8', + proargtypes => 'float4', prosrc => 'aggregate_dummy' }, +{ oid => '4555', descr => 'product as double precision across all float8 input values', + proname => 'product', prokind => 'a', proisstrict => 'f', prorettype => 'float8', + 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 => 'float8', + proargtypes => 'float8 float4', prosrc => 'float4_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 7d07619956f..f60a96ff0da 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -3958,3 +3958,581 @@ 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 (signed zero, float8 semantics) + 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) + +-- Special values mixed with ordinary positive values in the same set: a NaN +-- input dominates and yields NaN, while Infinity propagates (with sign). +SELECT product(v) FROM (VALUES ('NaN'::numeric), ('Infinity'), (3.14)) j(v); + product +--------- + NaN +(1 row) + +SELECT product(v) FROM (VALUES ('Infinity'::numeric), (3.14), (2)) j(v); + product +---------- + Infinity +(1 row) + +SELECT product(v) FROM (VALUES ('-Infinity'::numeric), (3.14), (2)) j(v); + product +----------- + -Infinity +(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.63251078536 + 4296.7430123928 +(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.13921438 +(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.76000045776368 + 20.25 + +(3 rows) + +SELECT product(DISTINCT val::float4) FROM product_floats_nulls GROUP BY val ORDER BY 1 NULLS LAST; + product +------------------ + 2.40000009536743 + 4.5 + +(3 rows) + +SELECT product(val::float4) FROM product_floats_nulls; + product +------------------ + 116.640009269715 +(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; +-- Unlike numeric, product() over float4/float8 multiplies using the same +-- overflow-checked primitive sum() uses, so a product of finite values +-- that would exceed the range of double precision raises an error rather +-- than silently becoming Infinity. (Infinity is only produced when it is +-- already present in the input; see the special-value tests below.) +CREATE TABLE product_floats_overflow (val float4); +INSERT INTO product_floats_overflow SELECT '3.4e38' FROM generate_series(1, 9); +SELECT product(val) FROM product_floats_overflow; -- overflow error +ERROR: value out of range: overflow +SELECT product(val::float8) FROM product_floats_overflow; -- overflow error +ERROR: value out of range: overflow +-- 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) + +-- Special values mixed with ordinary positive values in the same set: a NaN +-- input dominates and yields NaN, while Infinity propagates (with sign). +SELECT product(v) FROM (VALUES ('NaN'::float), ('Infinity'), (3.14)) j(v); + product +--------- + NaN +(1 row) + +SELECT product(v) FROM (VALUES ('Infinity'::float4), (3.14), (2)) j(v); + product +---------- + Infinity +(1 row) + +SELECT product(v) FROM (VALUES ('-Infinity'::float8), (3.14), (2)) j(v); + product +----------- + -Infinity +(1 row) + +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_overflow; +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 c0bde1c5eec..e70bfca57b4 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 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 9 | 4500 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 11 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 10 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 8 | 6000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 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 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 7 | 4200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 9 | 4500 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 8 | 6000 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 3066336000000000000 + develop | 10 | 5200 | 3066336000000000000 | 3066336000000000000 | 3066336000000000000 | 3.066336e+18 | 3.066336e+18 | 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 91f8342166f..4b6c484436d 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -1786,3 +1786,207 @@ 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 (signed zero, float8 semantics) + +-- 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; +-- Special values mixed with ordinary positive values in the same set: a NaN +-- input dominates and yields NaN, while Infinity propagates (with sign). +SELECT product(v) FROM (VALUES ('NaN'::numeric), ('Infinity'), (3.14)) j(v); +SELECT product(v) FROM (VALUES ('Infinity'::numeric), (3.14), (2)) j(v); +SELECT product(v) FROM (VALUES ('-Infinity'::numeric), (3.14), (2)) j(v); + + +-- 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; + +-- Unlike numeric, product() over float4/float8 multiplies using the same +-- overflow-checked primitive sum() uses, so a product of finite values +-- that would exceed the range of double precision raises an error rather +-- than silently becoming Infinity. (Infinity is only produced when it is +-- already present in the input; see the special-value tests below.) +CREATE TABLE product_floats_overflow (val float4); +INSERT INTO product_floats_overflow SELECT '3.4e38' FROM generate_series(1, 9); +SELECT product(val) FROM product_floats_overflow; -- overflow error +SELECT product(val::float8) FROM product_floats_overflow; -- overflow error + +-- 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; +-- Special values mixed with ordinary positive values in the same set: a NaN +-- input dominates and yields NaN, while Infinity propagates (with sign). +SELECT product(v) FROM (VALUES ('NaN'::float), ('Infinity'), (3.14)) j(v); +SELECT product(v) FROM (VALUES ('Infinity'::float4), (3.14), (2)) j(v); +SELECT product(v) FROM (VALUES ('-Infinity'::float8), (3.14), (2)) j(v); + +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_overflow; +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 8e6f92d94c7..39624ea2166 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