0% found this document useful (0 votes)
143 views

Introduction To PL PGSQL Development

This document provides an overview of PL/pgSQL, PostgreSQL's procedural language extension. It discusses that PL/pgSQL allows writing user-defined functions using general programming tools like loops and conditions. It describes the basic structure of PL/pgSQL blocks including anonymous blocks and named blocks like functions. It also covers key PL/pgSQL concepts like variables, declarations, assignments, SELECT statements, and control structures like IF statements.

Uploaded by

aabch
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
143 views

Introduction To PL PGSQL Development

This document provides an overview of PL/pgSQL, PostgreSQL's procedural language extension. It discusses that PL/pgSQL allows writing user-defined functions using general programming tools like loops and conditions. It describes the basic structure of PL/pgSQL blocks including anonymous blocks and named blocks like functions. It also covers key PL/pgSQL concepts like variables, declarations, assignments, SELECT statements, and control structures like IF statements.

Uploaded by

aabch
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 145

Introduction to PL/pgSQL

Procedural Language Overview

• PostgreSQL allows user-defined functions to be written in a


variety of procedural languages. The database server has no
built-in knowledge about how to interpret the function’s source
text. Instead, the task is passed to a handler that knows the
details of that particular language.
• PostgreSQL currently supports several standard procedural
languages
• PL/pgSQL
• PL/Tcl
• PL/Perl
• PL/Python
• PL/Java
• And many more
What is PL/pgSQL

• PL/pgSQL is the procedural extension to SQL with features of


programming languages

• Data Manipulation and Query statements of SQL are included


within procedural units of code

• Allows using general programming tools with SQL, for example:


loops, conditions, functions, etc.

• This allows a lot more freedom than general SQL, and is


lighter-weight than calling from a client program
How PL/pgSQL works

• PL/pgSQL is like every other “loadable, procedural language.”

• When a PL function is executed, the fmgr loads the language


handler and calls it.

• The language handler then interprets the contents of the


pg_proc entry for the function (proargtypes, prorettype, prosrc).
How PL/pgSQL works

• On the first call of a function in a session, the call handler will


“compile” a function statement tree.

• SQL queries in the function are just kept as a string at this point.

• What might look to you like an expression is actually a SELECT


query:

my_variable := some_parameter * 100;


How PL/pgSQL works

• The PL/pgSQL statement tree is very similar to a PostgreSQL


execution tree.

• The call handler then executes that statement tree.

• On the first execution of a statement node, that has an SQL


query in it, that query is prepared via SPI.

• The prepared plan is then executed for every invocation of that


statement in the current session.
PL/pgSQL Environment
Kinds of PL/pgSQL Blocks

The basic unit in any PL/pgSQL code is a BLOCK. All PL/pgSQL code
is composed of a single block or blocks that occur either sequentially
or nested within another block. There are two kinds of blocks:
• Anonymous blocks (DO)
• Generally constructed dynamically and executed only once by the
user. It is sort of a complex SQL statement
• Named blocks (Functions and Stored Procedures)
• Have a name associated with them, are stored in the database,
and can be executed repeatably, and can take in parameters
Structure of Anonymous Block

DO $$
[ <<label>> ]
DECLARE
/* Declare section (optional). */

BEGIN
/* Executable section (required). */

EXCEPTION
/* Exception handling section (optional). */

END [ label ]
$$;
Comments

• There are two types of comments in PL/pgSQL


• -- starts a comment that extends to the end of the line
• /* multi-line comments */

• Commenting is necessary to tell people what is intended and


why it was done a specific way

• Err on the side of too much commenting


Variables

• Use variables for


• Temporary storage of data
• Manipulation of stored values
• Re-usability
• Ease of maintenance

• Declared in the declarative section within a block


v_last_name VARCHAR(15);
Handling Variables

• Variables declared in the declarations section preceding a block


are initialized to their default values every time the block is
entered, not only once per function call

• Variables in a declaration section can shadow variables of the


same name in an outer block. If the outer block is named with a
label, its variables are still available by specifying them as
<label>.<varname>
Declarations

Syntax
identifier [CONSTANT] datatype [NOT NULL] [:= | = | DEFAULT expr];

Examples
DECLARE
v_birthday DATE;
v_age INT NOT NULL = 21;
v_name VARCHAR(15) := 'Homer';
v_magic CONSTANT NUMERIC := 42;
v_valid BOOLEAN DEFAULT TRUE;
%TYPE

• Declare variable according to :


• A database column definition
• Another previously declared variable
identifier table.column_name%TYPE;

Example
DECLARE
v_email users.email%TYPE;
v_my_email v_email%TYPE := 'rds-postgres-extensions-request@amazon.com';
%ROWTYPE

• Declare a variable with the type of a ROW of a table


identifier table%ROWTYPE;

Example
DECLARE
v_user users%ROWTYPE;
Records

• A record is a type of variable similar to ROWTYPE, but with no


predefined structure

• The actual structure of the record is created when the variable is


first assigned

• A record is not a true data type, only a place holder

DECLARE
r record;
Variable Scope

DO $$
DECLARE
quantity integer := 30;
BEGIN
RAISE NOTICE 'Quantity here is %', quantity; -- 30
quantity := 50;
-- Create a subblock
DECLARE
quantity integer := 80;
BEGIN
RAISE NOTICE 'Quantity here is %', quantity; -- 80
END;
RAISE NOTICE 'Quantity here is %', quantity; -- 50
END
$$;
Qualify an Identifier
DO $$
<< mainblock >>
DECLARE
quantity integer := 30;
BEGIN
RAISE NOTICE 'Quantity here is %', quantity; --30
quantity := 50;
-- Create a subblock
DECLARE
quantity integer := 80;
BEGIN
RAISE NOTICE 'Quantity here is %', mainblock.quantity; --50
RAISE NOTICE 'Quantity here is %', quantity; --80
END;
RAISE NOTICE 'Quantity here is %', quantity; --50
END
$$;
RAISE

