16.4 절차적 언어
PostgreSQL의 절차적 언어(procedural language, PL)는 SQL 안에서 다른 언어로 함수·프로시저를 작성하게 해 줍니다. PL/pgSQL이 표준, 다른 언어도 확장으로 지원합니다. 핵심은 SQL과 코드의 통합과 트랜잭션 안의 로직.
지원 언어
| 언어 | 확장 | TRUSTED | 메모 |
|---|---|---|---|
| PL/pgSQL | 코어 (plpgsql) | TRUSTED | 기본 활성, Oracle PL/SQL과 유사 |
| SQL | 코어 | TRUSTED | 단순 함수, inline 최적화 |
| PL/Python | plpython3u | UNTRUSTED | 슈퍼유저만 |
| PL/Perl | plperl / plperlu | TRUSTED / UNTRUSTED | |
| PL/Tcl | pltcl / pltclu | 같음 | |
| PL/v8 | plv8 | UNTRUSTED | JavaScript |
| PL/Java | 외부 | — | 거의 안 씀 |
| PL/Rust | plrust | TRUSTED (sandboxed) | 신규, AWS Aurora |
운영에서 PL/pgSQL이 99%. PL/Python·PL/v8 같은 untrusted는 슈퍼유저만 만들 수 있어 보안 검토 필수입니다.
PL/pgSQL 기본
CREATE OR REPLACE FUNCTION calc_discount(price numeric, status text)
RETURNS numeric AS $$
DECLARE
discount numeric;
BEGIN
IF status = 'gold' THEN
discount := price * 0.2;
ELSIF status = 'silver' THEN
discount := price * 0.1;
ELSE
discount := 0;
END IF;
RETURN discount;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 사용
SELECT calc_discount(100000, 'gold'); -- 20000함수 vs 프로시저
| 종류 | 트랜잭션 제어 |
|---|---|
| FUNCTION | 트랜잭션 안에서 실행. BEGIN·COMMIT 직접 못 함 |
| PROCEDURE (PG 11+) | COMMIT·ROLLBACK 가능 |
-- 프로시저
CREATE OR REPLACE PROCEDURE batch_update()
LANGUAGE plpgsql
AS $$
BEGIN
FOR i IN 1..100 LOOP
UPDATE orders SET ... WHERE id_batch = i;
COMMIT; -- 매 배치마다 commit
END LOOP;
END;
$$;
CALL batch_update();큰 배치를 commit 단위로 쪼개 락·WAL 부담 줄입니다.
트리거
함수가 트리거의 콜백.
CREATE OR REPLACE FUNCTION trg_audit_orders()
RETURNS trigger AS $$
BEGIN
INSERT INTO audit_log(table_name, op, user_name, timestamp, row_data)
VALUES (TG_TABLE_NAME, TG_OP, session_user, now(), to_jsonb(NEW));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION trg_audit_orders();운영 시 트리거 성능 영향 — 무거운 로직 회피합니다. 단순 audit·denormalization 정도.
SQL 함수 vs PL/pgSQL
-- SQL 함수
CREATE FUNCTION get_user(p_id int) RETURNS users AS $$
SELECT * FROM users WHERE id = p_id LIMIT 1;
$$ LANGUAGE sql STABLE;
-- PL/pgSQL 함수
CREATE FUNCTION get_user_plpgsql(p_id int) RETURNS users AS $$
DECLARE
rec users;
BEGIN
SELECT * INTO rec FROM users WHERE id = p_id LIMIT 1;
RETURN rec;
END;
$$ LANGUAGE plpgsql STABLE;| 측면 | SQL | PL/pgSQL |
|---|---|---|
| 옵티마이저 inline | 단순한 경우 inline (호출 비용 0) | 항상 별도 함수 호출 |
| 변수·반복문 | 없음 | 가능 |
동적 SQL (EXECUTE) | 안 됨 | 가능 |
| TRIGGER 사용 | 안 됨 | 가능 |
| 디버깅 | 어려움 | RAISE NOTICE |
권장: 가능한 한 SQL 함수, 복잡한 로직만 PL/pgSQL.
volatility
| 표시 | 의미 |
|---|---|
IMMUTABLE | 입력이 같으면 항상 같은 결과. 옵티마이저가 constant folding |
STABLE | 한 트랜잭션 안에서 같은 결과. plan 캐시 가능 |
VOLATILE (기본) | 매번 다를 수 있음. 매번 호출 |
정확히 분류해야 옵티마이저가 최적화 + expression index 사용 (6.6).
SECURITY DEFINER vs INVOKER
CREATE FUNCTION log_event(event text) RETURNS void AS $$
INSERT INTO audit_log(event, session_user, timestamp)
VALUES (event, session_user, now());
$$ LANGUAGE sql
SECURITY DEFINER
SET search_path = pg_catalog, public;9.4 참고합니다. 정해진 인터페이스로만 보호 데이터 접근 가능하게.
동적 SQL — EXECUTE
CREATE FUNCTION dynamic_query(p_table text, p_id int)
RETURNS setof users AS $$
BEGIN
RETURN QUERY EXECUTE
format('SELECT * FROM %I WHERE id = $1', p_table)
USING p_id;
END;
$$ LANGUAGE plpgsql;format() + %I(identifier)·%L(literal)이 SQL injection 방어.
USING 절로 파라미터 — 평문 concat 절대 금지합니다.
예외 처리
BEGIN
INSERT INTO logs(...) VALUES (...);
EXCEPTION
WHEN unique_violation THEN
UPDATE logs SET ... WHERE id = ...;
WHEN OTHERS THEN
RAISE NOTICE 'Error: %', SQLERRM;
END;block 단위 BEGIN·EXCEPTION·END. 트랜잭션 자체와는 다른 개념.
PL/Python — 외부 라이브러리
CREATE EXTENSION plpython3u; -- 슈퍼유저, untrusted
CREATE FUNCTION fetch_external(url text)
RETURNS text AS $$
import urllib.request
return urllib.request.urlopen(url).read().decode()
$$ LANGUAGE plpython3u;장점: numpy·pandas·requests 등 활용 가능합니다. 단점: GIL, untrusted (보안), 운영 친화 ↓.
운영 권장:
- 외부 호출은 PostgreSQL 안에서 하지 말 것 — 트랜잭션 안 외부 API = 사고
- 무거운 계산은 application 또는 별도 worker
운영 시 주의
| 주의 | 메모 |
|---|---|
| 트리거의 무거운 로직 | INSERT/UPDATE 비용 폭증 |
| 함수 안의 외부 호출 | 트랜잭션 안 외부 API 사고 |
| 함수의 STABLE/IMMUTABLE 잘못 분류 | 옵티마이저 캐시 오작동 — 정확성 위험 |
| SECURITY DEFINER + search_path 누락 | 권한 escalation |
| 동적 SQL의 SQL injection | format() + USING 필수 |
| PL 함수의 plan 캐시 | 첫 호출의 generic plan에 묶여 다음 호출 성능 ↓ — SET plan_cache_mode |
디버깅
RAISE NOTICE 'value = %', some_var;
RAISE WARNING 'attention';
RAISE EXCEPTION 'fatal — abort';
-- 트랜잭션 안 출력
SET client_min_messages TO NOTICE;복잡한 로직은 plpgsql_check 확장으로 정적 분석합니다.
함수 vs application 코드
| 함수에 두는 게 좋음 | application에 두는 게 좋음 |
|---|---|
| 데이터에 가까운 단순 변환 | 비즈니스 로직 일반 |
| 트리거 콜백 | 외부 시스템 호출 |
| 정확성·일관성 필수 | 자주 변경되는 로직 |
| set-based 처리 (성능) | 다양한 client 지원 |
PostgreSQL 함수에 비즈니스 로직 폭주는 stored procedure 안티패턴의 위험합니다. 균형이 핵심입니다.
정리
- PL/pgSQL이 표준입니다. SQL 함수는 단순 case에 inline 효율
- 함수는 트랜잭션 안 — 프로시저(PG 11+)는 commit 가능
- 트리거의 무거운 로직 회피
- SECURITY DEFINER + search_path 표준 패턴
- 동적 SQL은
format() + USING으로 injection 방어 - 외부 호출은 함수 안에 두지 말 것
Part XVI 확장과 응용이 끝났습니다. 다음 Part XVII에서는 현대의 표준 — 클라우드 PostgreSQL을 봅니다.