PostgreSQL触发器高效写法需从类型、WHEN过滤、过渡表三层减法:行级必配WHEN(优化器静态跳过),语句级用REFERENCING过渡表替代逐行查表,慎用RETURN NULL避免事务不一致。

PostgreSQL中行级触发器默认比语句级慢一个数量级,尤其在批量操作时;真正高效的写法不是“优化函数体”,而是从触发器类型、WHEN过滤、过渡表使用这三层提前做减法。
行级触发器必须配WHEN子句,否则批量UPDATE可能全表扫描
没加WHEN的FOR EACH ROW触发器,哪怕函数里第一行就RETURN NULL,也会为每一行调用一次函数——开销已发生。而WHEN是在执行前由优化器静态判断,跳过整行不进触发流程。
-
WHEN只支持纯布尔表达式:允许NEW.status IS DISTINCT FROM OLD.status,禁止my_func(NEW)或子查询 - NULL要显式处理:
NEW.category = 'vip'对NULL行直接判false,应改用NEW.category IS NOT DISTINCT FROM 'vip'或拆条件 - 常见误写:
FOR EACH STATEMENT WHEN (NEW.id > 0)→ 直接报错,因为语句级不可访问NEW
语句级触发器用过渡表(transition table)替代逐行查表
想在AFTER UPDATE后统计“本次共更新了多少个status='active'的用户”,别在行级触发器里反复SELECT COUNT(*)——用REFERENCING NEW TABLE AS new_rows一次性拿到整批数据更高效。
- 仅AFTER语句级触发器支持
REFERENCING子句,BEFORE不支持 - 过渡表名是别名,不是真实表:
INSERT INTO audit_log SELECT id, 'update' FROM new_rows WHERE status = 'active' - 过渡表可JOIN其他表,但记得给JOIN字段建索引,否则
new_rows和users关联仍可能慢
BEFORE行级触发器慎用RETURN NULL,它会中断事务一致性
RETURN NULL在BEFORE行级触发器中表示“取消这一行的操作”,但不会回滚整个语句——比如UPDATE users SET name='x' WHERE id IN (1,2,3),若第2行触发器RETURN NULL,则id=1和3仍会更新成功,id=2静默丢弃。这容易导致业务逻辑断裂。
- 想阻止非法状态变更,优先用CHECK约束或应用层校验
- 真要用
RETURN NULL,必须配合明确的错误提示:RAISE EXCEPTION 'status transition not allowed',而不是静默失败 - 软删除场景下,
RETURN NULL+ INSERT到history表是可行模式,但需确保history表有主键或唯一约束防重复
触发器函数里避免SELECT、UPDATE非过渡表,尤其是无索引字段
触发器函数内任何对外部表的DML或SELECT,都会在每行(行级)或每次语句(语句级)执行一遍。如果函数里写了SELECT balance FROM accounts WHERE user_id = NEW.user_id,而accounts.user_id没索引,10万行更新就会触发10万次全表扫描。
- 把高频查询字段冗余进主表,或确保WHERE条件字段有索引
- 批量操作前先用
WITH查出所需数据集,再通过参数传入触发器函数(需改用EXECUTE FUNCTION f(...) USING ...调用方式) - 日志类副作用尽量异步化:写入一张轻量
log_buffer表,再由后台job合并落库,别卡在主事务里
最常被忽略的一点:触发器的执行时机(BEFORE/AFTER)和作用范围(ROW/STATEMENT)不是互斥选项,而是正交组合——你得先决定“要不要改数据”,再决定“按行还是按语句执行”,最后才选“要不要加WHEN过滤”。三者任意一层选错,性能和语义都可能偏移预期。

