• Reports messages
• Can be seen by the client if the appropriate level is used
RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;
Assigning Values

• Use the assignment operator ( := or = )


DECLARE
v_last_name VARCHAR := 'Smith';
v_date DATE;
BEGIN
v_last_name := lower(v_last_name);
v_date := to_date('2000-01-01', 'YYYY-MM-DD');
SELECT in PL/pgSQL

• Retrieve data from the database with a SELECT statement


• Queries must return only one row
• INTO clause is required
DECLARE
v_first_name users.first_name%TYPE;
v_last_name users.last_name%TYPE;
BEGIN
SELECT first_name, last_name
INTO v_first_name, v_last_name
FROM users
WHERE user_id = 1;
END
INSERT / UPDATE / DELETE

DECLARE
v_forum_name forums.name%TYPE := 'Hackers';
BEGIN
INSERT INTO forums (name)
VALUES (v_forum_name);

UPDATE forums
SET moderated = true
WHERE name = v_forum_name;
END
PERFORM

• Evaluate an expression or query but discard the result

• Frequently used when executing maintenance commands


BEGIN
PERFORM create_partition('moderation_log', '2016-06');
END
Structure of Named Blocks

CREATE FUNCTION [ function_name ] ()


RETURNS [return_type] $$
[ <<label>> ]
DECLARE
/* Declare section (optional). */

BEGIN
/* Executable section (required). */

EXCEPTION
/* Exception handling section (optional). */

END [ label ]
$$ LANGUAGE plpgsql;
Function Example

CREATE FUNCTION get_user_count()


RETURNS integer
AS $$
DECLARE
v_count integer;
BEGIN
SELECT count(*)
INTO v_count
FROM users;

RETURN v_count;
END
$$ LANGUAGE plpgsql;
Dollar Quoting

• The tag $$ denotes the start and end of a string


• Optionally can have a non-empty tag as part of the quote
• $_$
• $abc$
• Can be used to prevent unnecessary escape characters
throughout the string
$function$
BEGIN
RETURN ($1 ~ $q$[\t\r\n\v\\]$q$);
END;
$function$
Function Parameters
• One or more parameters can be used
• Parameter names are optional, but highly recommended
CREATE FUNCTION get_user_name(varchar, p_last_name varchar)
RETURNS varchar AS $$
DECLARE
v_first_name varchar;
v_name varchar;
BEGIN
v_first_name := $1;
SELECT name INTO v_name FROM users
WHERE first_name = v_first_name AND last_name = p_last_name
LIMIT 1;

RETURN v_name;
END
$$ LANGUAGE plpgsql;
Default Parameters

• Paramters can have a default value


• This essentially makes them optional parameters
CREATE FUNCTION get_user_count(p_active boolean DEFAULT true)
RETURNS integer AS $$
DECLARE
v_count integer;
BEGIN
SELECT count(*) INTO v_count
FROM users
WHERE active = p_active;

RETURN v_count;
END
$$ LANGUAGE plpgsql;
Assertions
• A convenient shorthand for inserting debugging checks
• Can be controlled by plpgsql.check_asserts variable
CREATE FUNCTION get_user_count(p_active boolean DEFAULT true)
RETURNS integer AS $$
DECLARE
v_count integer;
BEGIN
ASSERT p_active IS NOT NULL;

SELECT count(*) INTO v_count


FROM users
WHERE active = p_active;

RETURN v_count;
END
$$ LANGUAGE plpgsql;
PL/pgSQL Control Structures
Control the Flow

• The logical flow of statements can be changed using conditional


IF statements and loop control structures
• Conditional Strucutres
• Loop Structures
IF Statements

IF-THEN
IF boolean-expression THEN
statements
END IF;

IF-THEN-ELSE
IF boolean-expression THEN
statements
ELSE
statements
END IF;
Nested IF Statements

IF boolean-expression THEN
IF boolean-expression THEN
statements
END IF;
ELSE
statements
END IF;
ELSIF Statements

• A sequence of statements based on multiple conditions


IF number = 0 THEN
result := 'zero';
ELSIF number > 0 THEN
result := 'positive';
ELSIF number < 0 THEN
result := 'negative';
ELSE
-- the only other possibility is that number is null
result := 'NULL';
END IF;
CASE Statements

• Used for complex conditionals


• Allows a variable to be tested for equality against a list of values
BEGIN
CASE status
WHEN 'Pending' THEN RAISE NOTICE 'PENDING';
WHEN 'Accepted' THEN RAISE NOTICE 'ACCEPTED';
WHEN 'Declined' THEN RAISE NOTICE 'DECLINED';
WHEN 'Blocked' THEN RAISE NOTICE 'BLOCKED';
ELSE RAISE NOTICE 'UNKNOWN';
END CASE;
END
Searched CASE Statements

• Each WHEN clause sequentially evaluated until a TRUE is


evaluated
• Subsequent WHEN expressions are not evaluated
BEGIN
CASE
WHEN x BETWEEN 0 AND 10 THEN
RAISE NOTICE 'Value is between zero and ten';
WHEN x BETWEEN 11 AND 20 THEN
RAISE NOTICE 'Value is between eleven and twenty';
END CASE;
$$;
FOUND
• FOUND, which is of type boolean, starts out false within each
PL/pgSQL function call

• It is set by each of the following types of statements:


