I'm new to writing triggers and functions in Postgres.
I have written a function that changes prices to .99 or .00 whenever a price is put into the database.
CREATE OR REPLACE FUNCTION public.cents(price numeric)
RETURNS numeric
LANGUAGE plpgsql
LEAKPROOF
AS $function$
DECLARE
dollars text;
cents text;
new_price numeric;
BEGIN
dollars := split_part(price::text, '.', 1);
cents := split_part(price::text, '.', 2);
IF cents != '00' THEN cents := '99';
ENDIF;
new_price := dollars || '.' || cents;
RETURN new_price;
END;
$function$
I've read the doc on triggers and these examples seem more complex.
I'm not sure I understand how to create a trigger that will run this function specifically whenever a record in the price column is updated.
Does this look correct? The price column isn't mentioned in the trigger..
CREATE OR REPLACE FUNCTION public.cents()
RETURNS trigger
LANGUAGE plpgsql
LEAKPROOF
AS $tr_cents$
DECLARE
dollars text;
cents text;
new_price numeric;
BEGIN
dollars := split_part(OLD::text, '.', 1);
cents := split_part(OLD::text, '.', 2);
IF cents != '00' THEN cents := '99';
ENDIF;
new_price := dollars || cents;
RETURN new_price;
END;
$tr_cents$;
CREATE TRIGGER tr_cents BEFORE INSERT OR UPDATE ON microwaves
FOR EACH ROW EXECUTE PROCEDURE cents();
The examples in the docs also use RETURN NEW but I'm not exactly sure how that would work with my code, or if it's necessary?
Assuming missing information:
price is defined numeric NOT NULL.
There is a CHECK constraint enforcing positive prices.
Postgres 9.5. (Solution should work for Postgres 9.0+.)
I read your objective like this:
Leave numbers without (significant) fractional digits (.00) and change all others to .99.
See below about "without (significant) fractional digits" or .00 ...
If that's all the trigger does, the most efficient way is to place the condition in a WHEN clause to the trigger itself. The manual:
In a BEFORE trigger, the WHEN condition is evaluated just before the
function is or would be executed, so using WHEN is not materially
different from testing the same condition at the beginning of the trigger function.
(There is more, read the manual.)
This way, the trigger function is not even called if not needed. The logic can be radically simplified:
CREATE OR REPLACE FUNCTION tr_cents()
RETURNS trigger AS
$tr_cents$
BEGIN
-- only called WHEN (NEW.price % 1 IN (.00, .99)
NEW.price := trunc(NEW.price) + .99;
RETURN NEW;
END
$tr_cents$ LANGUAGE plpgsql LEAKPROOF;
CREATE TRIGGER microwaves_cents
BEFORE INSERT OR UPDATE ON microwaves
FOR EACH ROW
WHEN ((NEW.price % 1) <> ALL ('{.00,.99}'::numeric[]))
EXECUTE PROCEDURE tr_cents();
Note that the trigger kicks in for INSERT and UPDATE with illegal price values. Not just
whenever a record in the price column is updated.
You need RETURN NEW; at the end of the trigger function or the operation on the row will be cancelled. The manual:
A trigger function must return either NULL or a record/row value having exactly the structure of the table the trigger was fired for.
You don't need the function public.cents() at all for this.
Test case
CREATE TABLE microwaves (m_id serial PRIMARY KEY, price numeric);
INSERT INTO microwaves (m_id, price) VALUES
(1, 0.00)
, (2, 0.01)
, (3, 0.02)
, (4, 0.99)
, (5, 1.00)
, (6, 1.01)
, (7, 1.02)
, (8, 1.99)
, (9, 12.34);
UPDATE microwaves SET price = 2.0 WHERE m_id = 7;
UPDATE microwaves SET price = 2.5 WHERE m_id = 8;
UPDATE microwaves SET price = 5.99 WHERE m_id = 9;
TABLE microwaves;
Result:
m_id | price
------+-------
1 | 0.00
2 | 0.99
3 | 0.99
4 | 0.99
5 | 1.00
6 | 1.99
7 | 2.0
8 | 2.99
9 | 5.99
Data type numeric and scale
.. and why your function public.cents(price numeric) is a trap.
Scale being the number of decimal fractional digits.
numeric is an arbitrary precision type. It preserves literal digits exactly as entered - unless you specify precision and scale for the type. Like: numeric(10,2). The manual:
Specifying:
NUMERIC
without any precision or scale creates a column in which numeric
values of any precision and scale can be stored, up to the
implementation limit on precision. A column of this kind (numeric
without precision and scale) will not coerce input values to any
particular scale, whereas numeric columns with a declared scale will
coerce input values to that scale.
Leading zeroes are never stored, but trailing zeroes in the fractional part are kept this way, even if insignificant. The manual can easily be misread in this respect, further down:
Numeric values are physically stored without any extra leading or trailing zeroes.
Note the word "extra". Meaning, Postgres will not add trailing zeros, but it will keep the ones you added - even if those are completely insignificant for the numeric value.
You need to be aware of this when converting numeric to text. A check for "00" in the fractional part will work for numeric with a configured scale like numeric (9,2). But it is unreliable for plain numeric like you use in your function. Consider:
SELECT (numeric(9,2) '1')::numeric AS num_cast_from_num_with_scale
, numeric '1.00' AS num_with_scale
, numeric '1' AS num_without_scale;
num_cast_from_num_with_scale | num_with_scale | num_without_scale
------------------------------+----------------+-------------------
1.00 | 1.00 | 1
This way, insignificant trailing zeros become significant. And I seriously doubt that's how it's supposed to be. The test IF cents != '00' ... in your function public.cents(price numeric) is a loaded footgun. It may work as expected while you pass values from a numeric(9,2) column, but "suddenly" break once you use values from other sources.
You described return value as numeric, but return a string by fact. Also several type conversions not a good point. There is more easy way. F. ex.
CREATE OR REPLACE FUNCTION cents(price numeric) RETURNS numeric AS
$BODY$
begin
IF price IS NOT NULL then
IF price % 1 != 0 then
price := floor(price) + 0.99;
end IF;
END IF;
RETURN price;
end;
$BODY$ LANGUAGE plpgsql;
To execute such update on any insert/update need:
CREATE TABLE test (
price numeric
);
CREATE FUNCTION price_update() RETURNS trigger AS $price_update$
BEGIN
IF NEW.price IS NOT NULL THEN
NEW.price = public.cents(NEW.price);
END IF;
RETURN NEW;
END;
$price_update$ LANGUAGE plpgsql;
CREATE TRIGGER on_price_update BEFORE INSERT OR UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE price_update();
Lets check:
=# insert into test (price) values (2), (1.1), (5);
INSERT 0 3
=# select * from test;
price
-------
2
1.99
5
(3 rows)
=# update test set price = 5.01 where price = 5;
UPDATE 1
=# select * from test;
price
-------
2
1.99
5.99
(3 rows)
=# update test set price = 3 where price = 1.99;
UPDATE 1
=# select * from test;
price
-------
2
5.99
3
(3 rows)
Related
I have a PostgreSQL's table with three columns: quantity, price and total.
Total saves a calculation: total = quantity * price.
I would like to saves that calculation as a default value for total column. Excel can do it (I know it is not the same), so I am thinking something similar.
Example:
quantity: 2
price: 5
total: 10 (quantity x price)
You should make the total column NULL-able (allow NULL values). Then add a trigger for INSERT and UPDATE which will compute the NEW.total value if it is NULL.
However, I would advice against using computed columns in your database - it is always better to perform those calculations on the client-side.
CREATE FUNCTION tr_insert (
)
RETURNS trigger AS
$body$
BEGIN
IF NEW.total IS NULL THEN
NEW.total = NEW.quantity * NEW.price;
END IF;
RETURN NEW;
END;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER;
CREATE TRIGGER task_main_tr
BEFORE INSERT OR UPDATE
ON public.task_main
FOR EACH ROW
EXECUTE PROCEDURE tr_insert();
Which method does Postgres round(v numeric, s int) use?
Round half up
Round half down
Round half away from zero
Round half towards zero
Round half to even
Round half to odd
I'm looking for documentation reference.
It's not documented, so it can change.
Here are my round_half_even(numeric,integer):
create or replace function round_half_even(val numeric, prec integer)
returns numeric
as $$
declare
retval numeric;
difference numeric;
even boolean;
begin
retval := round(val,prec);
difference := retval-val;
if abs(difference)*(10::numeric^prec) = 0.5::numeric then
even := (retval * (10::numeric^prec)) % 2::numeric = 0::numeric;
if not even then
retval := round(val-difference,prec);
end if;
end if;
return retval;
end;
$$ language plpgsql immutable strict;
And round_half_odd(numeric,integer):
create or replace function round_half_odd(val numeric, prec integer)
returns numeric
as $$
declare
retval numeric;
difference numeric;
even boolean;
begin
retval := round(val,prec);
difference := retval-val;
if abs(difference)*(10::numeric^prec) = 0.5::numeric then
even := (retval * (10::numeric^prec)) % 2::numeric = 0::numeric;
if even then
retval := round(val-difference,prec);
end if;
end if;
return retval;
end;
$$ language plpgsql immutable strict;
They manage about 500000 invocations per second, 6 times slower than a standard round(numeric,integer). They also work for zero and for negative precision.
Sorry, I don't see any hint of this in the documentation, but a look at the code indicates it's using round half away from zero; the carry is always added to digits, thereby increasing the absolute value of the variable, regardless of what its sign is. A simple experiment (psql 9.1) confirms this:
test=# CREATE TABLE nvals (v numeric(5,2));
CREATE TABLE
test=# INSERT INTO nvals (v) VALUES (-0.25), (-0.15), (-0.05), (0.05), (0.15), (0.25);
INSERT 0 6
test=# SELECT v, round(v, 1) FROM nvals;
v | round
-------+-------
-0.25 | -0.3
-0.15 | -0.2
-0.05 | -0.1
0.05 | 0.1
0.15 | 0.2
0.25 | 0.3
(6 rows)
Interesting, because round(v dp) uses half even:
test=# create table vals (v double precision);
CREATE TABLE
test=# insert into vals (v) VALUES (-2.5), (-1.5), (-0.5), (0.5), (1.5), (2.5);
INSERT 0 6
test=# select v, round(v) from vals;
v | round
------+-------
-2.5 | -2
-1.5 | -2
-0.5 | -0
0.5 | 0
1.5 | 2
2.5 | 2
(6 rows)
The latter behavior is almost certainly platform-dependent, since it looks like it uses rint(3) under the hood.
You could always implement a different rounding scheme if necessary. See Tometzky's answer for examples.
I have table with real column type with example values:
123456,12
0,12345678
And code in stored procedure:
CREATE OR REPLACE FUNCTION test3()
RETURNS integer AS
$BODY$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT
gme.abs_km as km,
CAST(gme.abs_km as numeric) as cast,
round(gme.abs_km:: numeric(16,2), 2) as round
FROM gps_entry gme
LOOP
RAISE NOTICE 'Km: % , cast: % , round: %', rec.km, rec.cast, rec.round;
INSERT INTO test (km, casting, rounding) VALUES (rec.km, rec.cast, rec.round);
END LOOP;
RETURN 1;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
Here is output:
2014-02-05 12:49:53 CET NOTICE: Km: 0.12345678 , cast: 0.123457 , round: 0.12
2014-02-05 12:49:53 CET NOTICE: Km: 123456.12 , cast: 123456 , round: 123456.00
DB table with columns NUMERIC(19,2):
km casting rounding
0.12 0.12 0.12
123456.00 123456.00 123456.00
Why do cast and round functions not work for the value 123456.12?
real is a lossy, inexact floating-point type. It only uses 4 bytes for storage and may not store the presented numeric literals precisely to begin with. In addition, implementation details depend on your platform. Consider the chapter "Floating-Point Types" in the manual.
There is nothing wrong with either round() or cast(). For (more) exact results, use numeric.
Function audit
CREATE OR REPLACE FUNCTION test3()
RETURNS void
LANGUAGE plpgsql AS
$func$
DECLARE
r record;
BEGIN
FOR r IN
SELECT abs_km AS km
, cast(abs_km AS numeric) AS km_cast
, round(abs_km::numeric, 2) AS km_round
FROM gps_entry
LOOP
RAISE NOTICE 'km: % , km_cast: % , km_round: %'
, r.km, r.km_cast, r.km_round;
INSERT INTO test (km, casting, rounding)
VALUES (r.km, r.km_cast, r.km_round);
END LOOP;
END
$func$;
Of course, it would be more efficient to replace the loop with a single multi-row INSERT statement.
Do not quote the language name plpgsql. It's an identifier.
Makes no sense to round to 2 fractional digits after casting to numeric(16,2), which forcibly rounds already. Either / or ..
round(abs_km:: numeric(16,2), 2) as round
round(abs_km::numeric, 2) as round
abs_km::numeric(16,2) as round
I have a PostgreSQL table of this form:
base_id int | mods smallint[]
3 | {7,15,48}
I need to populate a table of this form:
combo_id int | base_id int | mods smallint[]
1 | 3 |
2 | 3 | {7}
3 | 3 | {7,15}
4 | 3 | {7,48}
5 | 3 | {7,15,48}
6 | 3 | {15}
7 | 3 | {15,48}
8 | 3 | {48}
I think I could accomplish this using a function that does almost exactly this, iterating over the first table and writing combinations to the second table:
Generate all combinations in SQL
But, I'm a Postgres novice and cannot for the life of me figure out how to do this using plpgsql. It doesn't need to be particularly fast; it will only be run periodically on the backend. The first table has approximately 80 records and a rough calculation suggests we can expect around 2600 records for the second table.
Can anybody at least point me in the right direction?
Edit: Craig: I've got PostgreSQL 9.0. I was successfully able to use UNNEST():
FOR messvar IN SELECT * FROM UNNEST(mods) AS mod WHERE mod BETWEEN 0 AND POWER(2, #n) - 1
LOOP
RAISE NOTICE '%', messvar;
END LOOP;
but then didn't know where to go next.
Edit: For reference, I ended up using Erwin's solution, with a single line added to add a null result ('{}') to each set and the special case Erwin refers to removed:
CREATE OR REPLACE FUNCTION f_combos(_arr integer[], _a integer[] DEFAULT '{}'::integer[], _z integer[] DEFAULT '{}'::integer[])
RETURNS SETOF integer[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
IF array_length(_arr,1) > 0 THEN
_up := array_upper(_arr, 1);
IF _a = '{}' AND _z = '{}' THEN RETURN QUERY SELECT '{}'::int[]; END IF;
FOR i IN array_lower(_arr, 1) .. _up LOOP
FOR j IN i .. _up LOOP
CASE j-i
WHEN 0,1 THEN
RETURN NEXT _a || _arr[i:j] || _z;
ELSE
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN QUERY SELECT *
FROM f_combos(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
END CASE;
END LOOP;
END LOOP;
ELSE
RETURN NEXT _arr;
END IF;
END;
$BODY$
Then, I used that function to populate my table:
INSERT INTO e_ecosystem_modified (ide_ecosystem, modifiers)
(SELECT ide_ecosystem, f_combos(modifiers) AS modifiers FROM e_ecosystem WHERE ecosystemgroup <> 'modifier' ORDER BY ide_ecosystem, modifiers);
From 79 rows in my source table with a maximum of 7 items in the modifiers array, the query took 250ms to populate 2630 rows in my output table. Fantastic.
After I slept over it I had a completely new, simpler, faster idea:
CREATE OR REPLACE FUNCTION f_combos(_arr anyarray)
RETURNS TABLE (combo anyarray) LANGUAGE plpgsql AS
$BODY$
BEGIN
IF array_upper(_arr, 1) IS NULL THEN
combo := _arr; RETURN NEXT; RETURN;
END IF;
CASE array_upper(_arr, 1)
-- WHEN 0 THEN -- does not exist
WHEN 1 THEN
RETURN QUERY VALUES ('{}'), (_arr);
WHEN 2 THEN
RETURN QUERY VALUES ('{}'), (_arr[1:1]), (_arr), (_arr[2:2]);
ELSE
RETURN QUERY
WITH x AS (
SELECT f.combo FROM f_combos(_arr[1:array_upper(_arr, 1)-1]) f
)
SELECT x.combo FROM x
UNION ALL
SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
END CASE;
END
$BODY$;
Call:
SELECT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
512 rows, total runtime: 2.899 ms
Explain
Treat special cases with NULL and empty array.
Build combinations for a primitive array of two.
Any longer array is broken down into:
the combinations for same array of length n-1
plus all of those combined with element n .. recursively.
Really simple, once you got it.
Works for 1-dimensional integer arrays starting with subscript 1 (see below).
2-3 times as fast as old solution, scales better.
Works for any element type again (using polymorphic types).
Includes the empty array in the result as is displayed in the question (and as #Craig pointed out to me in the comments).
Shorter, more elegant.
This assumes array subscripts starting at 1 (Default). If you are not sure about your values, call the function like this to normalize:
SELECT * FROM f_combos(_arr[array_lower(_arr, 1):array_upper(_arr, 1)]);
Not sure if there is a more elegant way to normalize array subscripts. I posted a question about that:
Normalize array subscripts for 1-dimensional array so they start with 1
Old solution (slower)
CREATE OR REPLACE FUNCTION f_combos2(_arr int[], _a int[] = '{}', _z int[] = '{}')
RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
IF array_length(_arr,1) > 0 THEN
_up := array_upper(_arr, 1);
FOR i IN array_lower(_arr, 1) .. _up LOOP
FOR j IN i .. _up LOOP
CASE j-i
WHEN 0,1 THEN
RETURN NEXT _a || _arr[i:j] || _z;
WHEN 2 THEN
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN NEXT _a || _arr[i:j] || _z;
ELSE
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN QUERY SELECT *
FROM f_combos2(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
END CASE;
END LOOP;
END LOOP;
ELSE
RETURN NEXT _arr;
END IF;
END;
$BODY$;
Call:
SELECT * FROM f_combos2('{7,15,48}'::int[]) ORDER BY 1;
Works for 1-dimensional integer arrays.
This could be further optimized, but that's certainly not needed for the scope of this question.
ORDER BY to impose the order displayed in the question.
Provide for NULL or empty array, as NULL is mentioned in the comments.
Tested with PostgreSQL 9.1, but should work with any halfway modern version.
array_lower() and array_upper() have been around for at least since PostgreSQL 7.4. Only parameter defaults are new in version 8.4. Could easily be replaced.
Performance is decent.
SELECT DISTINCT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
511 rows, total runtime: 7.729 ms
Explanation
It builds on this simple form that only creates all combinations of neighboring elements:
CREATE FUNCTION f_combos(_arr int[])
RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
_up := array_upper(_arr, 1);
FOR i in array_lower(_arr, 1) .. _up LOOP
FOR j in i .. _up LOOP
RETURN NEXT _arr[i:j];
END LOOP;
END LOOP;
END;
$BODY$;
But this will fail for sub-arrays with more than two elements. So:
For any sub-array with 3 elements one array with just the outer two elements is added. this is a shortcut for this special case that improves performance and is not strictly needed.
For any sub-array with more than 3 elements I take the outer two elements and fill in with all combinations of inner elements built by the same function recursively.
One approach is with a recursive CTE. Erwin's updated recursive function is significantly faster and scales better, though, so this is really useful as an interesting different approach. Erwin's updated version is much more practical.
I tried a bit counting approach (see the end) but without a fast way to pluck arbitrary elements from an array it proved slower then either recursive approach.
Recursive CTE combinations function
CREATE OR REPLACE FUNCTION combinations(anyarray) RETURNS SETOF anyarray AS $$
WITH RECURSIVE
items AS (
SELECT row_number() OVER (ORDER BY item) AS rownum, item
FROM (SELECT unnest($1) AS item) unnested
),
q AS (
SELECT 1 AS i, $1[1:0] arr
UNION ALL
SELECT (i+1), CASE x
WHEN 1 THEN array_append(q.arr,(SELECT item FROM items WHERE rownum = i))
ELSE q.arr END
FROM generate_series(0,1) x CROSS JOIN q WHERE i <= array_upper($1,1)
)
SELECT q.arr AS mods
FROM q WHERE i = array_upper($1,1)+1;
$$ LANGUAGE 'sql';
It's a polymorphic function, so it'll work on arrays of any type.
The logic is to iterate over each item in the unnested input set, using a working table. Start with an empty array in the working table, with a generation number of 1. For each entry in the input set insert two new arrays into the working table with an incremented generation number. One of the two is a copy of the input array from the previous generation and the other is the input array with the (generation-number)'th item from the input set appended to it. When the generation number exceeds the number of items in the input set, return the last generation.
Usage
You can use the combinations(smallint[]) function to produce the results you desire, using it as a set-returning function in combinatin with the row_number window function.
-- assuming table structure
regress=# \d comb
Table "public.comb"
Column | Type | Modifiers
---------+------------+-----------
base_id | integer |
mods | smallint[] |
SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod
FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
ORDER BY mod;
Results
regress=# SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod
regress-# FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
regress-# ORDER BY mod;
base_id | mod_id | mod
---------+--------+-----------
3 | 1 | {}
3 | 2 | {7}
3 | 3 | {7,15}
3 | 4 | {7,15,48}
3 | 5 | {7,48}
3 | 6 | {15}
3 | 7 | {15,48}
3 | 8 | {48}
(8 rows)
Time: 2.121 ms
Zero element arrays produce a null result. If you want combinations({}) to return one row {} then a UNION ALL with {} will do the job.
Theory
It appears you want the k-combinations for all k in a k-multicombination, rather than simple combinations. See number of combinations with repetition.
In other words, you want all k-combinations of elements from your set, for all k from 0 to n where n is the set size.
Related SO question: SQL - Find all possible combination, which has the really interesting answer about bit counting.
Bit operations exist in Pg, so a bit counting approach should be possible. You'd expect it to be more efficient, but because it's so slow to select a scattered subset of elements from an array it actually works out slower.
CREATE OR REPLACE FUNCTION bitwise_subarray(arr anyarray, elements integer)
RETURNS anyarray AS $$
SELECT array_agg($1[n+1])
FROM generate_series(0,array_upper($1,1)-1) n WHERE ($2>>n) & 1 = 1;
$$ LANGUAGE sql;
COMMENT ON FUNCTION bitwise_subarray(anyarray,integer) IS 'Return the elements from $1 where the corresponding bit in $2 is set';
CREATE OR REPLACE FUNCTION comb_bits(anyarray) RETURNS SETOF anyarray AS $$
SELECT bitwise_subarray($1, x)
FROM generate_series(0,pow(2,array_upper($1,1))::integer-1) x;
$$ LANGUAGE 'sql';
If you could find a faster way to write bitwise_subarray then comb_bits would be very fast. Like, say, a small C extension function, but I'm only crazy enough to write one of those for an SO answer.
Is there a concise way to select the nextval for a PostgreSQL sequence multiple times in 1 query? This would be the only value being returned.
For example, I would like to do something really short and sweet like:
SELECT NEXTVAL('mytable_seq', 3) AS id;
And get:
id
-----
118
119
120
(3 rows)
select nextval('mytable_seq') from generate_series(1,3);
generate_series is a function which returns many rows with sequential numbers, configured by it's arguments.
In above example, we don't care about the value in each row, we just use generate_series as row generator. And for each row we can call nextval. In this case it returns 3 numbers (nextvals).
You can wrap this into function, but I'm not sure if it's really sensible given how short the query is.
There is a great article about this exact problem: "getting multiple values from sequences".
If performance is not an issue, for instance when using the sequence values dwarfs the time used to get them or n is small, then the SELECT nextval('seq') FROM generate_series(1,n) approach is the simplest and most appropriate.
But when preparing data for bulk loads the last approach from the article of incrementing the sequence by n from within a lock is appropriate.
CREATE OR REPLACE FUNCTION foo() RETURNS SETOF INT AS $$
DECLARE
seqval int; x int;
BEGIN
x := 0;
WHILE x < 100 LOOP
SELECT into seqval nextval('f_id_seq');
RETURN NEXT seqval;
x := x+1;
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql STRICT;
Of course, if all you're trying to do is advance the sequence, there's setval().
You could also have the function take a parameter for how many times to loop:
CREATE OR REPLACE FUNCTION foo(loopcnt int) RETURNS SETOF INT AS $$
DECLARE
seqval int;
x int;
BEGIN
x := 0;
WHILE x < loopcnt LOOP
SELECT into seqval nextval('f_id_seq');
RETURN NEXT seqval;x := x+1;
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql STRICT;
Unless you really want three rows returned I would set the sequence to 'INCREMENT BY 3' for each select. Then you can simple add 1 and 2 to the result have have your three sequence numbers.
I tried to add a link to the postgresql docs, but apparenty I am not allowed to post links.
My current best solution is:
SELECT NEXTVAL('mytable_seq') AS id
UNION ALL
SELECT NEXTVAL('mytable_seq') AS id
UNION ALL
SELECT NEXTVAL('mytable_seq') AS id;
Which will correctly return 3 rows... but I would like something that is minimal SQL for even as much as 100 or more NEXTVAL invocations.