# 触发器与约束触发器

LLMS 索引： [llms.txt](/llms.txt)

---

trigger 是“当某类事件发生时，在同一 PostgreSQL 事务中自动调用函数”的
对象。自动不等于异步，也不等于免费：

```text
original DML
  + trigger function SQL
  + trigger locks
  + trigger WAL
  + trigger errors
= caller latency and transaction outcome
```

设计 trigger 时，必须同时说明事件、粒度、时机、返回语义、权限、顺序、
批量成本和失败合同。

## 13.3.1 行级、语句级与 transition table {#item-13-3-1}

### 行级：一次处理一对 `OLD/NEW`

`FOR EACH ROW` 对每个受影响行调用一次：

```sql
CREATE TRIGGER a_guard_order_transition
BEFORE UPDATE OF status, version
ON shop_ch13.sales_order
FOR EACH ROW
EXECUTE FUNCTION shop_ch13.guard_order_transition();
```

一条更新三行的 SQL，会进入 trigger function 三次。PL/pgSQL trigger
function 通过特殊变量取得上下文：

| 变量 | 作用 |
|---|---|
| `TG_OP` | `INSERT` / `UPDATE` / `DELETE` / `TRUNCATE` |
| `TG_WHEN` | `BEFORE` / `AFTER` / `INSTEAD OF` |
| `TG_LEVEL` | `ROW` / `STATEMENT` |
| `TG_TABLE_SCHEMA`、`TG_TABLE_NAME` | 触发关系 |
| `TG_ARGV[]` | `CREATE TRIGGER` 传入的文本参数 |
| `OLD` | UPDATE/DELETE 的旧行 |
| `NEW` | INSERT/UPDATE 的新行 |

本章 guard 比较：

```plpgsql
IF NEW.status IS DISTINCT FROM OLD.status THEN
    IF NOT shop_ch13.allowed_transition(
               OLD.status,
               NEW.status
           ) THEN
        RAISE ... ERRCODE = 'P3613';
    END IF;

    IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
        RAISE ... ERRCODE = 'P3615';
    END IF;
END IF;
```

这是行级 trigger 的合适形状：判断只依赖一对旧、新行和纯 transition
matrix，没有为每行扫描整张表。

### 语句级：一次处理整个命令

`FOR EACH STATEMENT` 对一条符合事件的语句调用一次，即使最终影响零行也可能
调用。它没有单行 `OLD/NEW`。如果需要看到受影响集合，使用 transition
relations：

```sql
CREATE TRIGGER z_audit_order_transition
AFTER UPDATE
ON shop_ch13.sales_order
REFERENCING
    OLD TABLE AS old_rows
    NEW TABLE AS new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION shop_ch13.audit_order_transition();
```

trigger function 将它们当只读关系使用：

```sql
INSERT INTO shop_ch13.order_history (...)
SELECT ...
FROM old_rows
JOIN new_rows USING (order_id)
WHERE old_rows.status IS DISTINCT FROM new_rows.status;
```

随后写一条 statement audit：

```sql
INSERT INTO shop_ch13.statement_audit (...)
SELECT
    pg_current_xact_id(),
    actor,
    session_user,
    count(*)::integer,
    array_agg(new_rows.order_id ORDER BY new_rows.order_id),
    statement_timestamp()
FROM old_rows
JOIN new_rows USING (order_id)
WHERE old_rows.status IS DISTINCT FROM new_rows.status;
```

实验中：

```sql
UPDATE shop_ch13.sales_order
SET status = 'canceled', version = version + 1
WHERE order_id IN (105, 106, 107);
```

得到：

```text
affected_count=3
order_ids={105,106,107}
statement_audit rows added=1
order_history rows added=3
```

这比 row trigger 内每行再做聚合更符合集合模型。

### transition table 的边界

transition relations：

- 只用于 `AFTER` trigger；
- 捕获一条原始 SQL 对该关系形成的旧/新行集合；
- 可以给 `AFTER ROW` 或 `AFTER STATEMENT` trigger 使用；
- 不能与 constraint trigger 结合；
- PostgreSQL 当前不允许带 transition relations 的 UPDATE trigger 同时使用
  `UPDATE OF column_list`；