• A SELECT INTO statement sets FOUND true if it returns a row,
false if no row is returned
• A PERFORM statement sets FOUND true if it produces (and
discards) a row, false if no row is produced
• UPDATE, INSERT, and DELETE statements set FOUND true if at
least one row is affected, false if no row is affected
• A FETCH statement sets FOUND true if it returns a row, false if no
row is returned.
• A FOR statement sets FOUND true if it iterates one or more times,
else false.
FOUND

DECLARE
v_first_name users.first_name%TYPE;
v_last_name users.last_name%TYPE;
BEGIN
SELECT first_name, last_name
INTO v_first_name, v_last_name
FROM users
WHERE user_id = 1;

IF FOUND THEN
RAISE NOTICE 'User Found';
ELSE
RAISE NOTICE 'User Not Found';
END IF;
END
Loop Structures

• Unconstrained Loop

• WHILE Loop

• FOR Loop

• FOREACH Loop
Unconstrained Loops

• Allows execution of its statements at least once, even if the


condition already met upon entering the loop
LOOP
-- some computations
IF count > 0 THEN
EXIT; -- exit loop
END IF;
END LOOP;

LOOP
-- some computations
EXIT WHEN count > 0; -- same result as previous example
END LOOP;
CONTINUE
CONTINUE [ label ] [ WHEN expression ];

• If no label is given, the next iteration of the innermost loop is


begun
• If WHEN is specified, the next iteration of the loop is begun only
if expression is true. Otherwise, control passes to the statement
after CONTINUE
• CONTINUE can be used with all types of loops; it is not limited to
use with unconstrained loops.
LOOP
-- some computations
EXIT WHEN count > 100;
CONTINUE WHEN count < 50;
-- some computations for count IN [50 .. 100]
END LOOP;
WHILE Loops

WHILE condition LOOP


statement1..;
END LOOP;

• Repeats a sequence of statements until the controlling condition


is no longer TRUE
• Condition is evaluated at the beginning of each iteration
WHILE NOT done LOOP
-- some computations here
END LOOP;
FOR Loops

FOR <loop_counter> IN [REVERSE] <low bound>..<high bound> LOOP


-- some computations here
END LOOP;

• Use a FOR loop to shortcut the test for the number of iterations.
• Do not declare the counter; it is declared implicitly
DO $$
BEGIN
FOR i IN 1..10 LOOP
RAISE NOTICE 'value: %', i;
END LOOP;
END
$$;
Looping Over Results

• For loops can directly use a query result


DECLARE
r record;
BEGIN
FOR r IN SELECT email FROM users LOOP
RAISE NOTICE 'Email: %', r.email;
END LOOP;
END
Looping Over Results

• The last row is still accessible after exiting the loop


DECLARE
r record;
BEGIN
FOR r IN SELECT email FROM users LOOP
RAISE NOTICE 'Email: %', r.email;
END LOOP;
RAISE NOTICE 'Email: %', r.email;
END
Looping Over Results

• Looping over dynamic SQL


• Re-planned each time it is executed
DECLARE
rec RECORD;
sql text;
BEGIN
sql := 'SELECT email FROM users';
FOR rec IN EXECUTE sql LOOP
RAISE NOTICE 'Email: %', rec.email;
END LOOP;
END
Looping Over Arrays

• Uses the FOREACH statement


DECLARE
users varchar[] := ARRAY['Mickey', 'Donald', 'Minnie'];
v_user varchar;
BEGIN
FOREACH v_user IN ARRAY users LOOP
RAISE NOTICE 'User: %', v_user;
END LOOP;
END
Looping Over Arrays

• Use the SLICE syntax to iterate over multiple dimensions


DECLARE
users varchar[];
v_dim varchar[];
BEGIN
users := ARRAY[ARRAY['Mickey', 'Donald'], ARRAY['Mouse', 'Duck']];
FOREACH v_dim SLICE 1 IN ARRAY users LOOP
RAISE NOTICE 'Dimension: %', v_dim;
END LOOP;
END
Nested Loops
• Nest loops to multiple levels
• Use labels to distinguish between blocks
• Exit the outer loop with the EXIT statement that references the
label
BEGIN
<<Outer_loop>>
LOOP
v_counter := v_counter + 1;
EXIT WHEN v_counter > 10; -- leaves both loops
<<Inner_loop>>
LOOP
EXIT Outer_loop WHEN total_done = 'YES';
-- leaves both loops
EXIT WHEN inner_done = 'YES';
-- leaves inner loop only
END LOOP Inner_loop;
END LOOP Outer_loop;
END
Dynamic SQL
Dynamic SQL

• A programming methodology for generating and running SQL


statements at run time

• Useful for:
• Ad-hoc query systems
• DDL and database maitenance
EXECUTE command-string [ INTO target ] [ USING expression [, ... ] ];
Dynamic SQL - CAUTION

• There is no plan caching for commands executed via EXECUTE


• The command is planned each time it is run

• Open to SQL injection attacks


• All incoming parameters need to be validated
• Bind the parameters to the command instead of generating the
string
Execute

CREATE FUNCTION grant_select(p_table varchar, p_role varchar)


RETURNS void AS
$$
DECLARE
sql varchar;
BEGIN
sql := 'GRANT SELECT ON TABLE ' || p_table || ' TO ' || p_role;
EXECUTE sql;
END
$$ LANGUAGE plpgsql;

Note: Do not do this. Validate the parameters first.


Execute Into
CREATE FUNCTION get_connection_count(p_role varchar)
RETURNS integer
AS $$
DECLARE
v_count integer;
sql varchar;
BEGIN
sql := 'SELECT count(*) FROM pg_stat_activity
WHERE usename = ''' || p_role || '''';
EXECUTE sql INTO v_count;

