13.3 触发器与约束触发器
trigger 是“当某类事件发生时,在同一 PostgreSQL 事务中自动调用函数”的 对象。自动不等于异步,也不等于免费:
设计 trigger 时,必须同时说明事件、粒度、时机、返回语义、权限、顺序、 批量成本和失败合同。
13.3.1 行级、语句级与 transition table
行级:一次处理一对 OLD/NEW
FOR EACH ROW 对每个受影响行调用一次:
一条更新三行的 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 比较:
这是行级 trigger 的合适形状:判断只依赖一对旧、新行和纯 transition matrix,没有为每行扫描整张表。
语句级:一次处理整个命令
FOR EACH STATEMENT 对一条符合事件的语句调用一次,即使最终影响零行也可能
调用。它没有单行 OLD/NEW。如果需要看到受影响集合,使用 transition
relations:
trigger function 将它们当只读关系使用:
随后写一条 statement audit:
实验中:
得到:
这比 row trigger 内每行再做聚合更符合集合模型。
transition table 的边界
transition relations:
- 只用于
AFTERtrigger; - 捕获一条原始 SQL 对该关系形成的旧/新行集合;
- 可以给
AFTER ROW或AFTER STATEMENTtrigger 使用; - 不能与 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 ROWtrigger; - 可声明
DEFERRABLE和INITIALLY DEFERRED; - 可被
SET CONSTRAINTS调整到事务末尾或立即检查; - 同样在当前事务中执行。
本章分别挂在订单与支付表:
command function 先插入 captured payment,再把订单改为 paid。两个中间瞬间 分别不满足最终关系,但提交点满足:
只把订单改成 paid,则提交点返回 P3614,订单更新、history 和 statement
audit 全部回滚。
延迟不等于并发安全
constraint trigger 能检查当前事务看到的最终状态,却不会自动选择正确锁。 例如两个事务并发改变同一聚合的不同明细,如果没有共同仲裁行、适当锁或 serializable 协议,双方可能基于不完整视图判断。
本章 capture_payment() 先:
同一订单的支付命令在订单行上串行化。这是显式并发设计,不是 deferred 关键字赠送的能力。复杂跨行断言必须单独做并发测试。
13.3.2 BEFORE、AFTER 与 INSTEAD OF
BEFORE:拒绝、规范化或改写当前行
row-level BEFORE 在行写入前运行,可以:
- 检查
OLD/NEW; - 修改 INSERT/UPDATE 的
NEW; - 返回
NEW继续; - 返回
NULL跳过该行。
本章在合法状态变化时统一:
返回 NULL 会让当前行操作被静默跳过,还会影响后续 row trigger 和命令
影响行数。除非“跳过”本身就是明确合同,通常应抛出带 SQLSTATE 的错误,
而不是让调用方误以为写入成功。
row-level BEFORE DELETE 返回 OLD 才能继续删除。trigger function 若要
复用于多个事件,必须逐个写清返回规则。
UPDATE OF 看 SET 列表,不看最终差异
在 status 出现在 SET 目标列表时触发,即使:
它也会触发。反过来,另一个 BEFORE trigger 修改 NEW.status 并不会让原本
未列出 status 的 column-specific trigger 补触发。
真正判断值是否变化要使用:
或在 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。
示意:
trigger function 必须:
- 定义哪些 view 列可写;
- 拒绝其余列;
- 处理并发 version;
- 返回符合 view 形状的
NEW; - 给出稳定 SQLSTATE;
- 保持权限边界。
如果实际意图是一个显式命令,SELECT transition_order(...) 往往比伪装成
view UPDATE 更清楚。
同类 trigger 的顺序
同一表、同一事件、同一时机的多个 trigger 按名字字母顺序执行。这个事实可 用于确定性,但不应构建脆弱流水线:
一旦正确性依赖命名,重命名、extension trigger 或迁移合并都可能改变行为。 更稳妥的选择:
- 合并强耦合逻辑到一个 trigger function;
- 让各 trigger 彼此独立、幂等;
- 用约束表达真正的最终条件;
- 在目录测试中冻结 trigger inventory。
官方顺序与语义见 CREATE TRIGGER。
运行角色
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 递归、顺序、批量写入与隐藏成本
trigger 是写路径的一部分
评估成本不要只看原 SQL:
一条 COPY 或无过滤 UPDATE 可能把平时每次一行的隐藏成本放大百万倍。
递归不会自动终止
trigger function 再写同一表,可能再次触发自己:
PostgreSQL 允许 cascading trigger;终止责任在设计者。
pg_trigger_depth() 能告诉当前嵌套深度,适合诊断。把:
当作主要正确性机制往往掩盖模型问题:另一个合法 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
反模式:
批量更新 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。
从目录取得事实
实验冻结四个 user trigger:
tgisinternal 过滤了外键等系统内部 trigger;不要把它们误认成“没有 trigger”。
用户定义 constraint trigger 还会在 pg_constraint 中留下 contype='t'
记录。
发布检查表
- 为什么不是原生 constraint 或原 SQL?
- event、row/statement、timing 与返回语义是什么?
- 零行、单行、批量和
ON CONFLICT路径是否测试? - transition table 会物化多少数据?
- deferred check 的锁与并发协议是什么?
- 有没有写触发表或递归链?
- 同类 trigger 是否依赖名字顺序?
- 运行角色和 definer owner 是否最小权限?
- 错误是否有稳定 SQLSTATE?
- bulk load、partition、复制和恢复行为是否明确?
pg_triggerinventory 是否进入 release gate?- 回退时是撤销新调用、禁用、替换还是删除,顺序是什么?
trigger 只有在这些问题都能回答时,才称得上数据库护栏。
上一节:SQL 与 PL/pgSQL 函数 · 返回本章目录 · 下一节:过程、任务与事务控制 · 查看全书目录 · 查看索引中心