PL/pgSQL
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.
소개
언어의 기본 구조
PL/pgSQL은 블록구조의 언어다. 함수 본문 구조는 block 형태여야 한다. 블록은 아래처럼 정의된다.
Dollar-Quoted String Constants
- PostgreSQL: Documentation: 8.1: SQL Syntax - 4.1.2.2. Dollar-Quoted String Constants
달러 쿼우팅 ($$
... $$
) 은 여러 줄에 걸친 코드나 쿼리를 작성할 수 있습니다.
$function$
... $function$
와 같이 시작과 끝은 동일한 라벨로 쿼우팅해야 한다.
중첩 쿼우팅의 예시:
변수와 상수
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
함수 예제
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;
RETURNS 생략 방법
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;
테이블 반환 방법
RETURNS SETOF 테이블이름
를 사용하면 된다.
INSERT RETURNING
또는
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
배열 병합:
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$;
요소 분해:
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;
함수 중간에서 즉시 종료 방법
해당하는 위치에서 return;
를 사용하면 된다:
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 $$;
Record 를 직접 INSERT 하는 방법
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;
$$
작동 여부는 테스트 해봐야 한다.
반환할 컬럼을 정의하여 반환 방법
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';
특정 테이블을 참조한 결과를 반환
다음과 같이 사용:
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;
supabase-py에서 rpc 호출시 row 를 꼭 반환해야 한다.
-
returns setof progress
-progress
라는 테이블을 참조하여 반환 한다. -
return query ...
- 쿼리 결과를 반환한다.
다음과 같이 호출하면:
update ... returning *;
결과와 비슷한 결과가 출력된다.
레코드 반환 방법
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;
다음과 같이 출력하면:
다음과 같은 결과가 하나의 컬럼으로 반환된다:
(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")
Answer의 관리자/그룹/프로젝트 조건에 맞춰 권한 반환
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$;
a4y 장치 상태/차트 정보 저장 함수
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);
트랜잭션
- Stackoverflow - PostgreSQL cannot begin/end transactions in PL/pgSQL
- KLDP - PostgreSQL설명과 예제: PL/pgSQL
plpgsql 함수 는 트랜잭션 내에서 자동으로 실행됩니다. 모두 성공하거나 모두 실패합니다. 설명서
함수와 트리거 프로시저는 항상 외부 쿼리에 의해 설정된 트랜잭션 내에서 실행됩니다. 실행할 컨텍스트가 없기 때문에 해당 트랜잭션을 시작하거나 커밋할 수 없습니다. 그러나 EXCEPTION절을 포함하는 블록은 효과적으로 하위 트랜잭션을 형성할 수 있습니다. 외부 트랜잭션에 영향을 주지 않고 롤백됩니다. 이에 대한 자세한 내용은
섹션 42.6.6
을 참조하십시오.
KLDP 설명서는 다음과 같이 설명되어 있다:
PL/pgSQL 의 BEGIN/END 와 Transaction(BEGIN; END;)을 위한 데이타베이스의 명령문과는 다르다는것을 이해해야 한다. 또한 Function과 Trigger Procedure 에서는 트랜잭션을 시작 하거나 commit 을 할 수 없고 Postgres는 중첩된 트랜잭션을 가질 수 없다.
See also
Favorite site
- Wikipedia (en) PL/pgSQL
- PL/pgSQL - SQL 프로시저 언어
- PostgreSQL CREATE FUNCTION By Practical Examples
- PostgreSQL: Documentation: 13: CREATE FUNCTION
Tutorials
- PostgreSQL의 PL/pgSQL 튜토리얼 – 1 : 소개 및 첫번째 사용자 정의 함수
- PostgreSQL의 PL/pgSQL 튜토리얼 – 2 : 함수 인자
- PostgreSQL의 PL/pgSQL 튜토리얼 – 3 : 변수와 상수
- PostgreSQL의 PL/pgSQL 튜토리얼 – 4 : IF 조건문
- PostgreSQL의 PL/pgSQL 튜토리얼 – 5 : CASE 조건문
- PostgreSQL의 PL/pgSQL 튜토리얼 – 6 : 반복문
- PostgreSQL의 PL/pgSQL 튜토리얼 – 7 : 질의 결과를 반환하는 함수