RETURN v_count;
END
$$ LANGUAGE plpgsql;

Note: Do not do this. Validate the parameters first.


Execute Using

CREATE FUNCTION get_connection_count(p_role varchar)


RETURNS integer
AS $$
DECLARE
v_count integer;
sql varchar;
BEGIN
sql := 'SELECT count(*) FROM pg_stat_activity
WHERE usename = $1';
EXECUTE sql INTO v_count USING p_role;

RETURN v_count;
END
$$ LANGUAGE plpgsql;
PL/pgSQL Cursors
Cursors

• Every SQL statement executed by PostgreSQL has an individual


cursor associated with it
• Implicit cursors: Declared for all DML and PL/pgSQL SELECT
statements
• Explicit cursors: Declared and named by the programmer

• Use CURSOR to individually process each row returned by a


multiple-row SELECT Statement
Cursor Flow
Declaring Cursors

• A cursor must be declared as a variable


• Use the SCROLL keyword to move backwards through a cursor

name [ [ NO ] SCROLL ] CURSOR [ ( arguments ) ] FOR query;


DECLARE
curs1 refcursor;
curs2 CURSOR FOR SELECT * FROM tenk1;
curs3 CURSOR (key integer) FOR SELECT *
FROM tenk1
WHERE unique1 = key;
Opening Cursors

• The OPEN method to use is dependant on the way it was


declared
OPEN curs1 FOR SELECT * FROM foo WHERE key = mykey;
OPEN cur2;

OPEN curs3(42);
OPEN curs3 (key := 42);
Fetching Data

• FETCH returns the next row


FETCH curs2 INTO foo, bar, baz;

• FETCH can also move around the cursor


FETCH LAST FROM curs3 INTO x, y;
Fetching Data
CREATE FUNCTION grant_select(p_role varchar)
RETURNS void AS $$
DECLARE
sql varchar;
r record;
tbl_cursor CURSOR FOR SELECT schemaname, relname
FROM pg_stat_user_tables;
BEGIN
OPEN tbl_cursor;
LOOP
FETCH tbl_cursor INTO r;
EXIT WHEN NOT FOUND;
sql := 'GRANT SELECT ON TABLE ' || r.schemaname ||
'.' || r.relname || ' TO ' || p_role;
EXECUTE sql;
END LOOP;
CLOSE tbl_cursor;
END
$$ LANGUAGE plpgsql;
PL/pgSQL Returning Data
Returning Scalars
• Simplest return type
CREATE FUNCTION get_connection_count()
RETURNS integer AS $$
DECLARE
v_count integer;
BEGIN
SELECT count(*) INTO v_count
FROM pg_stat_activity;

RETURN v_count;
END
$$ LANGUAGE plpgsql;

SELECT get_connection_count();
get_connection_count
----------------------
11
(1 row)
Returning Nothing
• Some functions do not need a return value
• This is usually a maintenance function of some sort such as
creating partitions or data purging
• Starting in PostgreSQL 11, Stored Procedures can be used in
these cases

• Return VOID
CREATE FUNCTION purge_log()
RETURNS void AS
$$
BEGIN
DELETE FROM moderation_log
WHERE log_date < now() - '90 days'::interval;
END
$$ LANGUAGE plpgsql;
Returning Sets

• Functions can return a result set

• Use SETOF

• Use RETURN NEXT


• RETURN NEXT does not actually return from the function
• Successive RETURN NEXT commands build a result set

• A final RETURN exits the function


