PostgreSQL is a powerful, open source object-relational database system. It has a proven architecture that has earned it a strong reputation for reliability, data integrity, and correctness. It runs on all major operating systems, including Linux and UNIX (AIX, BSD, HP-UX, SGI IRIX, Mac OS X, Solaris, Tru64).
Let’s start with a simple example. Many applications include a list of customers who have a balance in their account. We’ll use this sample schema and data:
CREATE TABLE accounts ( owner text , balance numeric , amount numeric );
INSERT INTO accounts VALUES ( 'Bob' , 100 , 4467678 );
INSERT INTO accounts VALUES ( 'Mary' , 200 , 7897989 );
If anything fails, then none of the transactions will happen:
BEGIN ;
UPDATE accounts SET amount = amount - 14 . 00 WHERE owner = 'Bob' ;
UPDATE accounts SET amount = amount + 14 . 00 WHERE owner = 'Mary' ;
COMMIT ;
PostgreSQL includes its own programming language named PL/pgSQL that is aimed to
integrate easily with SQL commands:
CREATE OR REPLACE FUNCTION transfer (
i_payer text ,
i_recipient text ,
i_amount numeric ( 15 , 2 ))
RETURNS text
AS
$$
DECLARE
payer_bal numeric ;
BEGIN
SELECT balance INTO payer_bal FROM accounts WHERE owner = i_payer ;
IF NOT FOUND THEN
RETURN 'Payer account not found' ;
END IF ;
IF payer_bal < i_amount THEN
RETURN 'Not enough funds' ;
END IF ;
UPDATE accounts SET balance = balance + i_amount WHERE owner = i_recipient ;
IF NOT FOUND THEN
RETURN 'Recipient does not exist' ;
END IF ;
UPDATE accounts SET balance = balance - i_amount WHERE owner = i_payer ;
RETURN 'OK' ;
END ;
$$ LANGUAGE plpgsql ;
SELECT * FROM accounts ;
SELECT * FROM transfer ( 'Bob' , 'Mary' , 14 . 00 );
SELECT * FROM accounts ;
Data comparisons using operators
For more complex tasks, you can define your own types, operators, and casts from one type to another, letting you actually compare apples and oranges:
CREATE TYPE FRUIT_QTY as ( name text , qty int );
SELECT '("APPLE", 3)' :: FRUIT_QTY ;
CREATE FUNCTION fruit_qty_larger_than ( left_fruit FRUIT_QTY ,
right_fruit FRUIT_QTY )
RETURNS BOOL
AS $$
BEGIN
IF ( left_fruit . name = 'APPLE' AND right_fruit . name = 'ORANGE' )
THEN
RETURN left_fruit . qty > ( 1 . 5 * right_fruit . qty );
END IF ;
IF ( left_fruit . name = 'ORANGE' AND right_fruit . name = 'APPLE' )
THEN
RETURN ( 1 . 5 * left_fruit . qty ) > right_fruit . qty ;
END IF ;
RETURN left_fruit . qty > right_fruit . qty ;
END ;
$$
LANGUAGE plpgsql ;
SELECT fruit_qty_larger_than ( '("APPLE", 3)' :: FRUIT_QTY , '("ORANGE", 2)' :: FRUIT_QTY );
SELECT fruit_qty_larger_than ( '("APPLE", 4)' :: FRUIT_QTY , '("ORANGE", 2)' :: FRUIT_QTY );
CREATE OPERATOR > (
leftarg = FRUIT_QTY ,
rightarg = FRUIT_QTY ,
procedure = fruit_qty_larger_than ,
commutator = >
);
SELECT '("ORANGE", 2)' :: FRUIT_QTY > '("APPLE", 2)' :: FRUIT_QTY ;
SELECT '("ORANGE", 2)' :: FRUIT_QTY > '("APPLE", 3)' :: FRUIT_QTY ;
Managing related data with triggers
Server programming can also mean setting up automated actions (TRIGGERS), so that some operations in the database cause some other things to happen as well.
For example, you can set up a process where making an offer on some items automatically reserved them in the stock table.
So let’s create a fruit stock table:
CREATE TABLE fruits_in_stock (
name text PRIMARY KEY ,
in_stock integer NOT NULL ,
reserved integer NOT NULL DEFAULT 0 ,
CHECK ( in_stock between 0 and 1000 ),
CHECK ( reserved <= in_stock )
);
CREATE TABLE fruit_offer (
offer_id serial PRIMARY KEY ,
recipient_name text ,
offer_date timestamp default current_timestamp ,
fruit_name text REFERENCES fruits_in_stock ,
offered_amount integer
);
The offer table has an ID for the offer (so you can distinguish between offers later), recipient, date, offered fruit name, and offered amount.
For automating the reservation management, you first need a trigger function, which implements the management logic:
CREATE OR REPLACE FUNCTION reserve_stock_on_offer () RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE fruits_in_stock
SET reserved = reserved + NEW . offered_amount
WHERE name = NEW . fruit_name ;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE fruits_in_stock
SET reserved = reserved - OLD . offered_amount
+ NEW . offered_amount
WHERE name = NEW . fruit_name ;
ELSIF TG_OP = 'DELETE' THEN
UPDATE fruits_in_stock
SET reserved = reserved - OLD . offered_amount
WHERE name = OLD . fruit_name ;
END IF ;
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
You have to tell PostgreSQL to call this function each and every time the offer row is changed:
CREATE TRIGGER manage_reserve_stock_on_offer_change
AFTER INSERT OR UPDATE OR DELETE ON fruit_offer
FOR EACH ROW EXECUTE PROCEDURE reserve_stock_on_offer ();
After this we are ready to test the functionality.
First, we will add some fruit to our stock:
INSERT INTO fruits_in_stock VALUES ( 'APPLE' , 500 );
INSERT INTO fruits_in_stock VALUES ( 'ORANGE' , 500 );
SELECT * FROM fruits_in_stock ;
INSERT INTO fruit_offer ( recipient_name , fruit_name , offered_amount ) VALUES ( 'Bob' , 'APPLE' , 100 );
SELECT * FROM fruit_offer ;
SELECT * FROM fruits_in_stock ;
UPDATE fruit_offer SET offered_amount = 115 WHERE offer_id = 1 ;
SELECT * FROM fruits_in_stock ;
DELETE FROM fruit_offer WHERE offer_id = 1 ;
SELECT * FROM fruits_in_stock ;
Auditing changes
CREATE TABLE salaries (
emp_name text PRIMARY KEY ,
salary integer NOT NULL
);
CREATE TABLE salary_change_log (
changed_by text DEFAULT CURRENT_USER ,
changed_at timestamp DEFAULT CURRENT_TIMESTAMP ,
salary_op text ,
emp_name text ,
old_salary integer ,
new_salary integer
);
REVOKE ALL ON salary_change_log FROM PUBLIC ;
GRANT ALL ON salary_change_log TO managers ;
CREATE OR REPLACE FUNCTION log_salary_change () RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO salary_change_log ( salary_op , emp_name , new_salary ) VALUES ( TG_OP , NEW . emp_name , NEW . salary );
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO salary_change_log ( salary_op , emp_name , old_salary , new_salary ) VALUES ( TG_OP , NEW . emp_name , OLD . salary , NEW . salary );
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO salary_change_log ( salary_op , emp_name , old_salary ) VALUES ( TG_OP , NEW . emp_name , OLD . salary );
END IF ;
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql SECURITY DEFINER ;
CREATE TRIGGER audit_salary_change
AFTER INSERT OR UPDATE OR DELETE ON salaries
FOR EACH ROW EXECUTE PROCEDURE log_salary_change ();
INSERT INTO salaries values ( 'Bob' , 1000 );
UPDATE salaries SET salary = 1100 WHERE emp_name = 'Bob' ;
INSERT INTO salaries VALUES ( 'Mary' , 1000 );
UPDATE salaries SET salary = salary + 200 ;
SELECT * FROM salaries ;
-[ RECORD 1 ] --
emp_name | Bob
salary | 1300
-[ RECORD 2 ] --
emp_name | Mary
salary | 1200
SELECT * FROM salary_change_log ;
Each one of these changes is saved into the salary change log table for auditing purposes:
-[ RECORD 1 ] --------------------------
changed_by | frank
changed_at | 2012-01-25 15:44:43.311299
salary_op | INSERT
emp_name | Bob
old_salary |
new_salary | 1000
-[ RECORD 2 ] --------------------------
changed_by | frank
changed_at | 2012-01-25 15:44:43.313405
salary_op | UPDATE
emp_name | Bob
old_salary | 1000
new_salary | 1100
-[ RECORD 3 ] --------------------------
changed_by | frank
changed_at | 2012-01-25 15:44:43.314208
salary_op | INSERT
emp_name | Mary
old_salary |
new_salary | 1000
-[ RECORD 4 ] --------------------------
changed_by | frank
changed_at | 2012-01-25 15:44:43.314903
salary_op | UPDATE
emp_name | Bob
old_salary | 1100
new_salary | 1300
-[ RECORD 5 ] --------------------------
changed_by | frank
changed_at | 2012-01-25 15:44:43.314903
salary_op | UPDATE
emp_name | Maryold_salary | 1000
new_salary | 1200
Also, give users access to only two functions: the first function is for any user taking a look at salaries and the other function can be used to change salaries, which is available only to managers.
The functions will have all the access to the underlying tables because they are declared as SECURITY DEFINER, which means that they run with the privileges of the user who created them.
This is how the salary lookup function will look:
CREATE OR REPLACE FUNCTION get_salary ( text )
RETURNS integer
AS $$
-- if you look at other people's salaries, it gets logged
INSERT INTO salary_change_log ( salary_op , emp_name , new_salary )
SELECT 'SELECT' , emp_name , salary
FROM salaries
WHERE upper ( emp_name ) = upper ( $ 1 )
AND upper ( emp_name ) != upper ( CURRENT_USER );
-- don't log select of own salary
-- return the requested salary
SELECT salary FROM salaries WHERE upper ( emp_name ) = upper ( $ 1 );
$$ LANGUAGE SQL SECURITY DEFINER ;
The set_salary() function abstracts away the need to check whether the user exists; if the user does not exist, it is created. Setting someone’s salary to 0 will remove him or her from the salary table. Thus, the interface is simplified to a large extent, and the client application of these functions needs to know, and do, less:
CREATE OR REPLACE FUNCTION set_salary ( i_emp_name text , i_salary int )
RETURNS TEXT AS $$
DECLARE
old_salary integer ;
BEGIN
SELECT salary INTO old_salary
FROM salaries
WHERE upper ( emp_name ) = upper ( i_emp_name );
IF NOT FOUND THEN
INSERT INTO salaries VALUES ( i_emp_name , i_salary );
INSERT INTO salary_change_log ( salary_op , emp_name , new_salary )
VALUES ( 'INSERT' , i_emp_name , i_salary );
RETURN 'INSERTED USER ' || i_emp_name ;
ELSIF i_salary > 0 THEN
UPDATE salaries
SET salary = i_salary
WHERE upper ( emp_name ) = upper ( i_emp_name );
INSERT INTO salary_change_log ( salary_op , emp_name , old_salary , new_salary ) VALUES ( 'UPDATE' , i_emp_name , old_salary , i_salary );
RETURN 'UPDATED USER ' || i_emp_name ;
ELSE -- salary set to 0
DELETE FROM salaries WHERE upper ( emp_name ) = upper ( i_emp_name );
INSERT INTO salary_change_log ( salary_op , emp_name , old_salary ) VALUES ( 'DELETE' , i_emp_name , old_salary );
RETURN 'DELETED USER ' || i_emp_name ;
END IF ;
END ;
$$ LANGUAGE plpgsql SECURITY DEFINER ;
Now, drop the audit trigger (otherwise the changes will be logged twice) and test the new functionality:
DROP TRIGGER audit_salary_change ON salaries ;
SELECT set_salary ( 'Fred' , 750 );
SELECT set_salary ( 'frank' , 100 );
SELECT * FROM salaries ;
-[ RECORD 1 ] ---
emp_name | Bob
salary | 1300
-[ RECORD 2 ] ---
emp_name | Mary
salary | 1200
-[ RECORD 3 ] ---
emp_name | Fred
salary | 750
-[ RECORD 4 ] ---
emp_name | frank
salary | 100
SELECT set_salary ( 'mary' , 0 );
-[ RECORD 1 ] -----------------
set_salary | DELETED USER mary
SELECT * FROM salaries ;
- [ RECORD 1 ] ---
emp_name | Bob
salary | 1300
- [ RECORD 2 ] ---
emp_name | Fred
salary | 750
- [ RECORD 3 ] ---
emp_name | frank
salary | 100
SELECT * FROM salary_change_log ;
...
-[ RECORD 6 ] --------------------------
changed_by | gsmith
changed_at | 2013-01-25 15:57:49.057592
salary_op | INSERT
emp_name | Fred
old_salary |
new_salary | 750
-[ RECORD 7 ] --------------------------
changed_by | gsmith
changed_at | 2013-01-25 15:57:49.062456
salary_op | INSERT
emp_name | frank
old_salary |
new_salary | 100
-[ RECORD 8 ] --------------------------
changed_by | gsmith
changed_at | 2013-01-25 15:57:49.064337
salary_op | DELETE
emp_name | mary
old_salary | 1200
new_salary |
Data cleaning
CREATE OR REPLACE FUNCTION uppercase_name ()
RETURNS trigger AS $$
BEGIN
NEW . emp_name = upper ( NEW . emp_name );
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TRIGGER uppercase_emp_name
BEFORE INSERT OR UPDATE OR DELETE ON salaries
FOR EACH ROW EXECUTE PROCEDURE uppercase_name ();
SELECT set_salary ( 'arnold' , 80 );
SELECT * FROM salaries ;
-[ RECORD 1 ] ---
emp_name | Bob
salary | 1300
-[ RECORD 2 ] ---
emp_name | Fred
salary | 750
-[ RECORD 3 ] ---
emp_name | Frank
salary | 100
-[ RECORD 4 ] ---
emp_name | ARNOLD
salary | 80
After fixing the existing mixed-case employee names, we can make sure that all employee names will be uppercased in the future by adding a constraint:
alter table salaries add constraint emp_name_must_be_uppercasepostgres CHECK ( emp_name = upper ( emp_name ));
Conditional expressions
CREATE OR REPLACE FUNCTION format_us_full_name (
prefix text , firstname text ,
mi text , lastname text ,
suffix text )
RETURNS text AS
$$
DECLARE
fname_mi text ;
fmi_lname text ;
prefix_fmil text ;
pfmil_suffix text ;
BEGIN
fname_mi : = CONCAT_WS ( ' ' ,
CASE trim ( firstname )
WHEN ''
THEN NULL
ELSE firstname
END ,
CASE trim ( mi )
WHEN ''
THEN NULL
ELSE mi
END || '.' );
fmi_lname : = CONCAT_WS ( ' ' ,
CASE fname_mi
WHEN ''
THEN NULL
ELSE fname_mi
END ,
CASE trim ( lastname )
WHEN ''
THEN NULL
ELSE lastname
END );
prefix_fmil : = CONCAT_WS ( '. ' ,
CASE trim ( prefix )
WHEN ''
THEN NULL
ELSE prefix
END ,
CASE fmi_lname
WHEN ''
THEN NULL
ELSE fmi_lname
END );
pfmil_suffix : = CONCAT_WS ( ', ' ,
CASE prefix_fmil
WHEN ''
THEN NULL
ELSE prefix_fmil
END ,
CASE trim ( suffix )
WHEN ''
THEN NULL
ELSE suffix || '.'
END );
RETURN pfmil_suffix ;
END ;
$$
LANGUAGE plpgsql ;
SELECT format_us_full_name ( 'Mr' , 'Martin' , 'L' , 'King' , 'Jr' );
format_us_full_name
-------------------------
Mr. Martin L. King, Jr.
Loops with counters
CREATE OR REPLACE FUNCTION fib ( n integer )
RETURNS decimal ( 1000 , 0 )
AS $$
DECLARE counter integer : = 0 ;
DECLARE a decimal ( 1000 , 0 ) : = 0 ;
DECLARE b decimal ( 1000 , 0 ) : = 1 ;
BEGIN
IF ( n < 1 ) THEN
RETURN 0 ;
END IF ;
LOOP
EXIT WHEN counter = n ;
counter : = counter + 1 ;
SELECT b , a + b INTO a , b ;
END LOOP ;
RETURN a ;
END ;
$$
LANGUAGE plpgsql ;
SELECT fib ( 4 );
Looping through query results
FOR row IN
EXECUTE 'SELECT * FROM job_queue q WHERE NOT processed LIMIT 100'
LOOP
CASE row . process_type
WHEN 'archive_point_of_sale'
THEN INSERT INTO hist_orders (...)
SELECT ... FROM orders
INNER JOIN order_detail ...
INNER JOIN item ...;
WHEN 'prune_archived_orders'
THEN DELETE FROM order_detail
WHERE order_id in ( SELECT order_id FROM hist_orders );
DELETE FROM orders
WHERE order_id IN ( SELECT order_id FROM hist_orders );
ELSE
RAISE NOTICE 'Unknown process_type: %' , row . process_type ;
END ;
UPDATE job_queue SET processed = TRUE WHERE id = q . id ;
END LOOP ;
The following is an example from the PostgreSQL documentation that shows dynamic
commands running inside a loop:
CREATE FUNCTION cs_refresh_mviews () RETURNS integer AS $$
DECLARE
mviews RECORD ;
BEGIN
PERFORM cs_log ( 'Refreshing materialized views...' );
FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key
LOOP
--Now "mviews" has one record from cs_materialized_views
PERFORM cs_log ( 'Refreshing materialized view ' || quote_ident ( mviews . mv_name ) || ' ...' );
EXECUTE 'TRUNCATE TABLE ' || quote_ident ( mviews . mv_name );
EXECUTE 'INSERT INTO ' || quote_ident ( mviews . mv_name ) || ' ' || mviews . mv_query ;
END LOOP ;
PERFORM cs_log ( 'Done refreshing materialized views.' );
RETURN 1 ;
END ;
$$ LANGUAGE plpgsql ;
Looping Through Arrays
CREATE FUNCTION findmax ( int []) RETURNS int8 AS $$
DECLARE
max int8 : = 0 ;
x int ;
BEGIN
FOREACH x IN ARRAY $ 1
LOOP
IF x > max THEN
max : = x ;
END IF ;
END LOOP ;
RETURN max ;
END ;
$$ LANGUAGE plpgsql ;
select findmax ( ARRAY [ 1 , 2 , 3 , 4 , 5 , - 1 ]);
Returning a record
So far, all of our function examples have featured a simple scalar value in the RETURN clause. In PL/pgSQL, you can also define set-returning functions (SRF). These functions can return either a type defined by an existing table or a generic record type. Let’s take a look at a simple example:
CREATE TABLE names ( id serial , name varchar );
INSERT INTO names ( name ) VALUES ( 'John' );
INSERT INTO names ( name ) VALUES ( 'Martin' );
INSERT INTO names ( name ) VALUES ( 'Peter' );
CREATE OR REPLACE FUNCTION GetNames () RETURNS SETOF names AS 'SELECT * FROM names;' LANGUAGE 'sql' ;
select GetNames ();
getnames
------------
( 1 , John )
( 2 , Martin )
( 3 , Peter )
( 3 rows )
select * from GetNames () where id > 2 ;
id | name
---- +-------
3 | Peter
( 1 row)
CREATE TYPE nametype AS ( id int , name varchar );
CREATE FUNCTION PlpgGetNames () RETURNS SETOF nametype AS
$$
DECLARE
r nametype % rowtype ;
BEGIN
FOR r IN SELECT id , name FROM names LOOP
RETURN NEXT r ;
END LOOP ;
RETURN ;
END ;
$$ LANGUAGE 'plpgsql' ;
SELECT PlpgGetNames ();
plpggetnames
--------------
( 1 , John )
( 2 , Martin )
( 3 , Peter )
( 3 rows )
CREATE TABLE application_settings_old (
version varchar ( 200 ),
key varchar ( 200 ),
value varchar ( 2000 )
);
CREATE TABLE application_settings_new (
version varchar ( 200 ),
full_name varchar ( 2000 ),
description varchar ( 2000 ),
print_certificate varchar ( 2000 ),
show_advertisements varchar ( 2000 ),
show_splash_screen varchar ( 2000 )
);
CREATE OR REPLACE FUNCTION
flatten_application_settings ( app_version varchar ( 200 ))
RETURNS setof application_settings_new
AS $$
BEGIN
-- Create a temporary table to hold a single row of data
IF EXISTS ( SELECT relname FROM pg_class WHERE relname = 'tmp_settings' )
THEN
TRUNCATE TABLE tmp_settings ;
ELSE
CREATE TEMP TABLE tmp_settings ( LIKE application_settings_new );
END IF ;
-- the row will contain all of the data for this application version
INSERT INTO tmp_settings ( version ) VALUES ( app_version );
-- add the details to the record for this application version
UPDATE tmp_settings
SET full_name = ( SELECT value
FROM application_settings_old
WHERE version = app_version
AND key = 'full_name' ),
description = ( SELECT value
FROM application_settings_old
WHERE version = app_version
AND key = 'description' ),
print_certificate = ( SELECT value
FROM application_settings_old
WHERE version = app_version
AND key = 'print_certificate' ),
show_advertisements = ( SELECT value
FROM application_settings_old
WHERE version = app_version
AND key = 'show_advertisements' ),
show_splash_screen = ( SELECT value
FROM application_settings_old
WHERE version = app_version
AND key = 'show_splash_screen' );
-- hand back the results to the caller
RETURN QUERY SELECT * FROM tmp_settings ;
END ;
$$ LANGUAGE plpgsql ;
INSERT INTO application_settings_new
SELECT ( flatten_application_settings ( version )). *
FROM (
SELECT version
FROM application_settings_old
GROUP BY version
) As ver ;
SELECT fib ( 25 );
SELECT ( flatten_application_settings ( '9.08.97' ));
SELECT * FROM flatten_application_settings ( '9.08.97' );
UPDATE application_settings_new
SET full_name = flat . full_name ,
description = flat . description ,
print_certificate = flat . print_certificate ,
show_advertisements = flat . show_advertisements ,
show_splash_screen = flat . show_splash_screen
FROM flatten_application_settings ( '9.08.97' ) flat ;
Sets and arrays
Returning a set of integers
CREATE OR REPLACE FUNCTION fibonacci_seq ( num integer )
RETURNS SETOF integer AS $$
DECLARE
a int : = 0 ;
b int : = 1 ;
BEGIN
IF ( num <= 0 )
THEN RETURN ;
END IF ;
RETURN NEXT a ;
LOOP
EXIT WHEN num <= 1 ;
RETURN NEXT b ;
num = num - 1 ;
SELECT b , a + b INTO a , b ;
END LOOP ;
END ;
$$ language plpgsql ;
SELECT fibonacci_seq( 5) ;
fibonacci_seq
---------------
0
1
1
2
3
( 5 rows)
Using a set returning function
SELECT * FROM fibonacci_seq( 3) ;
fibonacci_seq
---------------
0
1
1
( 3 rows)
SELECT * FROM fibonacci_seq( 3) WHERE 1 = ANY( SELECT fibonacci_seq( 3)) ;
fibonacci_seq
---------------
0
1
1
( 3 rows)
SELECT * FROM fibonacci_seq( 10) as fib WHERE fib > 3;
fibonacci_seq
---------------
5
8
13
21
34
( 5 rows)
CREATE OR REPLACE FUNCTION installed_languages ()
RETURNS SETOF pg_language AS $$
BEGIN
RETURN QUERY SELECT * FROM pg_language ;
END ;
$$ LANGUAGE plpgsql ;
SELECT * FROM installed_languages ();
- [ RECORD 1 ] -+ ----------
lanname | internal
lanowner | 10
lanispl | f
lanpltrusted | f
lanplcallfoid | 0
laninline | 0
lanvalidator | 2246
lanacl |
- [ RECORD 2 ] -+ ----------
lanname | c
lanowner | 10
lanispl | f
lanpltrusted | f
lanplcallfoid | 0
laninline | 0
lanvalidator | 2247
lanacl |
- [ RECORD 3 ] -+ ----------
lanname | sql
lanowner | 10
lanispl | f
lanpltrusted | t
lanplcallfoid | 0
laninline | 0
lanvalidator | 2248
lanacl |
- [ RECORD 4 ] -+ ----------
lanname | plpgsql
lanowner | 10
lanispl | t
lanpltrusted | t
lanplcallfoid | 12596
laninline | 12597
lanvalidator | 12598
lanacl |
- [ RECORD 5 ] -+ ----------
lanname | plpythonu
lanowner | 10
lanispl | t
lanpltrusted | f
lanplcallfoid | 17563
laninline | 17564
lanvalidator | 17565
lanacl |
Functions based on views
SELECT * FROM pg_stat_activity WHERE state = 'active' ;
-[ RECORD 1 ] ---- +--------------------------------
datid | 17557
datname | hannu
pid | 8933
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2013-03-19 13:47:45.920902-04
xact_start | 2013-03-19 14:05:47.91225-04
query_start | 2013-03-19 14:05:47.91225-04
state_change | 2013-03-19 14:05:47.912253-04
waiting | f
state | active
query | select * from pg_stat_activity | where state = 'active' ;
CREATE VIEW running_queries AS
SELECT
( CURRENT_TIMESTAMP - query_start ) as runtime ,
pid ,
usename ,
waiting ,
query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY 1 DESC
LIMIT 10 ;
CREATE OR REPLACE FUNCTION running_queries ( rows int , qlen int )
RETURNS SETOF running_queries AS
$$
BEGIN
RETURN QUERY SELECT
runtime ,
pid ,
usename ,
waiting ,
substring ( query , 1 , qlen ) as query
FROM running_queries
ORDER BY 1 DESC
LIMIT rows ;
END ;
$$ LANGUAGE plpgsql ;
CREATE OR REPLACE FUNCTION running_queries ( rows int , qlen int )
RETURNS SETOF running_queries AS
$$
BEGIN
RETURN QUERY SELECT
runtime ,
pid ,
usename ,
waiting ,
( CASE WHEN ( usename = session_user )
OR ( select usesuper
from pg_user
where usename = session_user )
THEN
substring ( query , 1 , qlen )
ELSE
substring ( ltrim ( query ), 1 , 6 ) || ' ***'
END ) as query ,
appname
FROM running_queries
ORDER BY 1 DESC
LIMIT rows ;
END ;
$$
LANGUAGE plpgsql
SECURITY DEFINER ;
SELECT * FROM running_queries ( 5 , 25 );
OUT parameters and records
CREATE OR REPLACE FUNCTION positives (
INOUT a int ,
INOUT b int ,
INOUT c int )
AS $$
BEGIN
IF a < 0 THEN a = null ; END IF ;
IF b < 0 THEN b = null ; END IF ;
IF c < 0 THEN c = null ; END IF ;
END ;
$$ LANGUAGE plpgsql ;
SELECT * FROM positives ( - 1 , 1 , 2 );
- [ RECORD 1 ]
a |
b | 1
c | 2
Returning records
CREATE FUNCTION permutations ( INOUT a int ,
INOUT b int ,
INOUT c int )
RETURNS SETOF RECORD
AS $$
BEGIN
RETURN NEXT ;
SELECT b , c INTO c , b ; RETURN NEXT ;
SELECT a , b INTO b , a ; RETURN NEXT ;
SELECT b , c INTO c , b ; RETURN NEXT ;
SELECT a , b INTO b , a ; RETURN NEXT ;
SELECT b , c INTO c , b ; RETURN NEXT ;
END ;
$$ LANGUAGE plpgsql ;
SELECT * FROM permutations ( 1 , 2 , 3 );
- [ RECORD 1 ]
a | 1
b | 2
c | 3
- [ RECORD 2 ]
a | 1
b | 3
c | 2
- [ RECORD 3 ]
a | 3
b | 1
c | 2
- [ RECORD 4 ]
a | 3
b | 2
c | 1
- [ RECORD 5 ]
a | 2
b | 3
c | 1
- [ RECORD 6 ]
a | 2
b | 1
c | 3
Using RETURNS TABLE
CREATE FUNCTION permutations2 ( ia int , ib int , ic int )
RETURNS TABLE ( a int , b int , c int )
AS $$
BEGIN
RETURN NEXT a , b , c ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TYPE abc AS ( a int , b int , c int );
CREATE FUNCTION permutations2 ( ia int , ib int , ic int )
RETURNS SETOF abc
AS $$
BEGIN
RETURN NEXT a , b , c ;
END ;
$$ LANGUAGE plpgsql ;
Returning with no predefined structure
CREATE OR REPLACE FUNCTION run_a_query ( query TEXT )
RETURNS SETOF RECORD
AS $$
DECLARE
retval RECORD ;
BEGIN
FOR retval IN EXECUTE query LOOP
RETURN NEXT retval ;
END LOOP ;
END ;
$$ LANGUAGE PLPGSQL ;
SELECT * FROM run_a_query ( 'SELECT usename::text,usesysid::int FROM
pg_user' ) AS ( "user" text , uid int );
- [ RECORD 1 ] --
user | postgres
uid | 10
- [ RECORD 2 ] --
user | hannu
uid | 17573
Returning SETOF ANY
CREATE OR REPLACE FUNCTION array_to_rows ( array_in ANYARRAY )
RETURNS TABLE ( row_out ANYELEMENT )
AS $$
BEGIN
FOR i IN 1 .. array_upper ( array_in , 1 ) LOOP
row_out = array_in [ i ];
RETURN NEXT ;
END LOOP ;
END ;
$$ LANGUAGE plpgsql ;
SELECT array_to_rows ( '{1,2,3}' :: int []);
- [ RECORD 1 ] -+ --
array_to_rows | 1
- [ RECORD 2 ] -+ --
array_to_rows | 2
- [ RECORD 3 ] -+ --
array_to_rows | 3
Variadic argument lists
PostgreSQL allows you to write a function with a variable number of arguments. This is accomplished using VARIADIC.
CREATE OR REPLACE FUNCTION get_nspc_tbls ( VARIADIC arr name [])
RETURNS TABLE ( table_name name , id oid , nspname name )
AS $$
BEGIN
RETURN QUERY SELECT c . relname , c . oid , n . nspname from pg_class c ,
pg_namespace n where c . relnamespace = n . oid and n . nspname = any ( arr );
END ;
$$ LANGUAGE plpgsql ;
SELECT * FROM get_nspc_tbls ( 'public' , 'pg_temp' );
- [ RECORD 1 ] ------------------------
table_name | a
id | 16434
nspname | public
- [ RECORD 2 ] ------------------------
table_name | parameter
id | 24682
nspname | public
- [ RECORD 3 ] ------------------------
table_name | application_settings_old
id | 24690
nspname | public
- [ RECORD 4 ] ------------------------
table_name | foo
id | 16455
nspname | pg_temp
Returning cursors
DECLARE
curs1 refcursor ;
curs2 CURSOR FOR SELECT * FROM tenk1 ;
curs3 CURSOR ( key integer ) IS SELECT * FROM tenk1 WHERE unique1 = key ;
CREATE FUNCTION myfunc ( refcursor , refcursor ) RETURNS SETOF refcursor AS $$
BEGIN
OPEN $ 1 FOR SELECT * FROM table_1 ;
RETURN NEXT $ 1 ;
OPEN $ 2 FOR SELECT * FROM table_2 ;
RETURN NEXT $ 2 ;
END ;
$$ LANGUAGE plpgsql ;
CREATE FUNCTION myfunc2 ( cur1 refcursor , cur2 refcursor )
RETURNS VOID AS $$
BEGIN
OPEN cur1 FOR SELECT * FROM table_1 ;
OPEN cur2 FOR SELECT * FROM table_2 ;
END ;
$$ LANGUAGE plpgsql ;
BEGIN ;
SELECT * FROM myfunc ( 'a' , 'b' );
FETCH ALL FROM a ;
FETCH ALL FROM b ;
COMMIT ;
Iterating over cursors returned from another function
Let’s define our cursor returning function. This function will open a cursor for a query, based on its argument and then return that cursor:
create table fiverows ( id serial primary key , data text );
insert into fiverows ( data ) values ( 'one' ), ( 'two' ),
( 'three' ), ( 'four' ), ( 'five' );
CREATE FUNCTION curtest1 ( cur refcursor , tag text )
RETURNS refcursor
AS $$
BEGIN
OPEN cur FOR SELECT id , data || '+' || tag FROM fiverows ;
RETURN cur ;
END ;
$$ LANGUAGE plpgsql ;
Next, we define a function, which uses the function we just created to open two additional cursors, and then process the query results. To show that we are not cheating and that the function really creates the cursors, we use the function twice and iterate over the results in parallel:
CREATE FUNCTION curtest2 ( tag1 text , tag2 text )
RETURNS SETOF fiverows
AS $$
DECLARE
cur1 refcursor ;
cur2 refcursor ;
row record ;
BEGIN
cur1 = curtest1 ( NULL , tag1 );
cur2 = curtest1 ( NULL , tag2 );
LOOP
FETCH cur1 INTO row ;
EXIT WHEN NOT FOUND ;
RETURN NEXT row ;
FETCH cur2 INTO row ;
EXIT WHEN NOT FOUND ;
RETURN NEXT row ;
END LOOP ;
END ;
$$ LANGUAGE plpgsql ;
PL/pgSQL Trigger Functions
Creating the trigger function:
CREATE OR REPLACE FUNCTION notify_trigger ()
RETURNS TRIGGER AS $$
BEGIN
RAISE NOTICE 'Hi, I got % invoked FOR % % % on %' ,
TG_NAME ,
TG_LEVEL ,
TG_WHEN ,
TG_OP ,
TG_TABLE_NAME ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TABLE notify_test ( i int );
CREATE TRIGGER notify_insert_trigger
AFTER INSERT ON notify_test
FOR EACH ROW
EXECUTE PROCEDURE notify_trigger ();
INSERT INTO notify_test VALUES ( 1 ),( 2 );
NOTICE : Hi , I got notify_insert_trigger invoked FOR ROW AFTER INSERT on notify_test
ERROR : control reached end of trigger procedure without RETURN
CONTEXT : PL / pgSQL function notify_trigger ()
CREATE OR REPLACE FUNCTION notify_trigger ()
RETURNS TRIGGER AS $$
BEGIN
RAISE NOTICE 'Hi, I got % invoked FOR % % % on %' ,
TG_NAME ,
TG_LEVEL , TG_WHEN , TG_OP , TG_TABLE_NAME ;
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TRIGGER notify_update_trigger
AFTER UPDATE ON notify_test
FOR EACH ROW
EXECUTE PROCEDURE notify_trigger ();
CREATE TRIGGER notify_delete_trigger
AFTER DELETE ON notify_test
FOR EACH ROW
EXECUTE PROCEDURE notify_trigger ();
CREATE TRIGGER notify_trigger
AFTER INSERT OR UPDATE OR DELETE
ON notify_test
FOR EACH ROW
EXECUTE PROCEDURE notify_trigger ();
CREATE TRIGGER notify_truncate_trigger
AFTER TRUNCATE ON notify_test
FOR EACH STATEMENT
EXECUTE PROCEDURE notify_trigger ();
The audit trigger
CREATE TABLE audit_log (
username text , -- who did the change
event_time_utc timestamp , -- when the event was recorded
table_name text , -- contains schema-qualified table name
operation text , -- INSERT, UPDATE, DELETE or TRUNCATE
before_value json , -- the OLD tuple value
after_value json -- the NEW tuple value
);
CREATE OR REPLACE FUNCTION audit_trigger ()
RETURNS trigger AS $$
DECLARE
old_row json : = NULL ;
new_row json : = NULL ;
BEGIN
IF TG_OP IN ( 'UPDATE' , 'DELETE' ) THEN
old_row = row_to_json ( OLD );
END IF ;
IF TG_OP IN ( 'INSERT' , 'UPDATE' ) THEN
new_row = row_to_json ( NEW );
END IF ;
INSERT INTO audit_log (
username ,
event_time_utc ,
table_name ,
operation ,
before_value ,
after_value
) VALUES (
session_user ,
current_timestamp AT TIME ZONE 'UTC' ,
TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME ,
TG_OP ,
old_row ,
new_row
);
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TRIGGER audit_log
AFTER INSERT OR UPDATE OR DELETE
ON notify_test
FOR EACH ROW
EXECUTE PROCEDURE audit_trigger ();
CREATE OR REPLACE FUNCTION cancel_op ()
RETURNS TRIGGER AS $$
BEGIN
IF TG_WHEN = 'AFTER' THEN
RAISE EXCEPTION 'YOU ARE NOT ALLOWED TO % ROWS IN %.%' ,
TG_OP , TG_TABLE_SCHEMA , TG_TABLE_NAME ;
END IF ;
RAISE NOTICE '% ON ROWS IN %.% WON '' T HAPPEN' ,
TG_OP , TG_TABLE_SCHEMA , TG_TABLE_NAME ;
RETURN NULL ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TABLE delete_test1 ( i int );
CREATE TRIGGER disallow_delete AFTER DELETE ON delete_test1 FOR EACH ROW EXECUTE PROCEDURE cancel_op ();
CREATE TRIGGER disallow_truncate
AFTER TRUNCATE ON delete_test1
FOR EACH STATEMENT
EXECUTE PROCEDURE cancel_op ();
Disallowing DELETE and TRUNCATE
CREATE OR REPLACE FUNCTION cancel_op ()
RETURNS TRIGGER AS $$
BEGIN
IF TG_WHEN = 'AFTER' THEN
RAISE EXCEPTION 'YOU ARE NOT ALLOWED TO % ROWS IN %.%' ,
TG_OP , TG_TABLE_SCHEMA , TG_TABLE_NAME ;
END IF ;
RAISE NOTICE '% ON ROWS IN %.% WON '' T HAPPEN' ,
TG_OP , TG_TABLE_SCHEMA , TG_TABLE_NAME ;
RETURN NULL ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TABLE delete_test1 ( i int );
CREATE TRIGGER disallow_delete AFTER DELETE ON delete_test1 FOR EACH ROW EXECUTE PROCEDURE cancel_op ();
CREATE TRIGGER disallow_truncate
AFTER TRUNCATE ON delete_test1
FOR EACH STATEMENT
EXECUTE PROCEDURE cancel_op ();
Modifying the NEW record
CREATE OR REPLACE FUNCTION stamp ()
RETURNS TRIGGER AS $$
BEGIN
NEW . last_changed_by = SESSION_USER ;
NEW . last_changed_at = CURRENT_TIMESTAMP ;
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TABLE modify_test (
id serial PRIMARY KEY ,
data text ,
created_by text default SESSION_USER ,
created_at timestamp default CURRENT_TIMESTAMP ,
last_changed_by text default SESSION_USER ,
last_changed_at timestamp default CURRENT_TIMESTAMP
);
CREATE TRIGGER changestamp
BEFORE UPDATE ON modify_test
FOR EACH ROW
EXECUTE PROCEDURE changestamp ();
CREATE OR REPLACE FUNCTION usagestamp ()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW . created_by = SESSION_USER ;
NEW . created_at = CURRENT_TIMESTAMP ;
ELSE
NEW . created_by = OLD . created_by ;
NEW . created_at = OLD . created_at ;
END IF
NEW . last_changed_by = SESSION_USER ;
NEW . last_changed_at = CURRENT_TIMESTAMP ;
RETURN NEW ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TRIGGER usagestamp
BEFORE INSERT OR UPDATE ON modify_test
FOR EACH ROW
EXECUTE PROCEDURE usagestamp ();
CREATE OR REPLACE FUNCTION cancel_with_message ()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION '%' , TG_ARGV [ 0 ];
RETURN NULL ;
END ;
$$ LANGUAGE plpgsql ;
CREATE TRIGGER no_updates_on_friday_afternoon
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON new_tasks
FOR EACH STATEMENT
WHEN ( CURRENT_TIME > '12:00' AND extract ( DOW from CURRENT_TIMESTAMP ) = 5 )
EXECUTE PROCEDURE cancel_with_message ( new_tasks );
Event Triggers
CREATE TABLE track_ddl
(
event text ,
command text ,
ddl_time timestamp ,
usr text
);
CREATE OR REPLACE FUNCTION track_ddl_function ()
RETURNS event_trigger
AS
$$
BEGIN
INSERT INTO track_ddl values ( tg_tag , tg_event , now (), session_user );
RAISE NOTICE 'DDL logged' ;
END
$$ LANGUAGE plpgsql ;
CREATE EVENT TRIGGER track_ddl_event ON ddl_command_start
WHEN TAG IN ( 'CREATE TABLE' , 'DROP TABLE' , 'ALTER TABLE' )
EXECUTE PROCEDURE track_ddl_function ();
CREATE TABLE event_check ( i int );
SELECT * FROM track_ddl ;
- [ RECORD 1 ] ------------------------
event | CREATE TABLE
command | ddl_command_start
ddl_time | 2014 - 04 - 13 16 : 58 : 40 . 331385
usr | testusr
CREATE OR REPLACE FUNCTION abort_create_table_func ()
RETURNS event_trigger
AS
$$
DECLARE
current_hour int : = extract ( hour from now ());
BEGIN
if current_hour < 9 and current_hour > 18 and TG_TAG = 'CREATE TABLE'
then
RAISE NOTICE 'Not a suitable time to create a table' ;
endif ;
END ;
$$ LANGUAGE plpgsql SECURITY DEFINER ;