PostgreSQL 14强制触发器函数声明RETURNS TRIGGER且末尾RETURN NEW/OLD/NULL,否则CREATE TRIGGER直接报错;动态SQL须用format('%I', identifier)安全拼接标识符,NEW/OLD访问需依TG_OP分支判断,行级触发器批量操作性能开销显著。

触发器函数必须返回 TRIGGER 类型
PostgreSQL 14 强制要求触发器函数声明 RETURNS TRIGGER,否则创建触发器时直接报错:ERROR: function must return type trigger。这不是警告,是硬性类型检查。
常见错误:写成 RETURNS void 或 RETURNS text,哪怕函数体里有 RETURN 语句也无效。
- 函数定义开头必须明确写
RETURNS TRIGGER(大小写不敏感,但推荐全大写) - 函数体末尾必须有
RETURN NEW、RETURN OLD或RETURN NULL,不能遗漏 -
RETURN NEW用于BEFORE INSERT/UPDATE,让修改后的行继续执行原操作 -
RETURN NULL表示跳过当前行(常用于校验失败拦截)
动态 SQL 中拼接表名和字段名必须用 format('%I', ...)
在触发器函数里写 EXECUTE 'INSERT INTO ' || TG_TABLE_NAME || ' ...' 是危险且错误的——TG_TABLE_NAME 不是字符串变量,而是运行时值;直接拼接会引发 SQL 注入或解析错误。
正确做法是用 format() 配合占位符:
-
%I自动加双引号并转义,安全处理标识符(表名、列名、schema 名),例如format('SELECT %I FROM %I', 'name', TG_TABLE_NAME) -
%L处理字符串/数值字面量,等价于quote_literal(),自动处理NULL和单引号 - 禁止混用:
EXECUTE format('... %I ...') USING ...中,USING只能传值,不能传标识符
NEW 和 OLD 字段访问必须按 TG_OP 分支判断
在 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 触发器性能开销远超直觉
一条影响 50 万行的 UPDATE,FOR EACH ROW 触发器会被调用 50 万次。哪怕函数体只有一行赋值,开销也显著——尤其嵌套了 EXECUTE、JSON 构造或跨 schema 函数调用。
关键细节:
- 多个触发器共存时,执行顺序由创建时间决定(
pg_trigger.tgrelid + pg_trigger.tgname排序),无法靠BEFORE/AFTER控制依赖 - 若逻辑可批量处理(如日志归档),优先考虑
FOR EACH STATEMENT+ 手动查变更行,而非逐行触发 - 测试时别只用单行数据验证逻辑,务必在真实批量场景下压测
最容易被忽略的是:NEW 和 OLD 在语句级触发器中为 NULL,且 BEFORE 行级触发器中修改 NEW 后不 RETURN NEW,变更就完全丢失。

