Returning Sets
CREATE FUNCTION fibonacci(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;
Returning Records
• More complex structures can be returned
CREATE FUNCTION get_oldest_session()
RETURNS record AS
$$
DECLARE
r record;
BEGIN
SELECT *
INTO r
FROM pg_stat_activity
WHERE usename = SESSION_USER
ORDER BY backend_start DESC
LIMIT 1;

RETURN r;
END
$$ LANGUAGE plpgsql;
Returning Records

• Using a generic record type requires the structure to be defined


at run time
# SELECT * FROM get_oldest_session();
ERROR: a column definition list is required for functions ...
LINE 1: SELECT * FROM get_oldest_session();

SELECT * FROM get_oldest_session()


AS (a oid, b name, c integer, d oid, e name, f text, g inet,
h text, i integer, j timestamptz, k timestamptz,
l timestamptz, m timestamptz, n boolean, o text, p xid,
q xid, r text);
Returning Records
• All tables and views automatically have corresponding type
definitions so they can be used as return types
CREATE FUNCTION get_oldest_session()
RETURNS pg_stat_activity AS $$
DECLARE
r record;
BEGIN
SELECT *
INTO r
FROM pg_stat_activity
WHERE usename = SESSION_USER
ORDER BY backend_start DESC
LIMIT 1;

RETURN r;
END
$$ LANGUAGE plpgsql;
Returning Sets of Records

• Many times, a subset of the table data is needed


• A view can be used to define the necessary structure
CREATE VIEW running_queries AS
SELECT CURRENT_TIMESTAMP - query_start as runtime, pid,
usename, waiting, query
FROM pg_stat_activity
ORDER BY 1 DESC
LIMIT 10;
Returning Sets of Records

• RETURN QUERY can be used to simplify the function


CREATE FUNCTION running_queries(p_rows int, p_len int DEFAULT 50)
RETURNS SETOF running_queries AS
$$
BEGIN
RETURN QUERY SELECT runtime, pid, usename, waiting,
substring(query,1,p_len) as query
FROM running_queries
ORDER BY 1 DESC
LIMIT p_rows;
END
$$ LANGUAGE plpgsql;
OUT Parameters

• Used to return structured information

• RETURNS is optional, but must be record if included


CREATE FUNCTION active_locks(OUT p_exclusive int, OUT p_share int)
OUT Parameters
CREATE FUNCTION active_locks(OUT p_exclusive int, OUT p_share int) AS $$
DECLARE
r record;
BEGIN
p_exclusive := 0;
p_share := 0;
FOR r IN SELECT l.mode
FROM pg_locks l, pg_stat_activity a
WHERE a.pid = l.pid
AND a.usename = SESSION_USER
LOOP
IF r.mode = 'ExclusiveLock' THEN
p_exclusive := p_exclusive + 1;
ELSIF r.mode = 'ShareLock' THEN
p_share := p_share + 1;
END IF;
END LOOP;
END
$$ LANGUAGE plpgsql;
OUT Parameters
• TIP: Think in sets not loops when writing functions for better
performance
• NOTE: Use “OR REPLACE” when updating functions
CREATE OR REPLACE FUNCTION active_locks(OUT p_exclusive int,
OUT p_share int)
AS $$
BEGIN
SELECT sum(CASE l.mode WHEN 'ExclusiveLock' THEN 1 ELSE 0 END),
sum(CASE l.mode WHEN 'ShareLock' THEN 1 ELSE 0 END)
INTO p_exclusive, p_share
FROM pg_locks l, pg_stat_activity a
WHERE a.pid = l.pid
AND a.usename = SESSION_USER;

END
$$ LANGUAGE plpgsql;
Structured Record Sets
• Use OUT parameters and SETOF record
CREATE FUNCTION all_active_locks(OUT p_lock_mode varchar,
OUT p_count int)
RETURNS SETOF record AS $$
DECLARE
r record;
BEGIN
FOR r IN SELECT l.mode, count(*) as k
FROM pg_locks l, pg_stat_activity a
WHERE a.pid = l.pid
AND a.usename = SESSION_USER
GROUP BY 1
LOOP
p_lock_mode := r.mode;
p_count := r.k;
RETURN NEXT;
END LOOP;
RETURN;
...
Structured Record Sets
• Can return a TABLE
CREATE FUNCTION all_active_locks()
RETURNS TABLE (p_lock_mode varchar, p_count int) AS $$
DECLARE
r record;
BEGIN
FOR r IN SELECT l.mode, count(*) as k
FROM pg_locks l, pg_stat_activity a
WHERE a.pid = l.pid
AND a.usename = SESSION_USER
GROUP BY 1
LOOP
p_lock_mode := r.mode;
p_count := r.k;
RETURN NEXT;
END LOOP;
RETURN;
END
$$ LANGUAGE plpgsql;
Refcursors

• A cursor can be returned for large result sets

• The only way to return multiple result sets from a function


CREATE FUNCTION active_info(OUT p_queries refcursor,
OUT p_locks refcursor)
Refcursors
CREATE FUNCTION active_info(OUT p_queries refcursor,
OUT p_locks refcursor)
AS $$
BEGIN
OPEN p_queries FOR SELECT runtime, pid, usename, waiting,
substring(query,1,50) as query
FROM running_queries
ORDER BY 1 DESC;

OPEN p_locks FOR SELECT l.mode, count(*) as k


FROM pg_locks l, pg_stat_activity a
WHERE a.pid = l.pid
AND a.usename = SESSION_USER
GROUP BY 1;
END
$$ LANGUAGE plpgsql;
Handling Meta Information and Exceptions
Meta Information

• Information about the last command run inside of a function

• Several available values


• ROW_COUNT
• RESULT_OID
• PG_CONTEXT

GET DIAGNOSTICS variable { = | := } item [ , ... ];


Meta Information

CREATE OR REPLACE FUNCTION purge_log()


RETURNS void AS
$$
DECLARE
l_rows int;
BEGIN
DELETE FROM moderation_log
WHERE log_date < now() - '90 days'::interval;

GET DIAGNOSTICS l_rows = ROW_COUNT;


RAISE NOTICE 'Deleted % rows from the log', l_rows;
END
$$ LANGUAGE plpgsql;
Exceptions

• An exception is an identifier in PL/pgSQL that is raised during


execution
• It is raised when an error occurs or explicitly by the function
• It is either handled in the EXCEPTION block or propagated to
the calling environment

[DECLARE]
BEGIN
Exception/Error is Raised
EXCEPTION
Error is Trapped
END
Exceptions

• Use the WHEN block inside of the EXCEPTION block to catch


specific cases
• Can use the error name or error code in the EXCEPTION block
WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN …

• Use the special conditions OTHERS as a catch all


WHEN OTHERS THEN ...
Sample Error Codes

Code Name
22000 data_exception
22012 division_by_zero
2200B escape_character_conflict
22007 invalid_datetime_format
22023 invalid_parameter_value
2200M invalid_xml_document
2200S invalid_xml_comment
23P01 exclusion_violation
Exceptions
CREATE OR REPLACE FUNCTION get_connection_count()
RETURNS integer AS $$
DECLARE
v_count integer;
BEGIN
SELECT count(*)
INTO STRICT v_count
FROM pg_stat_activity;

RETURN v_count;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
RAISE NOTICE 'More than 1 row returned';
RETURN -1;
WHEN OTHERS THEN
RAISE NOTICE 'Unknown Error';
RETURN -1;
END
$$ LANGUAGE plpgsql;
Exception Information
• SQLSTATE Returns the numeric value for the error code.

• SQLERRM Returns the message associated with the error


number.
DECLARE
v_count integer;
err_num integer;
err_msg varchar;
BEGIN
...
EXCEPTION
WHEN OTHERS THEN
err_num := SQLSTATE;
err_msg := SUBSTR(SQLERRM,1,100);
RAISE NOTICE 'Trapped Error: %', err_msg;
RETURN -1;
END
Exception Information

• The details of an error are usually required when handling

• Use GET STACKED DIAGNOSTICS to return the details

GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];


