預存程序與函數
預存程序和函數讓你把業務邏輯封裝在資料庫內。本篇將介紹 PL/pgSQL 語法和常見應用場景。
一、 函數 vs 預存程序
| 特性 | 函數 (FUNCTION) | 預存程序 (PROCEDURE) |
|---|---|---|
| 傳回值 | 必須有 | 可選 |
| 交易控制 | 不能 COMMIT/ROLLBACK | 可以 |
| 呼叫方式 | SELECT func() | CALL proc() |
| 可用於觸發器 | 是 | 否 |
二、 建立函數
基本語法
sql
CREATE OR REPLACE FUNCTION 函數名稱(參數)
RETURNS 傳回類型
LANGUAGE plpgsql
AS $$
DECLARE
-- 宣告變數
BEGIN
-- 邏輯
RETURN 結果;
END;
$$;範例:計算總價
sql
CREATE OR REPLACE FUNCTION calculate_total(price NUMERIC, quantity INTEGER, tax_rate NUMERIC DEFAULT 0.05)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN price * quantity * (1 + tax_rate);
END;
$$;
-- 使用
SELECT calculate_total(100, 5); -- 525.00
SELECT calculate_total(100, 5, 0.1); -- 550.00範例:取得使用者資訊
sql
CREATE OR REPLACE FUNCTION get_user_info(user_id INTEGER)
RETURNS TABLE(name TEXT, email TEXT, created_at TIMESTAMPTZ)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT u.name, u.email, u.created_at
FROM users u
WHERE u.id = user_id;
END;
$$;
-- 使用
SELECT * FROM get_user_info(1);三、 PL/pgSQL 語法
變數宣告
sql
DECLARE
-- 基本型別
user_name TEXT;
total_amount NUMERIC(10, 2) := 0;
is_active BOOLEAN DEFAULT TRUE;
-- 從表推斷型別
user_row users%ROWTYPE;
user_email users.email%TYPE;
-- 常數
tax_rate CONSTANT NUMERIC := 0.05;
BEGIN
-- 使用變數
END;條件判斷
sql
IF condition THEN
-- 操作
ELSIF other_condition THEN
-- 操作
ELSE
-- 操作
END IF;
-- CASE 表達式
result := CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
ELSE 'F'
END;迴圈
sql
-- FOR 迴圈
FOR i IN 1..10 LOOP
RAISE NOTICE 'i = %', i;
END LOOP;
-- FOR 迴圈(步進)
FOR i IN REVERSE 10..1 BY 2 LOOP
RAISE NOTICE 'i = %', i;
END LOOP;
-- FOR 迴圈(查詢結果)
FOR rec IN SELECT * FROM users WHERE is_active LOOP
RAISE NOTICE 'User: %', rec.name;
END LOOP;
-- WHILE 迴圈
WHILE counter < 100 LOOP
counter := counter + 1;
END LOOP;
-- LOOP + EXIT
LOOP
-- 操作
EXIT WHEN done;
END LOOP;四、 例外處理
sql
CREATE OR REPLACE FUNCTION safe_divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN a / b;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Division by zero, returning NULL';
RETURN NULL;
WHEN OTHERS THEN
RAISE NOTICE 'Error: %', SQLERRM;
RETURN NULL;
END;
$$;常見例外
| 例外 | 說明 |
|---|---|
division_by_zero | 除以零 |
unique_violation | 違反唯一約束 |
foreign_key_violation | 違反外鍵約束 |
null_value_not_allowed | 不允許 NULL |
check_violation | 違反 CHECK 約束 |
OTHERS | 捕捉所有例外 |
拋出例外
sql
CREATE OR REPLACE FUNCTION validate_age(age INTEGER)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
IF age < 0 THEN
RAISE EXCEPTION 'Age cannot be negative: %', age
USING HINT = 'Please provide a valid age',
ERRCODE = 'check_violation';
END IF;
IF age < 18 THEN
RAISE WARNING 'User is underage: %', age;
END IF;
END;
$$;五、 傳回多個值
RETURNS TABLE
sql
CREATE OR REPLACE FUNCTION get_orders_by_user(uid INTEGER)
RETURNS TABLE(order_id INTEGER, total NUMERIC, created_at TIMESTAMPTZ)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT o.id, o.total, o.created_at
FROM orders o
WHERE o.user_id = uid
ORDER BY o.created_at DESC;
END;
$$;
-- 使用
SELECT * FROM get_orders_by_user(1);RETURNS SETOF
sql
CREATE OR REPLACE FUNCTION active_users()
RETURNS SETOF users
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT * FROM users WHERE is_active = TRUE;
END;
$$;OUT 參數
sql
CREATE OR REPLACE FUNCTION get_stats(
IN table_name TEXT,
OUT row_count BIGINT,
OUT total_size TEXT
)
LANGUAGE plpgsql
AS $$
BEGIN
EXECUTE format('SELECT COUNT(*) FROM %I', table_name) INTO row_count;
SELECT pg_size_pretty(pg_total_relation_size(table_name)) INTO total_size;
END;
$$;
-- 使用
SELECT * FROM get_stats('users');
-- row_count | total_size
-- 12345 | 2048 kB六、 動態 SQL
使用 EXECUTE 執行動態 SQL:
sql
CREATE OR REPLACE FUNCTION dynamic_query(table_name TEXT, filter_column TEXT, filter_value TEXT)
RETURNS SETOF RECORD
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY EXECUTE format(
'SELECT * FROM %I WHERE %I = $1',
table_name,
filter_column
) USING filter_value;
END;
$$;> **SQL Injection 風險**
使用 format() 的 %I(識別符)和 %L(字面值)來安全地插入變數。
sql
-- 安全的做法
EXECUTE format('SELECT * FROM %I WHERE name = %L', table_name, user_input);
-- 不安全!容易被注入
EXECUTE 'SELECT * FROM ' || table_name || ' WHERE name = ''' || user_input || '''';七、 預存程序
PostgreSQL 11+ 支援預存程序,可以控制交易。
sql
CREATE OR REPLACE PROCEDURE transfer_funds(
from_account INTEGER,
to_account INTEGER,
amount NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
-- 扣款
UPDATE accounts SET balance = balance - amount WHERE id = from_account;
-- 確認餘額足夠(否則回滾)
IF NOT FOUND OR (SELECT balance FROM accounts WHERE id = from_account) < 0 THEN
ROLLBACK;
RAISE EXCEPTION 'Insufficient funds';
END IF;
-- 入款
UPDATE accounts SET balance = balance + amount WHERE id = to_account;
COMMIT;
END;
$$;
-- 呼叫
CALL transfer_funds(1, 2, 100.00);八、 實用範例
自動分配流水號
sql
CREATE OR REPLACE FUNCTION generate_order_number()
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
seq_val BIGINT;
order_number TEXT;
BEGIN
SELECT nextval('order_number_seq') INTO seq_val;
order_number := 'ORD-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || LPAD(seq_val::TEXT, 6, '0');
RETURN order_number;
END;
$$;
-- 使用
SELECT generate_order_number(); -- ORD-20240115-000001軟刪除
sql
CREATE OR REPLACE FUNCTION soft_delete()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.deleted_at := NOW();
NEW.is_deleted := TRUE;
RETURN NEW;
END;
$$;審計日誌
sql
CREATE OR REPLACE FUNCTION audit_log()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_logs (table_name, action, old_data, new_data, changed_at)
VALUES (
TG_TABLE_NAME,
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP != 'DELETE' THEN row_to_json(NEW) ELSE NULL END,
NOW()
);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$;九、 函數管理
查看函數
sql
-- psql 內
\df -- 列出所有函數
\df+ function_name -- 顯示函數詳情
-- SQL 查詢
SELECT proname, prosrc FROM pg_proc WHERE proname = 'my_function';刪除函數
sql
DROP FUNCTION IF EXISTS calculate_total(NUMERIC, INTEGER, NUMERIC);授權
sql
GRANT EXECUTE ON FUNCTION calculate_total TO app_user;
REVOKE EXECUTE ON FUNCTION calculate_total FROM PUBLIC;總結
| 概念 | 說明 |
|---|---|
| FUNCTION | 傳回值,可用於 SELECT |
| PROCEDURE | 可控制交易,用 CALL 呼叫 |
| PL/pgSQL | PostgreSQL 的程序語言 |
| EXCEPTION | 例外處理 |
| format() | 安全的動態 SQL |
| RETURNS TABLE | 傳回多列多欄 |
函數讓你把複雜邏輯封裝起來,提高程式碼復用性和維護性。
進階挑戰
- 建立一個
calculate_order_total(order_id)函數,計算訂單總金額(含折扣) - 使用
RETURN TABLE建立一個返回多列的函數,產生銷售報表 - 比較
LANGUAGE sql和LANGUAGE plpgsql函數的效能差異