- 会物化变更集合，因此大批量语句要评估内存、临时文件与延迟。

它们不是跨事务 change stream，也不是 logical decoding 的替代品。

### constraint trigger

用户定义的 constraint trigger：

- 使用 `CREATE CONSTRAINT TRIGGER`；
- 必须是 plain table 上的 `AFTER ROW` trigger；
- 可声明 `DEFERRABLE` 和 `INITIALLY DEFERRED`；
- 可被 `SET CONSTRAINTS` 调整到事务末尾或立即检查；
- 同样在当前事务中执行。

本章分别挂在订单与支付表：

```sql
CREATE CONSTRAINT TRIGGER z_validate_paid_order
AFTER INSERT OR UPDATE OF status, total_minor
ON shop_ch13.sales_order
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION shop_ch13.validate_paid_order();

CREATE CONSTRAINT TRIGGER z_validate_payment
AFTER INSERT OR UPDATE OR DELETE
ON shop_ch13.payment
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION shop_ch13.validate_paid_order();
```

command function 先插入 captured payment，再把订单改为 paid。两个中间瞬间
分别不满足最终关系，但提交点满足：

```text
inside transaction:
  payment captured + order created   -- 暂时不一致
  payment captured + order paid      -- 最终一致
COMMIT:
  deferred checks run                -- 通过
```

只把订单改成 paid，则提交点返回 `P3614`，订单更新、history 和 statement
audit 全部回滚。

### 延迟不等于并发安全

constraint trigger 能检查当前事务看到的最终状态，却不会自动选择正确锁。
例如两个事务并发改变同一聚合的不同明细，如果没有共同仲裁行、适当锁或
serializable 协议，双方可能基于不完整视图判断。

本章 `capture_payment()` 先：

```sql
SELECT ...
FROM shop_ch13.sales_order
WHERE order_id = p_order_id
FOR UPDATE;
```

同一订单的支付命令在订单行上串行化。这是显式并发设计，不是 deferred
关键字赠送的能力。复杂跨行断言必须单独做并发测试。

## 13.3.2 BEFORE、AFTER 与 INSTEAD OF {#item-13-3-2}

### `BEFORE`：拒绝、规范化或改写当前行

row-level `BEFORE` 在行写入前运行，可以：

- 检查 `OLD/NEW`；
- 修改 INSERT/UPDATE 的 `NEW`；
- 返回 `NEW` 继续；
- 返回 `NULL` 跳过该行。

本章在合法状态变化时统一：

```plpgsql
NEW.updated_at := statement_timestamp();
RETURN NEW;
```

返回 `NULL` 会让当前行操作被静默跳过，还会影响后续 row trigger 和命令
影响行数。除非“跳过”本身就是明确合同，通常应抛出带 SQLSTATE 的错误，
而不是让调用方误以为写入成功。

row-level `BEFORE DELETE` 返回 `OLD` 才能继续删除。trigger function 若要
复用于多个事件，必须逐个写清返回规则。

### `UPDATE OF` 看 SET 列表，不看最终差异

```sql
BEFORE UPDATE OF status
```

在 `status` 出现在 `SET` 目标列表时触发，即使：

```sql
SET status = status
```

它也会触发。反过来，另一个 `BEFORE` trigger 修改 `NEW.status` 并不会让原本
未列出 `status` 的 column-specific trigger 补触发。

真正判断值是否变化要使用：

```sql
WHEN (OLD.status IS DISTINCT FROM NEW.status)
```

或在 body 内判断。`IS DISTINCT FROM` 对 NULL 有确定语义。

### `AFTER`：观察已完成变化

`AFTER` 运行时：

- 当前行操作和即时约束已经完成；
- 其他 trigger 造成的变化可见；
- 返回值被忽略；
- 抛错仍会回滚原语句和事务。

适合：

- 同事务 audit/history；
- 基于最终行值派生另一张表；
- transition table 集合处理；
- deferred constraint check。

不适合远端 I/O，原因仍是它属于原事务同步延迟。

### `INSTEAD OF`：为 view 定义写语义

`INSTEAD OF` 只用于 view 的 row trigger。它收到 view 的 `OLD/NEW`，由
trigger function 决定对底表做什么。

