사용자 정의 함수 — SQL·PL/pgSQL
DB 안에 함수를 두기. SQL vs PL/pgSQL 의 차이와 트리거 함수의 첫 인상.
비슷한 SQL 을 반복해서 쓰고 있다면 — 뷰(13편) 대신 함수를 만들 시점. 매개변수를 받고 결과를 반환하는 진짜 함수. 18편은 SQL 언어와 PL/pgSQL 의 차이, 자주 만드는 패턴 (스칼라·테이블 반환·트리거) 까지.
가장 단순한 SQL 함수
CREATE OR REPLACE FUNCTION full_name(first text, last text)
RETURNS text
LANGUAGE sql
AS $$
SELECT first || ' ' || last;
$$;
-- 사용
SELECT full_name('준성', '박'); -- '준성 박'
SELECT id, full_name(first_name, last_name) FROM users;
SQL 함수의 가치. ① 인라인 가능 — 옵티마이저가 함수 호출을 풀어서 최적화. ② 단순 SELECT 하나면 거의 무료. ③ 표현식 인덱스(16편) 와 함께 자주 쓰임.
PL/pgSQL — 절차형 언어
CREATE OR REPLACE FUNCTION transfer(
from_id bigint,
to_id bigint,
amount numeric
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
from_balance numeric;
BEGIN
SELECT balance INTO from_balance
FROM accounts WHERE id = from_id FOR UPDATE;
IF from_balance < amount THEN
RAISE EXCEPTION '잔액 부족: %', from_balance;
END IF;
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
END;
$$;
-- 사용
SELECT transfer(1, 2, 10000);
SQL vs PL/pgSQL — 언제 무엇
| SQL | PL/pgSQL | |
|---|---|---|
| 문법 | SELECT 한 줄(+여러 줄) | 변수·IF·LOOP 등 절차문 |
| 속도 | 인라인 가능 → 빠름 | 함수 호출 오버헤드 |
| 에러 처리 | 없음 | EXCEPTION 블록 |
| 제어 흐름 | 없음 | IF/LOOP/FOREACH |
| 언제 | 단순 표현식·계산 | 로직·트랜잭션 검증·트리거 |
RETURNS TABLE — 결과 집합 반환
CREATE OR REPLACE FUNCTION top_orders(limit_n int)
RETURNS TABLE (
user_id bigint,
user_name text,
total_rev numeric
)
LANGUAGE sql
AS $$
SELECT u.id, u.name, SUM(o.total)
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY SUM(o.total) DESC
LIMIT limit_n;
$$;
-- 사용 (FROM 절에)
SELECT * FROM top_orders(10);
RETURNS SETOF — 한 컬럼/한 행 여러 개
-- 한 컬럼 여러 행
CREATE FUNCTION fibo(n int)
RETURNS SETOF int
LANGUAGE plpgsql
AS $$
DECLARE
a int := 0;
b int := 1;
i int;
BEGIN
FOR i IN 1..n LOOP
RETURN NEXT a;
SELECT b, a + b INTO a, b;
END LOOP;
END;
$$;
SELECT * FROM fibo(10);
-- 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
STABLE · IMMUTABLE · VOLATILE — 옵티마이저 힌트
CREATE FUNCTION add(a int, b int)
RETURNS int
LANGUAGE sql
IMMUTABLE -- 입력 같으면 결과 항상 같음 (캐시 가능)
AS $$ SELECT a + b $$;
CREATE FUNCTION today()
RETURNS date
LANGUAGE sql
STABLE -- 한 쿼리 안에서는 같은 값 (현재 시각 등)
AS $$ SELECT CURRENT_DATE $$;
-- VOLATILE (기본) — 매 호출마다 달라질 수 있음 (random, nextval 등)
IMMUTABLE 의 가치. 표현식 인덱스(16편)·체크 제약·생성된 컬럼(generated column) 에서 사용 가능. 옵티마이저가 결과를 캐시할 수 있어 빠름. 가능하면 IMMUTABLE 또는 STABLE 명시.
예외 처리 — BEGIN ... EXCEPTION
CREATE FUNCTION safe_divide(a numeric, b numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
BEGIN
RETURN a / b;
EXCEPTION
WHEN division_by_zero THEN
RETURN NULL;
WHEN OTHERS THEN
RAISE NOTICE '예상치 못한 에러: %', SQLERRM;
RETURN NULL;
END;
$$;
트리거 함수 — 자동 실행
-- 1) 트리거 함수
CREATE FUNCTION set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
-- 2) 테이블에 연결
CREATE TRIGGER trg_users_updated
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
-- 이제 UPDATE 마다 updated_at 자동 갱신
UPDATE users SET name = '준석' WHERE id = 1;
-- updated_at 도 자동으로 NOW()
트리거 자세히는 19편에서.
실전 — UPSERT 헬퍼
CREATE FUNCTION upsert_user(
p_id bigint,
p_name text,
p_email text
)
RETURNS users
LANGUAGE plpgsql
AS $$
DECLARE
result users;
BEGIN
INSERT INTO users (id, name, email)
VALUES (p_id, p_name, p_email)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
email = EXCLUDED.email
RETURNING * INTO result;
RETURN result;
END;
$$;
안티패턴 4가지
1. 너무 많은 비즈니스 로직. DB 함수에 도메인 로직을 박으면 버전 관리·테스트가 어려움. 애플리케이션 레이어에서 처리하는 게 보통 더 좋음.
2. 트리거로 사이드 이펙트. 트리거가 다른 테이블도 자동으로 만지면 "어디서 이게 바뀐 거지" 디버깅 지옥. 명시적 함수 호출이 추적하기 좋음.
3. IMMUTABLE 거짓말. 실제로는 안 그런데 IMMUTABLE 표시 → 결과 캐시되어 잘못된 값. 정말 입력만으로 결과 결정되는지 확실히.
4. SECURITY DEFINER 남용. 호출자 권한이 아닌 함수 소유자 권한으로 실행. 강력하지만 권한 escalation 위험. search_path 명시 필수.
19편 — 트리거 (BEFORE·AFTER)
트리거 종류와 흔한 안티패턴, audit 로깅 패턴.
이전: 17편 EXPLAIN · 현재: 18편 (중급) · 다음 → 19편 트리거 · 진행: 18/24