PostgreSQL触发器函数必须声明RETURNS TRIGGER且末尾RETURN NEW/OLD/NULL;动态SQL须用format()配合%I/%L;避免递归更新,慎用未校验TG_OP直接访问NEW/OLD。

函数声明没写 RETURNS TRIGGER 就直接报错
PostgreSQL 在执行 CREATE TRIGGER 时,会立刻检查函数返回类型。只要函数定义里没写 RETURNS TRIGGER(大小写不敏感但推荐全大写),哪怕函数体完全正确,也会立即报错:ERROR: function my_func() does not exist,并提示 HINT: Function must return type trigger。
常见诱因包括:
- 把存储过程改成函数时,只改了
CREATE OR REPLACE PROCEDURE为FUNCTION,漏掉RETURNS TRIGGER - 函数开头写了
RETURNS VOID或RETURNS TEXT,甚至完全没写返回类型 - 用
CREATE OR REPLACE FUNCTION重建函数后,没重新运行CREATE TRIGGER,旧绑定仍指向错误签名
RETURN NEW/OLD/NULL 写错或漏写
PL/pgSQL 触发器函数末尾必须有 RETURN 语句,且值只能是 NEW、OLD 或 NULL —— 它们不是普通变量,而是向内核发送控制信号的“魔法值”。
典型问题:
- 写成
RETURN true、RETURN now()或RETURN 'blocked':类型校验失败,报错同上 - 函数末尾没写
RETURN:PL/pgSQL 默认返回NULL,但类型是unknown,不匹配TRIGGER - 在
TG_OP = 'INSERT'里访问OLD.id,或在TG_OP = 'DELETE'里读NEW.created_at:运行时报错record "old" is not assigned
动态 SQL 拼接表名/字段名引发语法错误或注入
直接字符串拼接 EXECUTE 'INSERT INTO ' || TG_TABLE_NAME || ' ...' 是危险且无效的:PostgreSQL 会把 TG_TABLE_NAME 当作未定义变量报错,更严重的是引入 SQL 注入风险。
正确做法是用 format():
format('SELECT %I FROM %I WHERE id = %L', 'name', TG_TABLE_NAME, NEW.id)-
%I用于标识符(表名、列名),自动加双引号并转义 -
%L用于字面量(字符串、数值),等价于quote_literal(),处理NULL和单引号 - 禁止混用:
USING只能传值,不能传标识符;%I必须在format()里完成
触发器递归导致 stack depth limit exceeded
最常见场景:在 AFTER UPDATE 触发器里执行 UPDATE 同一张表,触发自身再次调用,无限循环直到堆栈溢出。
解决方向很明确:
- 改用
BEFORE UPDATE,直接修改NEW字段,避免二次更新(例如自动设NEW.updated_at = NOW()) - 若逻辑必须用
UPDATE,加守卫变量:IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'guard_flag') THEN ...不现实;实际可用tg_level或临时pg_temp表标记 - 绝对不要在触发器里调用另一个会修改同一张表的函数,除非你已确认该函数内部也规避了递归
真正容易被忽略的是:触发器函数没有显式参数,但内核注入的 TG_OP、NEW、OLD 等变量存在与否取决于事件类型——不查 TG_OP 就直接读 NEW.id,上线后第一条 UPDATE 就崩。

