先确认 view 是否已经自动可更新。对简单单表 view，PostgreSQL 可以自动把
DML 映射到底表；不需要 trigger。只有复杂 join、聚合或有意设计的 view
command surface 才考虑 `INSTEAD OF`。

示意：

```sql
CREATE VIEW order_command AS
SELECT order_id, status, version
FROM private_order;

CREATE TRIGGER route_order_command
INSTEAD OF UPDATE ON order_command
FOR EACH ROW
EXECUTE FUNCTION route_order_command();
```

trigger function 必须：

- 定义哪些 view 列可写；
- 拒绝其余列；
- 处理并发 version；
- 返回符合 view 形状的 `NEW`；
- 给出稳定 SQLSTATE；
- 保持权限边界。

如果实际意图是一个显式命令，`SELECT transition_order(...)` 往往比伪装成
view UPDATE 更清楚。

### 同类 trigger 的顺序

同一表、同一事件、同一时机的多个 trigger 按名字字母顺序执行。这个事实可
用于确定性，但不应构建脆弱流水线：

```text
a_normalize
b_validate
c_audit
```

一旦正确性依赖命名，重命名、extension trigger 或迁移合并都可能改变行为。
更稳妥的选择：

- 合并强耦合逻辑到一个 trigger function；
- 让各 trigger 彼此独立、幂等；
- 用约束表达真正的最终条件；
- 在目录测试中冻结 trigger inventory。

