行级触发器在批量写入时拖慢速度,因其默认FOR EACH ROW对每行单独调用函数,10万行即执行10万次,上下文切换等开销快速累积;改用FOR EACH STATEMENT配合NEW TABLE可一次性处理全部行,显著提升吞吐。

为什么行级触发器在批量写入时会拖慢速度
PostgreSQL 默认的 FOR EACH ROW 触发器,会在每一条被 INSERT/UPDATE/DELETE 的数据上单独调用一次函数。插入 10 万行,就调用 10 万次——哪怕函数体只有一行 RAISE NOTICE,上下文切换、内存分配、执行计划重编译的开销也会快速累积。实测中,带简单逻辑的行级触发器可让批量插入吞吐下降 60% 以上。
改用 FOR EACH STATEMENT + NEW TABLE 是关键一步
语句级触发器只在整个 SQL 语句执行完毕后触发一次,配合 PostgreSQL 10+ 的过渡表(NEW TABLE / OLD TABLE),你就能一次性拿到所有受影响行的数据,做聚合、校验或异步分发,而无需逐行处理。
示例:给日志表自动补全统计字段,不逐行查
CREATE OR REPLACE FUNCTION log_batch_summary()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO log_summary(day, count, total_size)
SELECT CURRENT_DATE, COUNT(*), SUM(LENGTH(payload))
FROM NEW; -- 这里 NEW 是一个临时表,含本次 INSERT 所有行
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
<p>CREATE TRIGGER trg_log_summary
AFTER INSERT ON logs
REFERENCING NEW TABLE AS NEW
FOR EACH STATEMENT
EXECUTE FUNCTION log_batch_summary();注意点:
-
REFERENCING NEW TABLE AS NEW必须显式声明,否则NEW在语句级不可用 - 函数返回
NULL(不是NEW),因为语句级触发器不参与行修改 -
NEW TABLE是只读临时表,不能UPDATE或DELETE它 - 若触发器需访问基表其他字段(如关联用户信息),必须显式
JOIN,且关联字段要建索引
哪些场景不适合语句级触发器
不是所有逻辑都能平滑迁移到语句级。以下情况容易踩坑:
- 需要对每一行做独立判断并修改(如动态生成 UUID、计算行级 hash)——语句级无法返回修改后的行
- 触发器内执行
INSERT INTO other_table VALUES (NEW.*)且依赖主键自增顺序——语句级中NEW表无插入顺序保证 - 原逻辑含
RAISE EXCEPTION做单行校验(如邮箱格式)——语句级失败会回滚整批,而非仅当前行 - 使用了
NEW.col直接引用字段但未在SELECT中显式列出——语句级下必须从NEW TABLE显式查询,隐式字段访问报错
搭配批量写入路径才能真正见效
即使用了语句级触发器,如果底层还是用 100 万行拼成一条 INSERT ... VALUES (...),(...),...,PostgreSQL 解析器仍可能 OOM 或 WAL 写放大。真正快的组合是:
- 数据来自文件 → 用
COPY导入(触发器默认不触发,需加WITH (ON COMMIT PRESERVE ROWS)或改用INSTEAD OF) - 数据来自应用层 → 分批 2000–5000 行,每批用
BEGIN; INSERT ...; COMMIT;,再配语句级触发器 - 触发器函数内避免
SELECT ... FROM huge_table WHERE x IN (SELECT id FROM NEW)这类嵌套查询——应先CREATE TEMP TABLE tmp_new AS SELECT * FROM NEW再 JOIN,减少重复扫描 - 若触发器需写外部系统(如 Kafka),务必异步化(通过
pg_notify+ 后台 worker),否则事务卡住整个批次
最易被忽略的一点:语句级触发器不会自动规避索引维护或 WAL 开销——它只是减少了函数调用次数。真正的性能拐点,永远在 COPY 和分批事务控制上,触发器只是锦上添花,不是雪中送炭。


















