Introduction To PL PGSQL Development
Introduction To PL PGSQL Development
• SQL queries in the function are just kept as a string at this point.
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
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
Example
DECLARE
v_email users.email%TYPE;
v_my_email v_email%TYPE := 'rds-postgres-extensions-request@amazon.com';
%ROWTYPE
Example
DECLARE
v_user users%ROWTYPE;
Records
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
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
BEGIN
/* Executable section (required). */
EXCEPTION
/* Exception handling section (optional). */
END [ label ]
$$ LANGUAGE plpgsql;
Function Example
RETURN v_count;
END
$$ LANGUAGE plpgsql;
Dollar Quoting
RETURN v_name;
END
$$ LANGUAGE plpgsql;
Default Parameters
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;
RETURN v_count;
END
$$ LANGUAGE plpgsql;
PL/pgSQL Control Structures
Control the Flow
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
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
LOOP
-- some computations
EXIT WHEN count > 0; -- same result as previous example
END LOOP;
CONTINUE
CONTINUE [ label ] [ WHEN expression ];
• 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
• Useful for:
• Ad-hoc query systems
• DDL and database maitenance
EXECUTE command-string [ INTO target ] [ USING expression [, ... ] ];
Dynamic SQL - CAUTION
RETURN v_count;
END
$$ LANGUAGE plpgsql;
RETURN v_count;
END
$$ LANGUAGE plpgsql;
PL/pgSQL Cursors
Cursors
OPEN curs3(42);
OPEN curs3 (key := 42);
Fetching Data
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
• Use SETOF
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
RETURN r;
END
$$ LANGUAGE plpgsql;
Returning Sets of Records
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
[DECLARE]
BEGIN
Exception/Error is Raised
EXCEPTION
Error is Trapped
END
Exceptions
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.
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;
• Provide auditing
• Invalidate cache entries
Structure
• 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
pgbench -n -t 100000
-f INSERTS.pgbench postgres
pgbench -n -t 100000
-f INSERTS.pgbench postgres
• 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
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
• 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
• 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
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
Table Partitioning before PostgreSQL 10
• 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');
• 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
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
• 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
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Event Triggers
• ddl_command_start
• ddl_command_end
• table_rewrite
• sql_drop
ddl_command_start
• Does not fire for shared objects such as databases and roles
https://www.postgresql.org/docs/current/event-trigger-matrix.html
Event Trigger Functions
• 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
• 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;