行级触发器在copy或insert select时被调用n次,10万行即执行10万次,引发高io、锁等待与wal压力;应改用语句级触发器+过渡表,或严格按三步禁用再验证。

行级触发器在COPY或INSERT SELECT时被调用N次
PostgreSQL默认所有触发器都是FOR EACH ROW,哪怕一条COPY orders FROM '/data.csv'导入10万行,trg_audit_log也会执行10万次——不是1次,也不是10次,是精确的10万次。每次调用都涉及函数上下文初始化、WAL日志记录、锁获取与释放、可能的缓冲区刷脏。这些开销加起来远超逻辑本身。
常见错误现象包括:pg_stat_activity中看到大量状态为active且wait_event是Lock或ClientRead的会话;导入中途CPU不高但IO等待飙升;pg_stat_bgwriter中buffers_checkpoint突增。
- 别信“只写一行日志不慢”——10万次小WAL写比1次大WAL写更伤性能,尤其当
sync_commit = on时 -
pg_trigger_depth()在触发器内返回1,说明没嵌套,但不代表没放大——单层调用已足够致命 - MySQL和SQL Server也有同样问题,但PostgreSQL的过渡表(
NEW TABLE)提供了真正可行的替代路径
禁用触发器必须在事务外执行
很多人执行ALTER TABLE orders DISABLE TRIGGER ALL后发现导入依然卡顿,根本原因是命令被包裹在BEGIN; ... COMMIT;里。PostgreSQL明确报错:The context in which the current operation is executing does not allow for transaction control,但有些客户端(如psql默认自动提交关掉时)会静默失败。
正确姿势是三步分开:先连上数据库并确认上下文(SELECT current_database(), current_user),再单独执行禁用命令,最后再开新事务做导入。
- 验证是否生效:运行
SELECT tgname, tgenabled FROM pg_trigger WHERE tgrelid = 'orders'::regclass,tgenabled值应为D(disabled) -
DISABLE TRIGGER ALL不影响约束触发器(如外键、唯一索引),那些仍会检查,别误以为全关了 - 若只禁用某一个,用
ALTER TABLE orders DISABLE TRIGGER trg_status_sync,名字符合pg_trigger.tgname,大小写敏感
语句级触发器+过渡表才是批量场景正解
把FOR EACH ROW改成FOR EACH STATEMENT,配合NEW TABLE,就能把10万次调用压成1次。例如审计日志场景,原触发器逐行INSERT INTO audit_log VALUES (NEW.id, NEW.ts),新写法可聚合统计或批量插入。
注意:过渡表仅在触发器函数内可见,不能直接SELECT * FROM new_orders,必须在函数体里用FOR r IN SELECT * FROM new_orders LOOP ... END LOOP遍历。
- PostgreSQL 10+才支持,老版本只能靠禁用+补逻辑,或改应用层
-
WHEN子句可进一步过滤,比如WHEN (TG_OP = 'INSERT' AND NEW.status = 'paid'),避免无谓触发 - 函数内别对过渡表做
ORDER BY或LIMIT——它本质是临时关系,排序开销大且通常没必要
禁用期间的数据一致性风险常被低估
禁用触发器不是“暂停”,而是彻底跳过。如果触发器负责created_at自动填充、字段组合校验、或关联表计数更新,禁用后这些逻辑就完全丢失。重试导入时不会自动补偿,必须人工核查补全。
最容易被忽略的一点:禁用期间发生的任何其他DML(比如运维人员顺手UPDATE了一行),也不会触发逻辑。你只盯着导入任务,但表是活的。
- 业务强依赖触发器副作用时,禁用前必须评估数据修复成本——比如审计日志漏写10万条,对账系统可能直接告警
- 有CDC工具(如Debezium)监听该表,禁用触发器不影响其捕获,但若触发器本身发
pg_notify,这部分事件就永远消失了 - 导入脚本失败后,别急着
ENABLE TRIGGER,先确认当前表数据状态是否符合预期,再决定是否需要手动补触发逻辑