Exception Information

Diagnostic Item
RETURNED_SQLSTATE
COLUMN_NAME
CONSTRAINT_NAME
PG_DATATYPE_NAME
MESSAGE_TEXT
TABLE_NAME
SCHEMA_NAME
PG_EXCEPTION_DETAIL
PG_EXCEPTION_HINT
PG_EXCEPTION_CONTEXT
Propagating Exceptions
• Exceptions can be raised explicitly by the function
CREATE OR REPLACE FUNCTION grant_select(p_role varchar)
RETURNS void AS
$$
DECLARE
sql varchar;
r record;
tbl_cursor CURSOR FOR SELECT schemaname, relname
FROM pg_stat_user_tables;
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles
WHERE rolname = p_role) THEN
RAISE EXCEPTION 'Invalid Role: %', p_role;
END IF;
...
Exceptions
• TIP: Use exceptions only when necessary, there is a large
performance impact
• Sub transactions are created to handle the exceptions
CREATE FUNCTION t1() CREATE FUNCTION t2()
RETURNS void AS $$ RETURNS void AS $$
DECLARE DECLARE
i integer; i integer;
BEGIN BEGIN
i := 1; i := 1;
END EXCEPTION
$$ LANGUAGE plpgsql; WHEN OTHERS THEN
RETURN;
END
$$ LANGUAGE plpgsql;

Avg Time: 0.0017ms Avg Time: 0.0032ms


PL/pgSQL Triggers
Triggers

• Code that gets executed when an event happens in the database


• INSERT, UPDATE, DELETE

• Event Triggers fire on DDL


• CREATE, DROP, ALTER
Use Cases
• Table Partitioning before PostgreSQL 10

• Automatically generate derived column values

• Enforce complex constraints

• Enforce referential integrity across nodes in a distributed


database

• Provide transparent event logging

• Provide auditing
• Invalidate cache entries
Structure

• Unlike other databases, a trigger is broken into two pieces


• Trigger
• Trigger Function
CREATE TRIGGER name
{ BEFORE | AFTER | INSTEAD OF }
{ event [ OR ... ] }
ON table_name
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE PROCEDURE function_name ( arguments )
Trigger Function

• A function with no parameters that returns TRIGGER


CREATE FUNCTION trg() RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Trigger Events

• Insert

• Update

• Delete

• Truncate
Timing

• Before
• The trigger is fired before the change is made to the table
• Trigger can modify NEW values
• Trigger can suppress the change altogether

• After
• The trigger is fired after the change is made to the table
• Trigger sees final result of row
Frequency

• For Each Row


• The trigger is fired once each time a row is affected

• For Each Statement


• The trigger is fired once each time a statement is executed
Trigger Overhead

• A firing trigger adds overhead to the calling transaction

• The percentage overhead can be found with a simple pgbench


test:
INSERT INTO trigger_test (value) VALUES (‘hello’);

\set keys :scale


\setrandom key 1 :keys
UPDATE trigger_test SET value = 'HELLO' WHERE key = :key;
Trigger Overhead

pgbench -n -t 100000
-f INSERTS.pgbench postgres

pgbench -n -s 100000 -t 10000


-f UPDATES.pgbench postgres

Inserts: 4510 tps


Updates: 4349 tps
Trigger Overhead

CREATE FUNCTION empty_trigger()


RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER empty_trigger


BEFORE INSERT OR UPDATE ON trigger_test
FOR EACH ROW EXECUTE PROCEDURE empty_trigger();
Trigger Overhead

pgbench -n -t 100000
-f INSERTS.pgbench postgres

pgbench -n -s 100000 -t 10000


-f UPDATES.pgbench postgres

Inserts: 4296 tps (4.8% overhead)


Updates: 3988 tps (8.3% overhead)
Arguments

• NEW
• Variable holding the new row for INSERT/UPDATE operations in
row-level triggers

• OLD
• Variable holding the old row for UPDATE/DELETE operations in
row-level triggers
NEW vs OLD

CREATE TABLE audit (


event_time timestamp NOT NULL,
user_name varchar NOT NULL,
old_row json,
new_row json
);
NEW vs OLD

CREATE OR REPLACE FUNCTION audit_trigger()


RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit
VALUES (CURRENT_TIMESTAMP,
CURRENT_USER,
row_to_json(OLD),
row_to_json(NEW));

RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Arguments
• TG_OP
• A string of INSERT, UPDATE, DELETE, or TRUNCATE telling for
which operation the trigger was fired

• TG_NAME
• Variable that contains the name of the trigger actually fired

• TG_WHEN
• A string of BEFORE, AFTER, or INSTEAD OF, depending on the
trigger’s definition

• TG_LEVEL
• A string of either ROW or STATEMENT depending on the trigger’s
definition
TG_OP

