PL/pgSQL
lastmod 2025-12-10created 2021-05-0133 edits
PL/pgSQL (Procedural Language/PostgreSQL) is a procedural programming language supported by the PostgreSQL ORDBMS. It closely resembles Oracle's PL/SQL language. Implemented by Jan Wieck, PL/pgSQL first appeared with PostgreSQL 6.4, released on October 30, 1998. Version 9 also implements some ISO SQL/PSM features, like overloading of SQL-invoked functions and procedures.
Overview
Basic structure of the language
PL/pgSQL is a block-structured language. The function body must be in the form of a block. A block is defined as follows.
Dollar-Quoted String Constants
- PostgreSQL: Documentation: 8.1: SQL Syntax - 4.1.2.2. Dollar-Quoted String Constants
Dollar quoting ($$ ... $$) lets you write code or queries that span multiple lines.
The start and end must be quoted with the same label, as in $function$ ... $function$.
Example of nested quoting:
Variables and constants
DO $$
DECLARE
age integer := 40;
korean_name varchar(10) := '김형준';
alias_name varchar(50) := 'Dip2K';
weight numeric(3,1) := 65.5;
BEGIN
RAISE NOTICE '%의 별명은 % 이고 나이는 %이며 몸무게는 %입니다.',
korean_name, alias_name, age, weight;
END $$;
Conditional Expressions
Control Structures
RETURN
IF and CASE statements let you execute alternative commands based on certain conditions. PL/pgSQL has three forms of IF:
- IF ... THEN ... END IF
- IF ... THEN ... ELSE ... END IF
- IF ... THEN ... ELSIF ... THEN ... ELSE ... END IF
and two forms of CASE:
- CASE ... WHEN ... THEN ... ELSE ... END CASE
- CASE WHEN ... THEN ... ELSE ... END CASE
Loops
- LOOP, EXIT, CONTINUE, WHILE, FOR, and FOREACH
Data Types
Function example
CREATE OR REPLACE FUNCTION get_age_level(part_name CHAR(20))
RETURNS CHAR(20) AS $$
DECLARE
the_age INTEGER;
BEGIN
SELECT INTO the_age age
FROM person
WHERE name LIKE '%' || part_name || '%';
CASE
WHEN the_age < 10 THEN
RETURN '어린아이';
WHEN the_age >= 10 AND the_age < 20 THEN
RETURN '10대 사춘기';
WHEN the_age >= 20 AND the_age < 30 THEN
RETURN '20대 취준생';
WHEN the_age >= 30 AND the_age < 40 THEN
RETURN '30대 청춘';
ELSE
RETURN '불노장생';
END CASE;
END; $$
LANGUAGE plpgsql;
How to omit RETURNS
Use RETURNS void.
CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
#variable_conflict use_variable
DECLARE
curtime timestamp := now();
BEGIN
UPDATE users SET last_modified = curtime, comment = comment
WHERE users.id = id;
END;
$$ LANGUAGE plpgsql;
How to return a table
Use RETURNS SETOF 테이블이름.
INSERT RETURNING
Or
FOREACH
Syntax and semantics:
[ <<label>> ]
FOREACH var [ SLICE non_negative_integer_literal ] IN ARRAY expression LOOP
statements
END LOOP [ label ];
Looping over the values in an array without the SLICE keyword
Concatenating arrays:
do $body$
declare
arr1 int[] := array[1, 2];
arr2 int[] := array[3, 4, 5];
var int;
begin
<<"FOREACH eaxmple">>
foreach var in array arr1||arr2 loop
raise info '%', var;
end loop "FOREACH eaxmple";
end;
$body$;
Decomposing elements:
create type rt as (f1 int, f2 text);
-- ...
arr rt[] := array[a1, a2];
-- ...
foreach f1, f2 in array arr loop
raise info '% | %', f1::text, f2;
end loop;
How to exit immediately in the middle of a function
Use return; at the position where you want to exit:
create or replace function my_func()
returns setof int language plpgsql as $$
begin
return query select generate_series(1,2);
return;
return query select generate_series(3,4);
end $$;
How to INSERT a record directly
DO LANGUAGE PLPGSQL $$
DECLARE
data forall_data; --- or data forall_data%rowtype
BEGIN
FOR data IN SELECT * FROM forall_data
LOOP
INSERT INTO for_loop select (data).*;
END LOOP;
END;
$$
Whether this works still needs to be tested.
How to return with defined return columns
CREATE OR REPLACE FUNCTION get_persons(v INT)
RETURNS TABLE (f_name CHAR(20), f_age INT)
AS $$
DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT name, age
FROM person
WHERE age >= v)
LOOP
f_name := r.name || '(' || r.age || ')';
f_age := r.age - 1;
RETURN NEXT;
END LOOP;
END; $$
LANGUAGE 'plpgsql';
Returning results that reference a specific table
Use it as follows:
create function increase_progress_value(progress_id uuid, increase_value integer default 10)
returns setof progress as
$$
begin
return query update progress set value = value + increase_value where id = progress_id returning *;
end;
$$
language plpgsql;
When calling rpc from supabase-py, a row must be returned.
-
returns setof progress- returns by referencing the table namedprogress. -
return query ...- returns the query result.
If you call it as follows:
the output is similar to the result of update ... returning *;.
How to return a record
create function increase_progress_value(progress_id uuid, increase_value integer default 10)
returns table as
$$
declare
row record;
begin
update progress set value = value + increase_value where id = progress_id returning * into row;
return row;
end;
$$
language plpgsql;
If you output it as follows:
the following result is returned as a single column:
(451c45d4-290c-4fe9-9a53-bf3355314067,,65,"2024-02-08 12:42:43.503557+00","2024-02-07 12:42:43.503557+00","2024-02-08 09:56:08.025791+00")
Returning permissions according to Answer's administrator/group/project conditions
CREATE OR REPLACE FUNCTION recc_appropriate_permission (
u_uid INTEGER,
g_uid INTEGER,
p_uid INTEGER DEFAULT NULL
)
RETURNS SETOF recc_permission
LANGUAGE plpgsql
AS $function$
DECLARE
r_uid INTEGER := NULL;
is_admin BOOLEAN := FALSE;
BEGIN
SELECT u.is_admin
INTO is_admin
FROM recc_user u
WHERE u.uid=u_uid;
IF is_admin THEN
-- Administrator has full control.
RETURN QUERY
SELECT *
FROM recc_permission;
RETURN;
END IF;
IF NOT p_uid ISNULL THEN
-- First make sure you are a member of the project.
SELECT role_uid
INTO r_uid
FROM recc_project_member
WHERE user_uid=u_uid AND project_uid=p_uid;
END IF;
IF r_uid ISNULL THEN
-- If you are not a project member, check the group member.
SELECT role_uid
INTO r_uid
FROM recc_group_member
WHERE user_uid=u_uid AND group_uid=g_uid;
END IF;
RETURN QUERY
SELECT *
FROM recc_permission
WHERE uid IN (
SELECT permission_uid
FROM recc_role_permission
WHERE role_uid=r_uid
);
END;
$function$;
Function to store a4y device state/chart information
CREATE OR REPLACE FUNCTION add_a4y_state(
state_time TIMESTAMPTZ,
state_fw_ver INTEGER,
state_uid INTEGER,
state_pm10 INTEGER,
state_pm2_5 INTEGER,
state_co2 INTEGER,
state_humidity INTEGER,
state_temperature INTEGER,
state_voc INTEGER,
state_mode INTEGER,
state_power_state INTEGER,
state_fan_control INTEGER,
state_lock INTEGER,
state_filter INTEGER,
state_filter_life INTEGER,
state_uv_led INTEGER,
state_time_reservation INTEGER,
state_sleep_mode INTEGER
)
RETURNS VOID
LANGUAGE plpgsql
AS $function$
DECLARE
current_days TIMESTAMPTZ;
current_hours TIMESTAMPTZ;
days RECORD;
hours RECORD;
days_alpha real;
hours_alpha real;
days_pm10 real;
begin
current_days := date_trunc('day', state_time);
current_hours := date_trunc('hour', state_time);
select * into days
FROM a4y_chart_days
WHERE uid=state_uid and time=current_days;
select * into hours
FROM a4y_chart_hours
WHERE uid=state_uid AND time=current_hours;
-- RAISE NOTICE 'current_days: %', current_days;
-- RAISE NOTICE 'current_hours: %', current_hours;
RAISE NOTICE 'days: %', days;
RAISE NOTICE 'hours: %', hours;
-- RAISE NOTICE 'days_pm10: %', days_pm10;
if days ISNULL then
insert into a4y_chart_days(
time,
uid,
pm10,
pm2_5,
co2,
humidity,
temperature,
voc,
n
) values (
current_days,
state_uid,
state_pm10,
state_pm2_5,
state_co2,
state_humidity,
state_temperature,
state_voc,
1
);
else
days_alpha := days.n / (days.n + 1.0);
days.pm10 := (days_alpha * days.pm10) + ((1.0 - days_alpha) * state_pm10);
days.pm2_5 := (days_alpha * days.pm2_5) + ((1.0 - days_alpha) * state_pm2_5);
days.co2 := (days_alpha * days.co2) + ((1.0 - days_alpha) * state_co2);
days.humidity := (days_alpha * days.humidity) + ((1.0 - days_alpha) * state_humidity);
days.temperature := (days_alpha * days.temperature) + ((1.0 - days_alpha) * state_temperature);
days.voc := (days_alpha * days.voc) + ((1.0 - days_alpha) * state_voc);
days.n := days.n + 1;
update
a4y_chart_days
set
pm10=days.pm10,
pm2_5=days.pm2_5,
co2=days.co2,
humidity=days.humidity,
temperature=days.temperature,
voc=days.voc,
n=days.n
where
uid=state_uid
and time=current_days;
end if;
if hours ISNULL then
insert into a4y_chart_hours(
time,
uid,
pm10,
pm2_5,
co2,
humidity,
temperature,
voc,
n
) values (
current_hours,
state_uid,
state_pm10,
state_pm2_5,
state_co2,
state_humidity,
state_temperature,
state_voc,
1
);
else
hours_alpha := hours.n / (hours.n + 1.0);
hours.pm10 := (hours_alpha * hours.pm10) + ((1.0 - hours_alpha) * state_pm10);
hours.pm2_5 := (hours_alpha * hours.pm2_5) + ((1.0 - hours_alpha) * state_pm2_5);
hours.co2 := (hours_alpha * hours.co2) + ((1.0 - hours_alpha) * state_co2);
hours.humidity := (hours_alpha * hours.humidity) + ((1.0 - hours_alpha) * state_humidity);
hours.temperature := (hours_alpha * hours.temperature) + ((1.0 - hours_alpha) * state_temperature);
hours.voc := (hours_alpha * hours.voc) + ((1.0 - hours_alpha) * state_voc);
hours.n := hours.n + 1;
update
a4y_chart_hours
set
pm10=hours.pm10,
pm2_5=hours.pm2_5,
co2=hours.co2,
humidity=hours.humidity,
temperature=hours.temperature,
voc=hours.voc,
n=hours.n
where
uid=state_uid
and time=current_hours;
end if;
RAISE NOTICE 'days: %', days;
RAISE NOTICE 'hours: %', hours;
if exists(select uid from a4y_state_last where uid=state_uid) then
update
a4y_state_last
set
time=state_time,
fw_ver=state_fw_ver,
pm10=state_pm10,
pm2_5=state_pm2_5,
co2=state_co2,
humidity=state_humidity,
temperature=state_temperature,
voc=state_voc,
mode=state_mode,
power_state=state_power_state,
fan_control=state_fan_control,
lock=state_lock,
filter=state_filter,
filter_life=state_filter_life,
uv_led=state_uv_led,
time_reservation=state_time_reservation,
sleep_mode=state_sleep_mode
where
uid=state_uid;
else
insert into a4y_state_last (
time,
fw_ver,
uid,
pm10,
pm2_5,
co2,
humidity,
temperature,
voc,
mode,
power_state,
fan_control,
lock,
filter,
filter_life,
uv_led,
time_reservation,
sleep_mode
) values (
state_time,
state_fw_ver,
state_uid,
state_pm10,
state_pm2_5,
state_co2,
state_humidity,
state_temperature,
state_voc,
state_mode,
state_power_state,
state_fan_control,
state_lock,
state_filter,
state_filter_life,
state_uv_led,
state_time_reservation,
state_sleep_mode
);
end if;
end;
$function$;
select add_a4y_state(timestamptz '2021-11-26 16:37:16.0+09:00', 0, 100, 8, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
Transactions
- Stackoverflow - PostgreSQL cannot begin/end transactions in PL/pgSQL
- KLDP - PostgreSQL Explanation and Examples: PL/pgSQL
A plpgsql function runs automatically inside a transaction. Either everything succeeds or everything fails. Documentation
Functions and trigger procedures are always executed within a transaction established by an outer query. They cannot start or commit that transaction, since there is no context for them to execute in. However, a block containing an EXCEPTION clause effectively forms a subtransaction that can be rolled back without affecting the outer transaction. SeeSection 42.6.6for more details.
The KLDP documentation explains it as follows:
You must understand that PL/pgSQL's BEGIN/END is different from the database statements for transactions (BEGIN; END;). Also, functions and trigger procedures cannot start or commit a transaction, and Postgres cannot have nested transactions.
See also
Favorite site
- Wikipedia (en) PL/pgSQL
- PL/pgSQL - SQL Procedural Language
- PostgreSQL CREATE FUNCTION By Practical Examples
- PostgreSQL: Documentation: 13: CREATE FUNCTION
Tutorials
- PL/pgSQL Tutorial for PostgreSQL – 1: Introduction and First User-Defined Function
- PL/pgSQL Tutorial for PostgreSQL – 2: Function Arguments
- PL/pgSQL Tutorial for PostgreSQL – 3: Variables and Constants
- PL/pgSQL Tutorial for PostgreSQL – 4: IF Conditional Statements
- PL/pgSQL Tutorial for PostgreSQL – 5: CASE Conditional Statements
- PL/pgSQL Tutorial for PostgreSQL – 6: Loops
- PL/pgSQL Tutorial for PostgreSQL – 7: Functions That Return Query Results
Guide
- Postgresql function development and examples
- KLDP - PostgreSQL Explanation and Examples: PL/pgSQL
- PostgreSQL: Documentation: 11: 43.8. Transaction Management