PostgreSQL 14中触发器函数必须声明RETURNS TRIGGER且返回NEW、OLD或NULL;禁止使用void/text返回类型,动态SQL须用format('%I', identifier)安全拼接标识符,NEW/OLD字段访问需依TG_OP分支判断,行级触发器在批量操作中性能开销显著。

触发器函数必须返回 TRIGGER 类型
PostgreSQL 14 不允许用普通 RETURNS void 或 RETURNS text 的函数当触发器——创建触发器时会直接报错:ERROR: function must return type trigger。这是硬性类型检查,不是警告。
实操建议:
- 函数定义开头必须写
RETURNS TRIGGER(大小写不敏感,但推荐全大写) - 函数体末尾必须有
RETURN NEW、RETURN OLD或RETURN NULL,不能遗漏 -
RETURN NEW用于BEFORE INSERT/UPDATE,让修改后的行继续执行原操作;RETURN NULL表示跳过当前行(常用于校验拦截)
动态 SQL 中安全拼接表名和字段名
在函数里写 EXECUTE 'INSERT INTO ' || TG_TABLE_NAME || ' ...' 是危险且错误的。PostgreSQL 14 会解析字符串字面量,把 TG_TABLE_NAME 当作未定义变量或语法错误处理,更严重的是引入 SQL 注入风险。
正确做法是用 format() + %I 占位符:
-
%I自动加双引号并转义,把任意输入当作合法标识符(表名、列名、schema 名) -
%L用于字符串/数值字面量,等价于quote_literal(),自动处理NULL和单引号 - 禁止混用:
EXECUTE format('SELECT %I FROM %I', 'name', TG_TABLE_NAME) USING ...—— 这里USING只能传值,不能传标识符
NEW 和 OLD 在不同事件中不可直接访问
在 INSERT 触发器里读 OLD.id,或在 DELETE 里读 NEW.created_at,运行时报错:record "old" has no field "id"。PostgreSQL 14 不做静默兼容,而是严格按事件暴露记录变量。
实操建议:
- 用
TG_OP显式分支:IF TG_OP = 'UPDATE' THEN ... END IF; - 避免无条件引用:
OLD.updated_at只在UPDATE和DELETE中有效 - 需要统一取值时,用
COALESCE(NEW.field, OLD.field),但要确认语义合理(比如审计时间字段)
FOR EACH ROW 触发器性能容易被低估
PostgreSQL 14 默认是 FOR EACH ROW,一条影响 50 万行的 UPDATE 就会调用触发器函数 50 万次。哪怕函数体只有一行赋值,开销也远超预期——特别是里面还嵌套了 EXECUTE、JSON 构造或跨 schema 函数调用。
关键细节:
- 多个触发器共存时,执行顺序由创建时间决定(
pg_trigger.tgrelid+pg_trigger.tgname排序),无法靠BEFORE/AFTER控制依赖 - 若逻辑可批量处理(如日志归档),优先考虑
FOR EACH STATEMENT+ 手动查变更行,而非逐行触发 - 测试时别只用单行 INSERT 验证功能,一定要压测真实批量场景
最常被忽略的一点:触发器函数里调用的任何其他函数,如果没带 schema 前缀(比如直接写 normalize_phone(NEW.phone)),在 search_path 不含对应 schema 时就会报 function does not exist——而这个错误在开发环境可能碰巧不出现,上线后才爆发。

