CREATE TABLE audit (


event_time timestamp NOT NULL,
user_name varchar NOT NULL,
operation varchar NOT NULL,
old_row json,
new_row json
);
TG_OP
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO audit VALUES
(CURRENT_TIMESTAMP, CURRENT_USER,TG_OP, row_to_json(OLD), null);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO audit VALUES
(CURRENT_TIMESTAMP, CURRENT_USER,TG_OP,
row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO audit VALUES
(CURRENT_TIMESTAMP, CURRENT_USER,TG_OP, null, row_to_json(NEW));
RETURN NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Arguments

• TG_TABLE_NAME
• The name of the table that caused the trigger invocation.

• TG_RELNAME
• The name of the table that caused the trigger invocation

• TG_RELID
• The object ID of the table that caused the trigger invocation

• TG_TABLE_SCHEMA
• The name of the schema of the table that caused the trigger
invocation
TG_TABLE_NAME

CREATE TABLE audit (


event_time timestamp NOT NULL,
user_name varchar NOT NULL,
operation varchar NOT NULL,
table_name varchar NOT NULL,
old_row json,
new_row json
);
TG_TABLE_NAME

CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$


BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO audit
VALUES (CURRENT_TIMESTAMP, CURRENT_USER,TG_OP,
TG_TABLE_NAME, row_to_json(OLD), null);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO audit
VALUES (CURRENT_TIMESTAMP, CURRENT_USER,TG_OP,
TG_TABLE_NAME, row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
...
Arguments

• TG_NARGS
• The number of arguments given to the trigger procedure in the
CREATE TRIGGER statement

• TG_ARGV[]
• The arguments from the CREATE TRIGGER statement
Trigger Use Cases

• Table Partitioning
• Splitting what is logically one large table into smaller physical
pieces

• Used to:
• Increase performance
• Archive data
• Storage tiering
Table Partitioning before PostgreSQL 10

• Create child tables for each partition


CREATE TABLE audit_2014 (
CHECK ( event_time >= DATE '2014-01-01'
AND event_time < DATE '2015-01-01')
) INHERITS (audit);

CREATE TABLE audit_2015 (


CHECK ( event_time >= DATE '2015-01-01'
AND event_time < DATE '2016-01-01')
) INHERITS (audit);
Table Partitioning before PostgreSQL 10
• The trigger function will move the row to the correct child table
CREATE OR REPLACE FUNCTION partition_audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
EXECUTE 'INSERT INTO audit_' ||
to_char(NEW.event_time, 'YYYY') ||
' VALUES ($1, $2, $3, $4, $5, $6)'
USING NEW.event_time, NEW.user_name, NEW.operation,
NEW.table_name, NEW.old_row, NEW.new_row;

RETURN NULL;
END;
$$
LANGUAGE plpgsql;
Table Partitioning before PostgreSQL 10

• A trigger needs to be added to the parent table


CREATE TRIGGER partition_audit_trigger
BEFORE INSERT ON audit
FOR EACH ROW
EXECUTE PROCEDURE
partition_audit_trigger();
Execution Performance
• Performance is much better if dynamic SQL is not used
CREATE OR REPLACE FUNCTION partition_audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.event_time >= DATE '2015-01-01' AND
NEW.event_time < DATE '2016-01-01' ) THEN
INSERT INTO audit_2015 VALUES (NEW.*);
ELSIF ( NEW.event_time >= DATE '2014-01-01' AND
NEW.event_time < DATE '2015-01-01' ) THEN
INSERT INTO audit_2014 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Fix
partition_audit_trigger() function!';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Moving Partitions

• If the column used for the partition key changes, the row may
need to be moved to a different partition
CREATE TRIGGER move_partition_audit_trigger
BEFORE UPDATE
ON audit_2014
FOR EACH ROW EXECUTE PROCEDURE
move_partition_audit_trigger('2014-01-01', '2015-01-01');

CREATE TRIGGER move_partition_audit_trigger


BEFORE UPDATE
ON audit_2015
FOR EACH ROW EXECUTE PROCEDURE
move_partition_audit_trigger('2015-01-01', '2016-01-01');
Moving Partitions
CREATE FUNCTION move_partition_audit_trigger() RETURNS TRIGGER AS $$
DECLARE
start_date DATE;
end_date DATE;
BEGIN
start_date := TG_ARGV[0];
end_date := TG_ARGV[1];

IF ( NEW.event_time IS DISTINCT FROM OLD.event_time ) THEN


IF (NEW.event_time < start_date OR NEW.event_time >= end_date) THEN
EXECUTE 'DELETE FROM ' || TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME ||
' WHERE ctid = $1' USING OLD.ctid;
INSERT INTO audit VALUES (NEW.*);
RETURN null;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Moving Partitions
• Only fire the trigger if the partition key changes
CREATE TRIGGER move_partition_audit_trigger
BEFORE UPDATE
ON audit_2014
FOR EACH ROW
WHEN (NEW.event_time IS DISTINCT FROM OLD.event_time)
EXECUTE PROCEDURE
move_partition_audit_trigger('2014-01-01', '2015-01-01');

CREATE TRIGGER move_partition_audit_trigger


BEFORE UPDATE
ON audit_2015
FOR EACH ROW
WHEN (NEW.event_time IS DISTINCT FROM OLD.event_time)
EXECUTE PROCEDURE
move_partition_audit_trigger('2015-01-01', '2016-01-01');
Trigger Use Cases

• Calculate columns
• Calculate complex values
• Extract values from complex structures
• Enforce derived values when using denormalization
• Used to:
• Increase performance
• Simplify queries
Extract JSON
$ head -n 5 zips.json
{ ”_id” : ”01001”, ”city” : ”AGAWAM”,
”loc” : [ -72.622739, 42.070206 ], ”pop” : 15338, ”state” : ”MA” }
{ ”_id” : ”01002”, ”city” : ”CUSHMAN”,
”loc” : [ -72.51564999999999, 42.377017 ], ”pop” : 36963, ”state” : ”MA” }
{ ”_id” : ”01005”, ”city” : ”BARRE”,
”loc” : [ -72.10835400000001, 42.409698 ], ”pop” : 4546, ”state” : ”MA” }
{ ”_id” : ”01007”, ”city” : ”BELCHERTOWN”,
”loc” : [ -72.41095300000001, 42.275103 ], ”pop” : 10579, ”state” : ”MA” }
{ ”_id” : ”01008”, ”city” : ”BLANDFORD”,
”loc” : [ -72.936114, 42.182949 ], ”pop” : 1240, ”state” : ”MA” }
CREATE TABLE zips (
zip_code varchar PRIMARY KEY,
state varchar,
data json
);
Extract JSON

CREATE OR REPLACE FUNCTION extract_data_trigger()


RETURNS TRIGGER AS $$
BEGIN
NEW.zip_code := NEW.data->>'_id';
NEW.state := NEW.data->>'state';

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER extract_data_trigger


BEFORE UPDATE OR INSERT ON zips
FOR EACH ROW EXECUTE PROCEDURE extract_data_trigger();
Trigger Use Cases

• Cache invalidation
• Remove stale entries from a cache
• The database tracks all data so is the single source of truth
• Used to:
• Simplify cache management
• Remove application complexity
Note: Foreign Data Wrappers simplify this process significantly
Note: ON (action) CASCADE contraints can simplify this too.
Cache Invalidation

CREATE FUNCTION remove_cache_trigger()


RETURNS TRIGGER AS $$
BEGIN
DELETE from myredis_cache
WHERE key = OLD.id::varchar;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER remove_cache_trigger


AFTER UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE PROCEDURE remove_cache_trigger();
Cache Invalidation - Async

• The latency of updating the cache may not be an acceptable as


part of the main transaction
CREATE FUNCTION remove_cache_trigger()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(TG_TABLE_NAME, OLD.id::varchar);

RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Event Triggers

• Event triggers fire for DML commands (CREATE, ALTER, DROP,


etc)

• They are not tied to a single table

• They are global to a database


Event Triggers

CREATE OR REPLACE FUNCTION notice_ddl()


RETURNS event_trigger AS
$$
BEGIN
RAISE NOTICE 'DDL Fired: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER notice_ddl


ON ddl_command_start
EXECUTE FUNCTION notice_ddl();
Event Trigger Events

• ddl_command_start

• ddl_command_end

• table_rewrite

• sql_drop
ddl_command_start

• Fired just before the command starts


• This is before any information is known about the command

• Fires for all event trigger command tags

• Does not fire for shared objects such as databases and roles

• Does not fire for commands involving event triggers


ddl_command_end

• Fired after the command ends

• Fires for all event trigger command tags

• The objects have been affected so the details of the command


can be obtained
sql_drop

• Fired just before ddl_command_end fires


• The objects have already been removed so they are not accessible

• Only fired for commands that drop an object

• The objects have been affected so the details of the command


can be obtained
table_rewrite

• Fired just before the table is rewritten by the command

• Only fired for commands that rewrites an object

• CLUSTER and VACUUM FULL do not fire the event


Event Tags

• ALTER POLICY • CREATE TABLE


• ALTER SCHEMA • CREATE TABLE AS
• ALTER SEQUENCE • CREATE VIEW
• ALTER TABLE • DROP INDEX
• CREATE EXTENSION • DROP TABLE
• CREATE FUNCTION • DROP VIEW
• CREATE INDEX • GRANT
• CREATE SEQUENCE • REVOKE

The full list is available in the documentation

https://www.postgresql.org/docs/current/event-trigger-matrix.html
Event Trigger Functions

A set of functions to help retrieve information from event triggers

• pg_event_trigger_ddl_commands
• pg_event_trigger_dropped_objects
• pg_event_trigger_table_rewrite_oid
• pg_event_trigger_table_rewrite_reason
Understanding pg_event_trigger_ddl_commands
• Returns a line for each DDL command executed
• Only valid inside a ddl_command_end trigger

Column Type
classid oid
objid oid
objsubid integer
command_tag text
object_type text
schema_name text
object_identity text
in_extension bool
command pg_ddl_command
Understanding pg_event_trigger_dropped_objects

• Returns a line for each object dropped by the DDL executed


• Only valid inside a sql_drop trigger

Column Type Column Type


classid oid object_type text
objid oid schema_name text
objsubid integer object_name text
original bool object_identity text
normal bool address_names text[]
is_temporary bool address_args text[]
Understanding rewrite functions

• Only valid inside a table_rewrite trigger

• pg_event_trigger_table_rewrite_oid
• Returns the OID of the table about to be rewritten

• pg_event_trigger_table_rewrite_reason
• Returns the reason code of why the table was rewritten
Using Event Trigger Functions
CREATE OR REPLACE FUNCTION stop_drops()
RETURNS event_trigger AS
$$
DECLARE
l_tables varchar[] := '{sales, inventory}';
BEGIN
IF EXISTS(SELECT 1
FROM pg_event_trigger_dropped_objects()
WHERE object_name = ANY (l_tables)) THEN
RAISE EXCEPTION 'Drops of critical tables are not permitted';
END IF;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER stop_drops


ON sql_drop
EXECUTE FUNCTION stop_drops();
Things to Remember

• Triggers are part of the parent transaction


• The trigger fails, the main transaction fails
• The main transaction rolls back, the trigger call never happened
• If the trigger takes a long time, the whole transaction timing is
affected

• Triggers can be difficult to debug


• Especially cascaded triggers
PL/pgSQL Best Practices
Programming Practices

• Follow good programming practices


• Indent code consistantly
• Comment code liberly
• Code reuse/modularity practices are different than other
programming languages
• Deep call stacks in PL/pgSQL can be performance intensive
Naming Conventions

• Create and follow a consistent naming convention for objects


• PostgreSQL is case insensitive so init cap does not work, use ”_”
to seperate words in names
• Prefix all parameter names with something like “p_”
• Prefix all variable names with something like “v_”
Performance

• Avoid expensive constructs unless necessary


• Dynamic SQL
• EXCEPTION blocks

You might also like