觸發器與事件觸發器:PostgreSQL 的自動化執行機制 | PostgreSQL
觸發器(Trigger) 是 PostgreSQL 中附加於資料表或視圖的自動執行機制,當特定 DML 或 DDL 事件發生時,資料庫引擎會自動呼叫預先定義的函式。Trigger 是實作稽核日誌、商業邏輯強制執行、資料完整性維護的核心工具,搭配 Event Trigger 更能攔截 DDL 操作,守護資料庫結構安全。
Trigger 類型分類
PostgreSQL 的 Trigger 沿著兩個維度分類:觸發範圍 與 觸發時機。
ROW vs STATEMENT
| 類型 | 關鍵字 | 說明 | 適用場景 |
|---|---|---|---|
| 列級觸發器 | FOR EACH ROW | 每影響一列執行一次 | 審計單列變更、計算欄位 |
| 陳述式觸發器 | FOR EACH STATEMENT | 整個 SQL 陳述式執行一次 | 批次操作日誌、權限驗證 |
UPDATE 1000 rows 時,ROW Trigger 執行 1000 次;STATEMENT Trigger 只執行 1 次,效能差異顯著。
BEFORE / AFTER / INSTEAD OF
| 時機 | 適用對象 | NEW 可修改 | 可取消操作 | 典型用途 |
|---|---|---|---|---|
BEFORE | 資料表 | 是 | 是(return NULL) | 資料驗證、欄位自動填值 |
AFTER | 資料表 | 否 | 否 | 稽核日誌、通知、外部同步 |
INSTEAD OF | 視圖(View) | 是 | 是 | 可更新視圖的實作 |
INSTEAD OF 只能用於視圖,且只支援 ROW Trigger。
完整分類矩陣
BEFORE AFTER INSTEAD OF
┌──────────┬──────────┬──────────────────┐
FOR EACH ROW │ 資料表 │ 資料表 │ 視圖(View) │
├──────────┼──────────┼──────────────────┤
FOR EACH STATEMENT│ 資料表 │ 資料表 │ 不支援 │
└──────────┴──────────┴──────────────────┘
Trigger 執行流程
使用者執行 INSERT / UPDATE / DELETE
│
▼
┌───────────────────┐
│ BEFORE STATEMENT │ (若存在)
│ Trigger 執行 │
└────────┬──────────┘
│
▼ (對每一列重複)
┌───────────────────┐
│ BEFORE ROW │ 可修改 NEW、return NULL 取消
│ Trigger 執行 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 實際修改資料列 │ 寫入 heap
│ │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ AFTER ROW │ 可見最終資料狀態
│ Trigger 執行 │
└────────┬──────────┘
│
▼ (所有列處理完畢)
┌───────────────────┐
│ AFTER STATEMENT │ (若存在)
│ Trigger 執行 │
└────────┬──────────┘
│
▼
交易繼續
NEW / OLD 變數
NEW 與 OLD 是 ROW Trigger 中的特殊變數,型別為 RECORD:
| 操作 | OLD | NEW |
|---|---|---|
| INSERT | NULL(不存在) | 新插入的列 |
| UPDATE | 修改前的列 | 修改後的列 |
| DELETE | 被刪除的列 | NULL(不存在) |
| TRUNCATE | 不適用 | 不適用 |
STATEMENT Trigger 中 NEW 與 OLD 均不可用。若需批次操作的列資料,請使用 Transition Tables(REFERENCING OLD TABLE / NEW TABLE,PG10+)。
TG_* 特殊變數
| 變數 | 型別 | 內容 |
|---|---|---|
TG_OP | text | 'INSERT'、'UPDATE'、'DELETE'、'TRUNCATE' |
TG_NAME | name | Trigger 名稱 |
TG_TABLE_NAME | name | 觸發的資料表名稱 |
TG_TABLE_SCHEMA | name | 觸發的 Schema 名稱 |
TG_WHEN | text | 'BEFORE'、'AFTER'、'INSTEAD OF' |
TG_LEVEL | text | 'ROW' 或 'STATEMENT' |
TG_NARGS | integer | CREATE TRIGGER 中傳入的參數數量 |
TG_ARGV[] | text[] | 傳入的參數陣列(從 0 開始) |
Trigger Function 基本結構
CREATE OR REPLACE FUNCTION my_trigger_func()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
-- BEFORE ROW: 回傳 NEW 讓操作繼續,回傳 NULL 取消操作
-- AFTER ROW / STATEMENT: 回傳值被忽略,慣例回傳 NULL
RETURN NEW;
END;
$$;
CREATE TRIGGER 語法
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER 觸發器名稱
{ BEFORE | AFTER | INSTEAD OF }
{ INSERT | UPDATE [ OF 欄位 [, ...] ] | DELETE | TRUNCATE } [ OR ... ]
ON 資料表名稱
[ REFERENCING { { OLD | NEW } TABLE [ AS ] 別名 } [...] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( 條件 ) ]
EXECUTE FUNCTION 函式名稱 ( 引數 );
-- 移除
DROP TRIGGER 觸發器名稱 ON 資料表名稱;
-- 暫時停用/啟用
ALTER TABLE 資料表名稱 DISABLE TRIGGER 觸發器名稱;
ALTER TABLE 資料表名稱 ENABLE TRIGGER 觸發器名稱;
ALTER TABLE 資料表名稱 DISABLE TRIGGER ALL;
BEFORE ROW Trigger 實戰
自動填入時間戳記
CREATE OR REPLACE FUNCTION set_timestamps()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.created_at = CURRENT_TIMESTAMP;
NEW.updated_at = CURRENT_TIMESTAMP;
ELSIF TG_OP = 'UPDATE' THEN
NEW.created_at = OLD.created_at; -- 防止覆寫
NEW.updated_at = CURRENT_TIMESTAMP;
END IF;
RETURN NEW; -- 回傳修改後的列
END;
$$;
CREATE TRIGGER trg_set_timestamps
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION set_timestamps();
資料驗證
CREATE OR REPLACE FUNCTION validate_product_price()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.price < 0 THEN
RAISE EXCEPTION '價格不得為負數:%', NEW.price;
END IF;
IF NEW.discount_price IS NOT NULL AND NEW.discount_price >= NEW.price THEN
RAISE EXCEPTION '折扣價(%)必須低於原價(%)',
NEW.discount_price, NEW.price;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_validate_price
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION validate_product_price();
AFTER ROW Trigger:稽核日誌
-- 建立稽核日誌表
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
table_name text NOT NULL,
operation text NOT NULL,
old_data jsonb,
new_data jsonb,
changed_by text NOT NULL DEFAULT current_user,
changed_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 通用稽核函式(可套用至任何表)
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER -- 以函式擁有者身份執行
AS $$
BEGIN
INSERT INTO audit_log (table_name, operation, old_data, new_data)
VALUES (
TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME,
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) ELSE NULL END
);
RETURN NULL; -- AFTER Trigger 回傳值被忽略
END;
$$;
-- 套用至任意資料表
CREATE TRIGGER trg_audit_orders
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
EXECUTE FUNCTION audit_trigger_func();
條件觸發(WHEN 子句)
WHEN 子句讓 Trigger 只在滿足條件時執行,避免不必要的函式呼叫開銷:
-- 只在 status 真正改變時才記錄
CREATE TRIGGER trg_status_change
AFTER UPDATE ON orders
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION log_status_change();
-- 只在金額超過門檻時觸發審核
CREATE TRIGGER trg_high_value_order
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.total_amount > 100000)
EXECUTE FUNCTION notify_finance_team();
UPDATE OF 特定欄位觸發
-- 只在 price 或 stock_quantity 更新時才重新計算
CREATE TRIGGER trg_recalculate_cache
AFTER UPDATE OF price, stock_quantity ON products
FOR EACH ROW
EXECUTE FUNCTION recalculate_product_cache();
-- 組合 UPDATE OF 與 WHEN
CREATE TRIGGER trg_price_audit
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change();
INSTEAD OF Trigger(用於 View)
-- 跨表視圖
CREATE VIEW order_summary AS
SELECT
o.id AS order_id, o.total_amount,
c.name AS customer_name, c.email AS customer_email
FROM orders o
JOIN customers c ON c.id = o.customer_id;
-- INSTEAD OF Trigger 讓視圖可 UPDATE
CREATE OR REPLACE FUNCTION update_order_summary()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE orders SET total_amount = NEW.total_amount
WHERE id = NEW.order_id;
UPDATE customers
SET name = NEW.customer_name, email = NEW.customer_email
WHERE id = (SELECT customer_id FROM orders WHERE id = NEW.order_id);
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_update_order_summary
INSTEAD OF UPDATE ON order_summary
FOR EACH ROW
EXECUTE FUNCTION update_order_summary();
Trigger 執行順序規則
當同一事件上有多個 Trigger 時:
- BEFORE 先於 AFTER
- 相同時機的多個 Trigger,依名稱字母順序 執行
- 建議在名稱前加數字前綴控制順序:
10_validate_、20_audit_ - Constraint Trigger 總是在 AFTER ROW 之後執行,且可延遲(
DEFERRABLE) - 若 BEFORE ROW Trigger 回傳
NULL,該列的後續操作(含 AFTER Trigger)均被跳過
Event Trigger(DDL 攔截)
Event Trigger 是 PG9.3 引入的特殊 Trigger,可攔截 DDL 操作:
| 事件 | 觸發時機 |
|---|---|
ddl_command_start | DDL 指令開始前 |
ddl_command_end | DDL 指令完成後 |
sql_drop | DROP 操作執行後,物件被移除前 |
table_rewrite | 資料表重寫前 |
記錄 DDL 操作
CREATE OR REPLACE FUNCTION log_ddl_event()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
INSERT INTO ddl_audit_log (
event_type, object_type, object_identity,
command_tag, executed_by, executed_at
) VALUES (
TG_EVENT, r.object_type, r.object_identity,
r.command_tag, current_user, CURRENT_TIMESTAMP
);
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_audit_ddl
ON ddl_command_end
EXECUTE FUNCTION log_ddl_event();
防止 DROP TABLE
CREATE OR REPLACE FUNCTION prevent_table_drop()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_dropped_objects() LOOP
IF r.object_type = 'table' AND r.schema_name = 'public' THEN
RAISE EXCEPTION '禁止在 public schema 刪除資料表:%', r.object_name;
END IF;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_prevent_drop
ON sql_drop
EXECUTE FUNCTION prevent_table_drop();
常見設計模式
Soft Delete(軟刪除)
CREATE OR REPLACE FUNCTION soft_delete()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE documents SET deleted_at = CURRENT_TIMESTAMP
WHERE id = OLD.id;
RETURN NULL; -- 取消實際 DELETE
END;
$$;
CREATE TRIGGER trg_soft_delete
BEFORE DELETE ON documents
FOR EACH ROW
WHEN (OLD.deleted_at IS NULL)
EXECUTE FUNCTION soft_delete();
-- 搭配視圖隱藏已刪除的列
CREATE VIEW active_documents AS
SELECT * FROM documents WHERE deleted_at IS NULL;
欄位級差異日誌
CREATE OR REPLACE FUNCTION log_field_changes()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_data jsonb; new_data jsonb;
diff jsonb := '{}'; key text;
BEGIN
old_data := to_jsonb(OLD);
new_data := to_jsonb(NEW);
FOR key IN SELECT jsonb_object_keys(new_data) LOOP
IF old_data->key IS DISTINCT FROM new_data->key THEN
diff := diff || jsonb_build_object(
key, jsonb_build_object('from', old_data->key, 'to', new_data->key)
);
END IF;
END LOOP;
IF diff <> '{}' THEN
INSERT INTO field_change_log (table_name, row_id, changed_fields)
VALUES (TG_TABLE_NAME, NEW.id, diff);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_field_changes
AFTER UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION log_field_changes();
跨表一致性同步
CREATE OR REPLACE FUNCTION sync_order_total()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE orders
SET total_amount = (
SELECT COALESCE(SUM(unit_price * quantity), 0)
FROM order_items
WHERE order_id = COALESCE(NEW.order_id, OLD.order_id)
)
WHERE id = COALESCE(NEW.order_id, OLD.order_id);
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_sync_order_total
AFTER INSERT OR UPDATE OR DELETE ON order_items
FOR EACH ROW
EXECUTE FUNCTION sync_order_total();
Transition Tables(PG10+)
Transition Tables 讓 STATEMENT Trigger 也能存取被影響的所有列:
CREATE OR REPLACE FUNCTION audit_bulk_update()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO bulk_audit_log (table_name, operation, affected_count, sample_ids)
SELECT
TG_TABLE_NAME, TG_OP, COUNT(*),
array_agg(id ORDER BY id) FILTER (WHERE id IS NOT NULL)
FROM new_data;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_bulk_audit
AFTER UPDATE ON orders
REFERENCING OLD TABLE AS old_data NEW TABLE AS new_data
FOR EACH STATEMENT
EXECUTE FUNCTION audit_bulk_update();
特性:
REFERENCING OLD TABLE:包含所有修改前的列REFERENCING NEW TABLE:包含所有修改後的列- 只支援
FOR EACH STATEMENT+AFTER - 效能優於等量的 ROW Trigger(資料只傳遞一次)
Constraint Trigger 與 DEFERRABLE
-- Constraint Trigger 可延遲到交易結束時驗證
CREATE CONSTRAINT TRIGGER trg_check_balance
AFTER INSERT OR UPDATE ON account_transfers
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION check_account_balance();
BEGIN;
SET CONSTRAINTS trg_check_balance IMMEDIATE; -- 臨時改為立即驗證
COMMIT;
效能最佳化
-- 查詢 Trigger 函式的效能統計
SELECT
funcname, calls, total_time, self_time,
round((total_time / calls)::numeric, 3) AS avg_ms
FROM pg_stat_user_functions
WHERE funcname LIKE '%trigger%'
ORDER BY total_time DESC;
最佳化建議:
- 使用
WHEN子句:在 Trigger 層級過濾,避免進入函式 - 使用
UPDATE OF:只監聽必要欄位 - 改用
FOR EACH STATEMENT:批次操作搭配 Transition Tables - 保持函式輕量:複雜邏輯考慮
NOTIFY+ 外部 Worker 非同步處理 - 批次操作前暫停:
ALTER TABLE ... DISABLE TRIGGER ALL
版本演進
| 版本 | 重要改進 |
|---|---|
| 9.3 | Event Triggers 正式引入 |
| 9.4 | DISABLE/ENABLE TRIGGER 語法 |
| 10 | Transition Tables(REFERENCING OLD/NEW TABLE) |
| 11 | CREATE OR REPLACE TRIGGER 語法 |
| 14 | BEFORE Trigger 中的 Transition Tables 支援改善 |
常見陷阱
無限遞迴
Trigger 函式中若再次修改觸發 Trigger 的資料表,會造成無限遞迴:
-- 使用 pg_trigger_depth() 偵測遞迴深度
CREATE OR REPLACE FUNCTION safe_update_func()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF pg_trigger_depth() > 1 THEN
RETURN NEW; -- 已在 Trigger 內,跳過
END IF;
-- 正常邏輯...
RETURN NEW;
END;
$$;
BEFORE Trigger 忘記回傳 NEW
BEFORE ROW Trigger 必須明確回傳 NEW,否則操作會被取消(等同回傳 NULL)。
AFTER Trigger 中修改 NEW 無效
NEW 在 AFTER Trigger 中是唯讀的,任何修改都不會反映到資料庫。需要修改資料請改用 BEFORE Trigger。
Trigger 中的副作用回滾
若交易回滾,Trigger 函式中的所有資料庫操作也會一同回滾(包括稽核日誌的 INSERT)。若需在回滾後仍保留日誌,需使用 dblink 自主交易或外部日誌系統。
總結
Trigger 是 PostgreSQL 自動化業務邏輯與資料完整性保障的核心機制:
- ROW Trigger 逐列執行,適合審計與欄位填值;STATEMENT Trigger 適合批次操作
- BEFORE 可修改 NEW 並取消操作;AFTER 適合副作用;INSTEAD OF 用於視圖
- WHEN 子句 與 UPDATE OF 精確過濾觸發條件,提升效能
- Event Trigger 攔截 DDL 操作,守護資料庫結構安全
- Transition Tables(PG10+)讓 STATEMENT Trigger 也能存取列資料
- 注意無限遞迴、BEFORE 回傳值、AFTER 唯讀 NEW 等常見陷阱
下一篇,我們將深入探討 PL/pgSQL 程式語言——PostgreSQL 內建的過程化語言,掌握伺服器端程式設計的核心能力。