官方顺序与语义见
[CREATE TRIGGER](https://www.postgresql.org/docs/18/sql-createtrigger.html)。

### 运行角色

trigger 与触发语句属于同一事务。PostgreSQL 18 对 queued trigger 明确保留
排队时的 active role；若 trigger function 是 `SECURITY DEFINER`，则以
function owner 执行。14–17 的延迟触发角色细节必须按目标版本验证。

本章把会写保护表的 trigger function 显式设为 `SECURITY DEFINER`，固定
`search_path`，撤销应用对内部函数的 `EXECUTE`。这样权限意图不依赖嵌套
command function 返回后的角色状态。

创建 trigger 时，创建者需要表的 `TRIGGER` privilege 和 trigger function
的 `EXECUTE` privilege。运行态权限设计还必须结合 function 的
`SECURITY INVOKER/DEFINER`。

## 13.3.3 递归、顺序、批量写入与隐藏成本 {#item-13-3-3}

### trigger 是写路径的一部分

评估成本不要只看原 SQL：

```text
rows affected
× row triggers per row
× SQL issued per trigger
+ statement triggers
+ deferred trigger queue
+ indexes/WAL on derived tables
+ contention introduced by trigger queries
```

一条 `COPY` 或无过滤 `UPDATE` 可能把平时每次一行的隐藏成本放大百万倍。

### 递归不会自动终止

trigger function 再写同一表，可能再次触发自己：

```text
UPDATE t
  -> trigger
       -> UPDATE t
            -> trigger
                 -> ...
```

PostgreSQL 允许 cascading trigger；终止责任在设计者。

`pg_trigger_depth()` 能告诉当前嵌套深度，适合诊断。把：

```plpgsql
IF pg_trigger_depth() > 1 THEN
    RETURN NEW;
END IF;
```

当作主要正确性机制往往掩盖模型问题：另一个合法 trigger 链也可能让深度
大于一，而真正递归仍可能从其他路径进入。优先：

- 不在 trigger 中更新触发表；
- 在 `BEFORE` 中直接修改 `NEW`；
- 将派生写放到不同关系；
- 让操作幂等并用明确状态终止；
- 对递归反例做受控测试。

### `ON CONFLICT` 与 MERGE 会组合多个事件

`INSERT ... ON CONFLICT DO UPDATE` 可能先运行 row-level `BEFORE INSERT`，
冲突后再运行 `BEFORE UPDATE`。statement-level INSERT/UPDATE trigger 也有
定义好的组合顺序，即使 UPDATE 分支最终没有影响行。

因此：

- INSERT normalization 必须考虑其结果会进入 `EXCLUDED`；
- 两组 trigger 不应重复不可幂等副作用；
- 测试要覆盖 insert 成功、conflict update、conflict no-op；
- 不能从“最终是 UPDATE”推断只执行 UPDATE trigger。

PostgreSQL 15+ 的 `MERGE` 同样需要按实际 action 路径测试，不凭类比；
14 环境没有该语句。

### 每行查询导致 N+1

反模式：

```plpgsql
-- 每个更新行都扫描一次 history
SELECT count(*)
INTO n
FROM order_history
WHERE order_id = NEW.order_id;
```

批量更新 N 行就产生 N 次查询。替代方案：

- 用原生约束；
- 在原 UPDATE 中 join/CTE；
- 用 transition table 一次集合处理；
- 为不可避免的 lookup 建正确索引；
- 把可延后的分析移到异步 worker。

### 审计不是“复制整行就完成”

可靠 audit 要定义：

- 记录业务变化还是所有 UPDATE；
- old/new 哪些列，是否包含敏感数据；
- actor 是认证主体、数据库 session 还是服务；
- request/trace ID 如何传递和防伪；
- transaction ID 与 statement 时间是什么语义；
- 审计表谁能改、保留多久、如何分区；
- 失败时是否必须与主写入一起回滚。

本章保存 `actor` 与 `session_actor`，但 actor 来自受控 command function 设置
的 transaction-local custom setting。由于应用没有底表 DML，不能仅靠
设置该值伪造一次写入；真正系统还要把 actor 与认证层可信上下文绑定。

### 分区表的额外行为

在 partitioned table 上创建 row trigger，会在已有和后续
partition 上建立 clone trigger。attach/detach、同名冲突和 major version
行为都需要目录测试。行因更新 partition key 被移动时，源 partition 的
DELETE 与目标 partition 的 INSERT trigger 也会参与。

不要只在 root table 的 `\d` 输出上推断所有 partition 的实际 trigger。

### 禁用 trigger 是高风险动作

`ALTER TABLE ... DISABLE TRIGGER`、replication role 或恢复路径可能绕开
业务 trigger。批量导入前“先关 trigger 提速”意味着暂时取消不变量，必须
有：

- 明确授权与维护窗口；
- 隔离写入口；
- 导入后全量验证；
- 恢复 trigger 的 finally 路径；
- 失败时数据修复方案；
- 目录与配置证据。

若规则应是不可绕过的约束，优先用原生 constraint，而不是依赖所有人永不
禁用 trigger。

### 从目录取得事实

[trigger-catalog.sql](/labs/ch13/trigger-catalog.sql) 读取：

```sql
SELECT
    c.relname,
    t.tgname,
    t.tgfoid::regprocedure,
    t.tgdeferrable,
    t.tginitdeferred,
    t.tgoldtable,
    t.tgnewtable,
    pg_get_triggerdef(t.oid, true)
FROM pg_trigger AS t
JOIN pg_class AS c ON c.oid = t.tgrelid
WHERE NOT t.tgisinternal;
```

实验冻结四个 user trigger：

```text
payment:
  z_validate_payment          AFTER ROW, deferred

sales_order:
  a_guard_order_transition    BEFORE ROW
  z_audit_order_transition    AFTER STATEMENT, old_rows/new_rows
  z_validate_paid_order       AFTER ROW, deferred
```

`tgisinternal` 过滤了外键等系统内部 trigger；不要把它们误认成“没有 trigger”。
用户定义 constraint trigger 还会在 `pg_constraint` 中留下 `contype='t'`
记录。

### 发布检查表

1. 为什么不是原生 constraint 或原 SQL？
2. event、row/statement、timing 与返回语义是什么？
3. 零行、单行、批量和 `ON CONFLICT` 路径是否测试？
4. transition table 会物化多少数据？
5. deferred check 的锁与并发协议是什么？
6. 有没有写触发表或递归链？
7. 同类 trigger 是否依赖名字顺序？
8. 运行角色和 definer owner 是否最小权限？
9. 错误是否有稳定 SQLSTATE？
10. bulk load、partition、复制和恢复行为是否明确？
11. `pg_trigger` inventory 是否进入 release gate？
12. 回退时是撤销新调用、禁用、替换还是删除，顺序是什么？

trigger 只有在这些问题都能回答时，才称得上数据库护栏。

---

[上一节：SQL 与 PL/pgSQL 函数](../02/) · [返回本章目录](../) · [下一节：过程、任务与事务控制](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
