13.3 触发器与约束触发器
trigger 是“当某类事件发生时,在同一 PostgreSQL 事务中自动调用函数”的 对象。自动不等于异步,也不等于免费:
original DML
+ trigger function SQL
+ trigger locks
+ trigger WAL
+ trigger errors
= caller latency and transaction outcome设计 trigger 时,必须同时说明事件、粒度、时机、返回语义、权限、顺序、 批量成本和失败合同。
13.3.1 行级、语句级与 transition table
行级:一次处理一对 OLD/NEW
FOR EACH ROW 对每个受影响行调用一次:
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 比较:
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:
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 将它们当只读关系使用:
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:
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;实验中:
UPDATE shop_ch13.sales_order
SET status = 'canceled', version = version + 1
WHERE order_id IN (105, 106, 107);得到:
affected_count=3
order_ids={105,106,107}
statement_audit rows added=1
order_history rows added=3这比 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调整到事务末尾或立即检查; - 同样在当前事务中执行。
本章分别挂在订单与支付表:
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。两个中间瞬间 分别不满足最终关系,但提交点满足:
inside transaction:
payment captured + order created -- 暂时不一致
payment captured + order paid -- 最终一致
COMMIT:
deferred checks run -- 通过只把订单改成 paid,则提交点返回 P3614,订单更新、history 和 statement
audit 全部回滚。
延迟不等于并发安全
constraint trigger 能检查当前事务看到的最终状态,却不会自动选择正确锁。 例如两个事务并发改变同一聚合的不同明细,如果没有共同仲裁行、适当锁或 serializable 协议,双方可能基于不完整视图判断。
本章 capture_payment() 先:
SELECT ...
FROM shop_ch13.sales_order
WHERE order_id = p_order_id
FOR UPDATE;同一订单的支付命令在订单行上串行化。这是显式并发设计,不是 deferred 关键字赠送的能力。复杂跨行断言必须单独做并发测试。
13.3.2 BEFORE、AFTER 与 INSTEAD OF
BEFORE:拒绝、规范化或改写当前行
row-level BEFORE 在行写入前运行,可以:
- 检查
OLD/NEW; - 修改 INSERT/UPDATE 的
NEW; - 返回
NEW继续; - 返回
NULL跳过该行。
本章在合法状态变化时统一:
NEW.updated_at := statement_timestamp();
RETURN NEW;返回 NULL 会让当前行操作被静默跳过,还会影响后续 row trigger 和命令
影响行数。除非“跳过”本身就是明确合同,通常应抛出带 SQLSTATE 的错误,
而不是让调用方误以为写入成功。
row-level BEFORE DELETE 返回 OLD 才能继续删除。trigger function 若要
复用于多个事件,必须逐个写清返回规则。
UPDATE OF 看 SET 列表,不看最终差异
BEFORE UPDATE OF status在 status 出现在 SET 目标列表时触发,即使:
SET status = status它也会触发。反过来,另一个 BEFORE trigger 修改 NEW.status 并不会让原本
未列出 status 的 column-specific trigger 补触发。
真正判断值是否变化要使用:
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。
示意:
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 按名字字母顺序执行。这个事实可 用于确定性,但不应构建脆弱流水线:
a_normalize
b_validate
c_audit一旦正确性依赖命名,重命名、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:
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 再写同一表,可能再次触发自己:
UPDATE t
-> trigger
-> UPDATE t
-> trigger
-> ...PostgreSQL 允许 cascading trigger;终止责任在设计者。
pg_trigger_depth() 能告诉当前嵌套深度,适合诊断。把:
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
反模式:
-- 每个更新行都扫描一次 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。
从目录取得事实
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:
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, deferredtgisinternal 过滤了外键等系统内部 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 函数 · 返回本章目录 · 下一节:过程、任务与事务控制 · 查看全书目录 · 查看索引中心