在PostgreSQL里,触发器函数如果只针对某一张表写死字段名和逻辑,一旦表结构演进就会频繁改写函数。PL/pgSQL作为过程语言,允许在触发器函数内部通过特殊变量获取触发上下文,并利用动态SQL把逻辑推迟到运行期。理解这些机制,是编写可复用、可适应动态需求触发器的基础。

一、PL/pgSQL触发器中的内建变量与执行上下文
每个触发器函数在被调用时,PostgreSQL都会自动注入一组以TG_开头的内建变量。最常见的是TG_TABLE_NAME表示当前触发表名,TG_TABLE_SCHEMA表示模式名,TG_OP表示触发操作(INSERT、UPDATE、DELETE),TG_WHEN表示是BEFORE还是AFTER,NEW和OLD则是行级触发器中的新旧行记录。这些变量让同一个函数可以挂载到多张表上而不会混淆上下文。
例如,当我们希望对所有写入操作记录日志时,不需要在函数中写死users或orders,而是直接读取TG_TABLE_NAME。在行级BEFORE触发器中,还可以修改NEW记录的字段值再返回,从而实现统一的默认值填充或脱敏。这种上下文感知能力,是动态逻辑能够成立的前提。
需要注意的是,NEW和OLD是复合类型,不能直接用字符串拼接获取某个列的值,必须配合EXECUTE ... USING或EXECUTE 'SELECT $1.' || colname的方式。如果忽略类型安全,很容易在动态取值时抛出“record类型没有该字段”的错误。因此,在利用内建变量前,先明确触发器的粒度(语句级还是行级)非常关键。
二、使用EXECUTE实现动态SQL与字段级处理
PL/pgSQL的EXECUTE命令可以执行一段拼接出来的字符串,从而实现动态逻辑。比如我们要在更新时,自动把被修改字段名记录进审计表,就可以从TG_TABLE_NAME取出表结构,再比对NEW与OLD的字段差异。下面的示例展示了一个通用的审计触发器函数:
CREATE OR REPLACE FUNCTION dynamic_audit_trigger()
RETURNS trigger AS $$
DECLARE
col_list text;
sql_text text;
changed_cols text := '';
BEGIN
IF TG_OP = 'UPDATE' THEN
SELECT string_agg(column_name, ',')
INTO col_list
FROM information_schema.columns
WHERE table_schema = TG_TABLE_SCHEMA
AND table_name = TG_TABLE_NAME;
sql_text := 'SELECT string_agg(c, '','') FROM (';
sql_text := sql_text || 'SELECT unnest(string_to_array(' || quote_literal(col_list) || ', '','')) AS c';
sql_text := sql_text || ') t WHERE (NEW).(t.c) IS DISTINCT FROM (OLD).(t.c)';
EXECUTE sql_text INTO changed_cols USING NEW, OLD;
INSERT INTO audit_log(table_name, op_type, changed_columns, op_time)
VALUES (TG_TABLE_NAME, TG_OP, changed_cols, now());
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
上面的代码通过information_schema.columns拿到表的列清单,再动态构造比对语句。使用quote_literal可以避免列名中的特殊字符破坏SQL结构,而IS DISTINCT FROM能正确处理NULL值比较。相比为每张表手写触发器,这种方式在新增字段时无需改动函数。
不过动态SQL也有代价:每次执行都要做语法解析与计划生成,性能比静态SQL略低。在高频写入场景下,如果业务逻辑其实固定,应优先写静态函数;只有当表结构或规则确实多变时,才用EXECUTE换取灵活性。另外,拼接字符串时务必使用quote_ident和quote_literal,防止标识符注入。
三、动态逻辑的常见场景与避坑实践
最常见的动态触发器需求是“多表通用审计”“按配置表决定校验规则”“根据操作类型路由数据”。以路由为例,我们可以建一张route_rule表,存放目标表名与过滤条件,触发器函数读取规则后用EXECUTE插入对应表。这样业务调整只改数据,不碰函数代码。
另一个易踩的坑是权限与搜索路径。动态SQL在EXECUTE中执行时,默认使用函数的所有者权限而非调用者,若表不在search_path内,即使拼了TG_TABLE_SCHEMA也要用quote_ident包裹模式名与表名。否则会遇到“关系不存在”的错误。同时,在BEFORE触发器中若返回NULL,则表示终止该行操作,动态逻辑里要小心条件分支遗漏返回值。
最后建议把复杂动态逻辑拆成小函数,用RAISE NOTICE输出拼接后的SQL用于调试。对于必须处理动态列的场景,可结合jsonb把NEW转成jsonb再用->>取字段,比拼接复合类型更安全。如下片段展示了转换方式:
DECLARE
rec_json jsonb;
field_val text;
BEGIN
rec_json := to_jsonb(NEW);
field_val := rec_json->>'user_name';
IF field_val IS NULL THEN
RAISE EXCEPTION 'user_name cannot be null';
END IF;
RETURN NEW;
END;
通过这种方式,即使表增加了user_name之外的列,函数也不受影响。总体来看,基于PL/pgSQL的触发器动态逻辑核心在于善用TG变量、EXECUTE与字典表,在灵活与性能之间找到平衡。
PostgreSQLPL/pgSQLtrigger_function修改时间:2026-08-14 23:57:33