PL/pgSQL Basics
The Postgres database has a procedural language called PL/PGSQL which can save you db round-trips and do all sorts of optimizations on the db layer. In this post I’m going to be showing 10 sql files as we progress on PL/PGSQL’s basics.
First here’s a basic hello world function:
-- hello-world.sql
CREATE FUNCTION hello_world()
RETURNS VOID AS $$
BEGIN
RAISE NOTICE 'Hello World';
END; $$ LANGUAGE plpgsql;
Here we have a basic function with name hello_world, it returns nothing (VOID) and the statement body contains a single line which just raises the ‘Hello World’ text, its like the equivalent of echo or print if you will.
-- sum.sql
CREATE FUNCTION sum(a INT, b INT)
RETURNS INT AS $$
BEGIN
RETURN a + b;
END; $$ LANGUAGE plpgsql;
Here we have a sum function which accepts two integers and also returns an integer. Notice how the parameters are declared, should be name then followed by its type. If your function returns something you also need to specify its type after RETURNS.
-- greet.sql
CREATE OR REPLACE FUNCTION greet(name VARCHAR)
RETURNS VARCHAR AS $$
BEGIN
RETURN 'Hello ' || name;
END; $$ LANGUAGE plpgsql;
The only thing that’s new here is the body, we concatenate strings using || operator.
-- swap.sql
CREATE FUNCTION swap(a INT, b INT)
RETURNS RECORD AS $$
DECLARE
tmp INT;
BEGIN
tmp := a;
a := b;
b := tmp;
RETURN (a, b);
END; $$ LANGUAGE plpgsql;
Here we have the classic swap implementation, we declare variable via DECLARE block. Also we’re returning a set but the return type is RECORD. Its a special generic type which can hold any type, here we’re passing set to it.
-- status.sql
CREATE OR REPLACE FUNCTION status(status INT)
RETURNS VARCHAR AS $$
BEGIN
IF status = 100 THEN
RETURN 'DRAFT';
ELSIF status > 100 AND status < 400 THEN
RETURN 'PROCESSING';
ELSIF status = 400 THEN
RETURN 'RUNNING';
ELSIF status > 400 THEN
RETURN 'STOPPED';
ELSE
RETURN 'INVALID';
END IF;
END; $$ LANGUAGE plpgsql;
This is how you do conditions, notice that equal comparison uses a single = (equal) sign.
-- query-single.sql
CREATE OR REPLACE FUNCTION get_contact(contactid BIGINT)
RETURNS contact AS $$
DECLARE
result contact;
BEGIN
SELECT * INTO result FROM contact WHERE id = contactid;
RETURN result;
END; $$ LANGUAGE plpgsql;
This is our first example on how to use queries with it, we’re assigning the result on to result variable. Notice how the return type and result type is contact. This is the name of the table actually and if you use it as a type, it means that variable can hold a row type for that table.
-- result-existence.sql
CREATE OR REPLACE FUNCTION get_contact(contactid BIGINT)
RETURNS contact AS $$
DECLARE
result contact;
count INT;
BEGIN
SELECT * INTO result FROM contact WHERE id = contactid;
GET DIAGNOSTICS count := ROW_COUNT;
IF NOT FOUND THEN
RAISE NOTICE 'not found!';
ELSE
RAISE NOTICE 'found %!', count;
END IF;
RETURN result;
END; $$ LANGUAGE plpgsql;
PL/PGSQL has a bunch of special variables and FOUND is just one of them. It holds a boolean value of wether or not our query returned anything. We can also retrieve the number of rows affected via ROW_COUNT which is another one of those special variables.
-- return-next.sql
CREATE OR REPLACE FUNCTION get_numbers()
RETURNS setof INT AS $$
DECLARE
numbers INT[] = ARRAY[1, 2, 3];
BEGIN
FOR i IN 1 .. array_upper(numbers, 1) LOOP
RETURN NEXT numbers[i];
END LOOP;
END; $$ LANGUAGE plpgsql;
Then we introduce arrays, you can do inline initialization by using ARRAY[…] form. One thing to keep in mind is that array’s index starts at 1 instead of 0, and you can get the number of elements on the built-in array function array_upper. We also introduce RETURN NEXT, what it does is it doesn’t actually exit the function but just appends the item to the implicit output handler of our function. Then once the function ends, it returns this altogether.
So what about queryset?
-- return-query.sql
CREATE OR REPLACE FUNCTION get_contacts()
RETURNS SETOF contact AS $$
BEGIN
RETURN QUERY SELECT * FROM contact;
END; $$ LANGUAGE plpgsql;
Here we introduce RETURN QUERY which lets us return a cursor to the queryset. Notice that the return type is SETOF contact, its required when using RETURN NEXT or RETURN QUERY as the rows returned will contain the set of columns for that table, contacts in this case.
-- cursor.sql
CREATE OR REPLACE FUNCTION get_contacts()
RETURNS SETOF contact AS $$
DECLARE
pointer CURSOR
FOR SELECT * FROM contact;
BEGIN
FOR row IN pointer LOOP
RETURN NEXT row;
END LOOP;
END; $$ LANGUAGE plpgsql;
Finally as our last example, here’s an explicit usage of a bound cursor. Here pointer is a cursor pointing to its query’s result which we can iterate on a normal FOR loop. Again we use SETOF contact as return type to conform on the set of columns present on each row.
Note that while all we’ve shown are just stand alone functions, a function can call another one, can be called in a trigger and eveywhere else where a built-in postgres function can be called. Now go forth and try it!