觸發器與事件觸發器:PostgreSQL 的自動化執行機制 | PostgreSQL

2026/06/30
觸發器與事件觸發器: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 變數

NEWOLD 是 ROW Trigger 中的特殊變數,型別為 RECORD

操作OLDNEW
INSERTNULL(不存在)新插入的列
UPDATE修改前的列修改後的列
DELETE被刪除的列NULL(不存在)
TRUNCATE不適用不適用

STATEMENT Trigger 中 NEWOLD 均不可用。若需批次操作的列資料,請使用 Transition TablesREFERENCING OLD TABLE / NEW TABLE,PG10+)。

TG_* 特殊變數

變數型別內容
TG_OPtext'INSERT''UPDATE''DELETE''TRUNCATE'
TG_NAMEnameTrigger 名稱
TG_TABLE_NAMEname觸發的資料表名稱
TG_TABLE_SCHEMAname觸發的 Schema 名稱
TG_WHENtext'BEFORE''AFTER''INSTEAD OF'
TG_LEVELtext'ROW''STATEMENT'
TG_NARGSintegerCREATE 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 時:

  1. BEFORE 先於 AFTER
  2. 相同時機的多個 Trigger,依名稱字母順序 執行
  3. 建議在名稱前加數字前綴控制順序:10_validate_20_audit_
  4. Constraint Trigger 總是在 AFTER ROW 之後執行,且可延遲(DEFERRABLE
  5. 若 BEFORE ROW Trigger 回傳 NULL,該列的後續操作(含 AFTER Trigger)均被跳過

Event Trigger(DDL 攔截)

Event Trigger 是 PG9.3 引入的特殊 Trigger,可攔截 DDL 操作:

事件觸發時機
ddl_command_startDDL 指令開始前
ddl_command_endDDL 指令完成後
sql_dropDROP 操作執行後,物件被移除前
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;

最佳化建議:

  1. 使用 WHEN 子句:在 Trigger 層級過濾,避免進入函式
  2. 使用 UPDATE OF:只監聽必要欄位
  3. 改用 FOR EACH STATEMENT:批次操作搭配 Transition Tables
  4. 保持函式輕量:複雜邏輯考慮 NOTIFY + 外部 Worker 非同步處理
  5. 批次操作前暫停ALTER TABLE ... DISABLE TRIGGER ALL

版本演進

版本重要改進
9.3Event Triggers 正式引入
9.4DISABLE/ENABLE TRIGGER 語法
10Transition Tables(REFERENCING OLD/NEW TABLE
11CREATE OR REPLACE TRIGGER 語法
14BEFORE 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 內建的過程化語言,掌握伺服器端程式設計的核心能力。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。