数据库迁移最容易制造的一种错觉,是“脚本执行成功,所以数据库对象也成功了”。表和索引通常比较显眼,触发器却常常躲过验收:名字还在,真正需要的动作已经不会发生。
我在一次迁移复核里就遇到过这种缺口。旧检查器确认目标表上存在同名触发器,主要接口测试也通过,于是迁移被判断为正常。继续检查才发现,这个结论缺了三件事:触发器是否启用、实际调用哪个函数、触发时机和事件是否仍与设计一致。
后来我把验收规则改成了更严格的一句话:对象存在,只能证明目录里有一行记录;只有身份、状态、语义和行为都一致,才能证明触发器可用。
为什么只查名称不够
PostgreSQL 的系统目录 pg_trigger 保存表和视图上的触发器。目录里不仅有名称 tgname,还包括所属关系、调用函数 tgfoid、触发条件位掩码 tgtype 和启用模式 tgenabled。[1]
这意味着下面几种状态都可能同时满足“同名触发器存在”:
- 触发器已被禁用;
- 触发器仍启用,但绑定到了旧函数或错误函数;
- 原本应在
AFTER INSERT执行,后来变成了别的事件或时机; - 原本是逐行触发,后来变成逐语句触发;
- 触发器挂在同名但错误 schema 的表上。
PostgreSQL 官方文档明确区分逐行与逐语句触发:FOR EACH ROW 会针对受影响的每一行调用,FOR EACH STATEMENT 则无论影响多少行只调用一次。[2] 名字相同但触发粒度不同,业务结果可能完全不同。
我现在把验收拆成五层
1. 对象身份:它到底挂在哪张表上
先绑定 schema、表、触发器名称,而不是只用一个全库模糊搜索。多 schema、临时迁移表和历史遗留表都可能产生同名对象。
2. 启用状态:目录里有,不等于会触发
tgenabled 会区分普通启用、禁用、仅副本模式和始终启用。官方目录中,D 表示禁用,O 表示在 origin 与 local 模式触发,另外还有 R 与 A。[1]
因此“不是 D”也不一定满足业务预期。验收必须写清楚期望模式,而不是把所有非空值都当成通过。
3. 函数绑定:确认真实调用目标
tgfoid 指向被调用函数。检查时应把它解析成带 schema 和参数签名的 regprocedure,避免同名函数、重载函数或旧 schema 造成误判。
4. 触发语义:核对事件、时机、粒度和条件
直接手工解码 tgtype 位掩码不利于人工复核。我更愿意同时读取 pg_get_triggerdef(),让验收证据明确显示:
BEFORE、AFTER还是INSTEAD OF;INSERT、UPDATE、DELETE或TRUNCATE;FOR EACH ROW还是FOR EACH STATEMENT;- 是否有列限制或
WHEN条件; - 最终调用的函数签名。
5. 真实行为:用最小写入证明副作用发生
目录检查只能证明定义符合预期,不能证明函数体在当前数据、权限和会话条件下真的完成业务动作。最后还需要一个可回滚的最小写入测试:插入或更新一条测试记录,验证触发器负责的审计行、派生字段、事件记录或其他副作用确实出现,然后回滚或删除测试数据。
一条可复用的目录查询
SELECT
n.nspname AS table_schema,
c.relname AS table_name,
t.tgname,
t.tgenabled,
t.tgfoid::regprocedure AS trigger_function,
pg_get_triggerdef(t.oid, true) AS trigger_definition
FROM pg_trigger AS t
JOIN pg_class AS c ON c.oid = t.tgrelid
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE NOT t.tgisinternal
AND n.nspname = :expected_schema
AND c.relname = :expected_table
AND t.tgname = :expected_trigger;
这条查询不是完整验收器,但它能把最容易漏掉的身份、状态、函数和定义放在同一份证据里。真正的自动化检查不应只判断“返回一行”,而要逐项比对期望值。
负例比正常路径更能证明验收器有效
我后来补上的关键测试,不是再跑一次正常迁移,而是故意制造错误:
- 临时禁用目标触发器,确认验收必须失败;
- 把函数绑定替换成一个同签名测试函数,确认不能只认名称;
- 改变触发事件或行/语句粒度,确认定义漂移会被发现;
- 恢复正确定义后执行最小写入,确认目录与真实行为同时通过。
PostgreSQL 本身允许通过 ALTER TABLE 启用或禁用触发器。[3] 正因为“对象存在但被禁用”是合法状态,验收器才必须把它当成一个专门负例。
迁移完成应有三份证据
现在我不会再把“迁移命令退出码为零”当作完成,而是至少保留三层证据:
- 迁移执行证据:版本记录、事务结果和错误输出正常;
- 目录形状证据:表、函数、触发器、约束及其关键属性符合当前合同;
- 业务行为证据:从一次受控写入到预期副作用的最小链路成立。
如果系统还存在逻辑复制、分区表或数据导入工具,还要额外核对 session_replication_role、分区克隆触发器和导入过程是否会暂时改变触发行为。这里不能把普通单机写入测试外推成所有部署形态都已验证。
适用边界
这套方法适合依赖触发器完成审计、派生数据、事件记录、同步辅助或完整性保护的 PostgreSQL 系统。若触发器只做可丢失的缓存更新,验收强度可以更轻;若它承担财务、权限或不可逆状态转换,则还应增加并发、回滚、异常和恢复测试。
触发器最危险的状态不是“完全不存在”,而是看起来还在,却已经不再执行正确动作。迁移验收真正要证明的,从来不是目录里有没有一个名字,而是数据库仍在履行那份行为合同。
本文来自真实数据库迁移复核,使用 AI 辅助结构化与校对;项目身份、表名、函数名、内部地址、配置和业务数据已移除。
来源与核验
- PostgreSQL 官方文档:pg_trigger 系统目录(核验于 2026-08-31)
- PostgreSQL 官方文档:CREATE TRIGGER(核验于 2026-08-31)
- PostgreSQL 官方文档:ALTER TABLE(核验于 2026-08-31)