触发器函数编写
触发器函数(trigger function)是「触发器脑袋里的那段代码」——它用 CREATE FUNCTION 创建、声明为零参数、返回 trigger 类型,由 CREATE TRIGGER 绑定到表上。它与普通函数有四处关键差别:调用方式(只能被触发器管理器回调)、参数传递(走 TG_ARGV 而非形参)、自动可用的 TG_ 系列变量、返回值语义随时机与层级变化。本节把这些差别讲透,并给出可复用的编写模板。
前置知识
阅读本节前,建议先了解:过渡表(REFERENCING)、行级触发器(FOR EACH ROW)
基础概念
触发器函数 vs 普通函数
| 对比项 | 普通函数 | 触发器函数 |
|---|---|---|
| 声明方式 | CREATE FUNCTION f(a int) RETURNS int | CREATE FUNCTION f() RETURNS trigger |
| 参数 | 有若干形参 | 必须零参数,参数走 TG_ARGV |
| 调用者 | 你自己(SELECT、CALL、表达式中) | 只能由触发器管理器在事件发生时回调 |
能否被 SELECT 调用 | 可以 | 不可以(0A000 报错) |
| 自动变量 | 无 | NEW / OLD / TG_* 共 12 个 |
| 返回值含义 | 返回给调用方 | 语义随时机与层级变化(见下表) |
| 典型波动性 | 视情况 | 默认 VOLATILE,不应改成 STABLE / IMMUTABLE |
CREATE TRIGGER(绑定) CREATE FUNCTION ... RETURNS trigger(逻辑)
├─ 选表 / 视图 ├─ 零参数
├─ 选 BEFORE / AFTER / INSTEAD OF ├─ 读 NEW / OLD / TG_*
├─ 选 FOR EACH ROW / STATEMENT ├─ 可读写表、可抛错
├─ 选事件 INSERT / UPDATE / DELETE └─ 返回 NULL 或一行
├─ 可传参 f('a', 'b') → TG_ARGV
└─ 可加 WHEN 条件这种「绑定」与「逻辑」的分离,让同一个函数能被多张表、多个事件复用——只要函数内部用 TG_TABLE_NAME、TG_OP 做分支。
必须零参数、必须返回 trigger
-- 正确
CREATE FUNCTION f() RETURNS trigger AS $$ ... $$ LANGUAGE plpgsql;
-- 错误!有参数
CREATE FUNCTION f(a int) RETURNS trigger AS $$ ... $$ LANGUAGE plpgsql;
-- 错误!返回类型不是 trigger
CREATE FUNCTION f() RETURNS void AS $$ ... $$ LANGUAGE plpgsql;官方原文强调:
Note that the function must be declared with no arguments even if it expects to receive some arguments specified in
CREATE TRIGGER— such arguments are passed viaTG_ARGV.
也就是说,CREATE TRIGGER ... EXECUTE FUNCTION f('x') 里的 'x' 不会成为函数形参,而是被放进 TG_ARGV[0]。函数签名始终是空的括号。
返回类型必须精确匹配
把返回值写成 SETOF trigger、trigger 的数组、或是别的类型,CREATE TRIGGER 会拒绝执行。数据变更触发器用 trigger,事件触发器用 event_trigger,两者不可混用。
返回值语义总表
这是本站读者最容易踩坑的地方。官方对返回值的定义必须逐时机、逐层级对照:
| 触发器类型 | 返回 NULL | 返回非 NULL |
|---|---|---|
行级 BEFORE | 跳过该行(后续同行触发器不再执行,该行的 INSERT/UPDATE/DELETE 不发生) | 操作以该行值继续;返回不同于原 NEW 的行会改写将要写入的行 |
行级 BEFORE(DELETE) | 跳过该行删除 | 返回值无直接影响,但必须非 NULL 才允许继续;惯用返回 OLD |
行级 AFTER | 返回值总被忽略 | 返回值总被忽略 |
语句级 BEFORE | 返回值总被忽略 | 返回值总被忽略 |
语句级 AFTER | 返回值总被忽略 | 返回值总被忽略 |
行级 INSTEAD OF | 「我没有修改任何底层数据」→ 该行不计入影响行数,后续触发器不执行 | 「操作已完成」→ 影响行数 +1,并决定 RETURNING 输出 |
官方原文(逐句对应上表):
A trigger function must return either
NULLor a record/row value having exactly the structure of the table the trigger was fired for.Row-level triggers fired
BEFOREcan return null to signal the trigger manager to skip the rest of the operation for this row... Returning a row value different from the original value ofNEWalters the row that will be inserted or updated.In the case of a before-trigger on
DELETE, the returned value has no direct effect, but it has to be nonnull to allow the trigger action to proceed.
INSTEAD OFtriggers... can return null to signal that they did not perform any updates...The return value of a row-level trigger fired
AFTERor a statement-level trigger firedBEFOREorAFTERis always ignored; it might as well be null.
三条最易错的结论
AFTER行级触发器的返回值完全无效——返回NEW不会改变任何东西,只是给人错觉。BEFORE行级忘记RETURN时函数返回NULL,等价于静默跳过所有行(UPDATE 0而应用毫无察觉)。- 所有触发器都能通过
RAISE EXCEPTION中止整个操作,哪怕它的返回值被忽略。
返回值的标准模板
-- 行级通用模板:DELETE 返回 OLD,其余返回 NEW
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;-- 语句级 / 行级 AFTER 模板:返回 NULL 即可(反正被忽略)
RETURN NULL;TG_ 系列变量(复用视角)
| 变量 | 类型 | 用途举例 |
|---|---|---|
NEW | record | 改写将写入的行(仅行级 BEFORE 有效) |
OLD | record | 对比旧值、计算差异(始终只读) |
TG_NAME | name | 日志里记录「哪个触发器」 |
TG_WHEN | text | BEFORE / AFTER / INSTEAD OF |
TG_LEVEL | text | ROW / STATEMENT |
TG_OP | text | 统一分支的核心:INSERT / UPDATE / DELETE / TRUNCATE |
TG_RELID | oid | 需要 pg_class OID 时使用 |
TG_TABLE_NAME | name | 一个函数服务多表时区分表 |
TG_TABLE_SCHEMA | name | 多 schema 部署时区分 |
TG_NARGS | integer | 校验参数个数 |
TG_ARGV | text[] | 读取 CREATE TRIGGER 传入的配置 |
TG_RELNAME 已弃用,新代码统一用 TG_TABLE_NAME。TG_ARGV 下标从 0 开始,越界(小于 0 或不小于 TG_NARGS)返回 NULL。
语法与用法
完整语法原型
CREATE [ OR REPLACE ] FUNCTION name () -- 参数列表必须为空
RETURNS trigger -- 数据变更触发器
{ LANGUAGE lang_name
| TRANSFORM { FOR TYPE type_name } [, ... ]
| { IMMUTABLE | STABLE | VOLATILE } -- 触发器函数应保持 VOLATILE
| [ NOT ] LEAKPROOF
| { CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT }
| { [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER }
| PARALLEL { UNSAFE | RESTRICTED | SAFE }
| COST execution_cost
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
} ...
-- 事件触发器用另一套返回类型:
CREATE FUNCTION name () RETURNS event_trigger ...关键点:
- 函数名后的括号必须为空,不能有参数,也不能写
OUT参数。 RETURNS trigger是数据变更触发器的唯一合法返回类型。IMMUTABLE/STABLE与触发器场景冲突(见后文「波动性」)。SECURITY DEFINER会改变触发器运行时使用的权限身份。
最小可运行示例
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE OR REPLACE FUNCTION emp_stamp() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- 校验
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- 戳记
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$$;
CREATE TRIGGER emp_stamp
BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE FUNCTION emp_stamp();INSERT INTO emp (empname, salary) VALUES ('alice', 40000);
SELECT empname, salary, last_user FROM emp; empname | salary | last_user
---------+--------+-----------
alice | 40000 | ch
(1 row)绑定触发器
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
ON table_name
[ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )FUNCTION 与 PROCEDURE 关键字在 EXECUTE 后等价,但被调用的对象必须是函数(不是过程)。
详细说明
零参数约束与 TG_ARGV 读取
参数以字符串常量传入,函数里按下标读取:
CREATE OR REPLACE FUNCTION log_change() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE '参数个数 = %', TG_NARGS;
RAISE NOTICE '第 1 个参数 = %', TG_ARGV[0];
RAISE NOTICE '第 2 个参数 = %', TG_ARGV[1];
RETURN NULL;
END;
$$;CREATE TRIGGER trg_log
AFTER INSERT ON some_table
FOR EACH ROW EXECUTE FUNCTION log_change('audit', 'products');
INSERT INTO some_table DEFAULT VALUES;NOTICE: 参数个数 = 2
NOTICE: 第 1 个参数 = audit
NOTICE: 第 2 个参数 = products
NOTICE: 参数个数 = 2防御式写法:先校验个数,再看内容,避免下标越界带来的隐性 NULL。
CREATE OR REPLACE FUNCTION stamp_cols() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_NARGS <> 1 THEN
RAISE EXCEPTION '函数 % 需要 1 个参数: <列名>', TG_NAME;
END IF;
IF TG_ARGV[0] = 'updated_at' THEN
NEW.updated_at := now();
ELSIF TG_ARGV[0] = 'created_at' THEN
NEW.created_at := now();
ELSIF TG_ARGV[0] = 'updated_by' THEN
NEW.updated_by := current_user;
ELSE
RAISE EXCEPTION '不支持的列名参数: %', TG_ARGV[0];
END IF;
RETURN NEW;
END;
$$;不要用位置参数写「万能列名」
只有当列名来自可信配置时才考虑把 TG_ARGV 当列名用。若列名可能来自用户输入,必须先经过白名单校验,或改用显式分支。动态拼接列名必须用 format(... %I):
-- 危险:直接拼接
EXECUTE 'SELECT ($1).' || TG_ARGV[0] INTO v USING NEW;
-- 正确:只允许白名单内的标识符
IF TG_ARGV[0] NOT IN ('updated_at', 'created_at') THEN
RAISE EXCEPTION '非法列名: %', TG_ARGV[0];
END IF;
EXECUTE format('SELECT ($1).%I', TG_ARGV[0]) INTO v USING NEW;返回值语义逐时机/层级对照
用一个函数把每种情形的返回值都演示出来:
CREATE OR REPLACE FUNCTION demo_returns() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_LEVEL = 'STATEMENT' THEN
-- 语句级:返回什么都被忽略,惯例返回 NULL
RETURN NULL;
END IF;
IF TG_WHEN = 'AFTER' THEN
-- AFTER 行级:返回什么都被忽略,惯例返回 NULL
RETURN NULL;
END IF;
-- BEFORE 行级:必须返回行
IF TG_OP = 'DELETE' THEN
RETURN OLD; -- NEW 为 NULL,返回 NEW 不合理
END IF;
RETURN NEW; -- 返回非 NULL 才不会被跳过
END;
$$;三种「跳过」行为要分清:
BEFORE 行级返回 NULL → 该行被跳过,计数为 0
对不需要的行在函数里 RETURN NULL → 条件式跳过
AFTER 行级返回 NULL → 无事发生(返回 ANY 值都一样)DELETE 触发器里 NEW 是 NULL,所以「返回 NULL 表示跳过」与「返回 NULL 表示 NEW」在语义上会撞车——官方特意提醒 NEW 在 DELETE 中为 NULL,返回它通常不合理,惯用返回 OLD。
用 TG_OP 分支的统一模板
一个函数复用给 INSERT/UPDATE/DELETE 时,TG_OP 是唯一的分支依据:
CREATE OR REPLACE FUNCTION audit_row() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
INSERT INTO audit_log(op, changed_at, changed_by, payload)
VALUES ('D', now(), current_user, to_jsonb(OLD));
RETURN OLD;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log(op, changed_at, changed_by, payload)
VALUES ('U', now(), current_user, to_jsonb(NEW));
RETURN NEW;
ELSE
INSERT INTO audit_log(op, changed_at, changed_by, payload)
VALUES ('I', now(), current_user, to_jsonb(NEW));
RETURN NEW;
END IF;
END;
$$;更紧凑的分支:先算「要审计的行」,再统一插入。
CREATE OR REPLACE FUNCTION audit_row_v2() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_op char(1);
v_payload jsonb;
BEGIN
IF TG_OP = 'DELETE' THEN
v_op := 'D'; v_payload := to_jsonb(OLD);
ELSE
v_op := CASE TG_OP WHEN 'INSERT' THEN 'I' ELSE 'U' END;
v_payload := to_jsonb(NEW);
END IF;
INSERT INTO audit_log(op, changed_at, changed_by, payload)
VALUES (v_op, now(), current_user, v_payload);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;TG_OP 也可能是 TRUNCATE,但 TRUNCATE 只能是语句级,此时应返回 NULL。
一个函数服务多表
配合 TG_TABLE_NAME / TG_TABLE_SCHEMA,一个函数可绑定到多张表:
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_touch_users
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER trg_touch_orders
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();需要按表区分行为时读取表名:
CREATE OR REPLACE FUNCTION touch_with_policy() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
IF TG_TABLE_SCHEMA <> 'public' THEN
RAISE EXCEPTION '只允许在 public schema 触发,当前为 %', TG_TABLE_SCHEMA;
END IF;
IF TG_TABLE_NAME = 'orders' THEN
NEW.updated_by := current_user; -- 只有订单表记录操作者
END IF;
RETURN NEW;
END;
$$;通用函数 vs 专用函数
官方在过渡表示例里明确提示:「In practice, it might be better to use three separate functions and avoid the run-time tests on TG_OP.」 通用函数省了代码,但每次调用都要做分支判断;如果分支复杂或性能敏感,拆成三个函数反而更清晰。
波动性:为什么触发器函数必须 VOLATILE
CREATE FUNCTION 的默认波动性就是 VOLATILE。触发器函数执行 DML、依赖当前事务状态,必须保持这个默认值。官方原文:
It is inappropriate for
AFTERtriggers that wish to query rows modified by the current command.
被标为 STABLE 或 IMMUTABLE 后,函数内的查询可能被快照隔离或提前求值,看不到调用语句自身的修改:
-- 错误!AFTER 触发器里读不到本语句写入的行
CREATE OR REPLACE FUNCTION bad_after() RETURNS trigger
LANGUAGE plpgsql STABLE AS $$
DECLARE
v_cnt int;
BEGIN
SELECT count(*) INTO v_cnt FROM t; -- STABLE → 看不到本语句的改动
RETURN NULL;
END;
$$;-- 正确:保持默认 VOLATILE
CREATE OR REPLACE FUNCTION good_after() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_cnt int;
BEGIN
SELECT count(*) INTO v_cnt FROM t; -- VOLATILE → 看得到
RETURN NULL;
END;
$$;同理:任何有副作用的函数都应标 VOLATILE,即使结果可预测,以防调用被优化掉。PARALLEL UNSAFE 是默认值,触发器函数会修改数据库状态,保持默认即可。
SECURITY DEFINER 与权限模型
触发器调用的权限检查分两层:
- 建触发器需要表上的
TRIGGER权限; - 调用函数需要函数上的
EXECUTE权限。
触发器执行时,默认以「排队事件的角色」运行,即执行 DML 的那个用户。若希望函数以固定身份运行(例如只让审计表可写、不让普通用户直接写),应声明 SECURITY DEFINER:
CREATE OR REPLACE FUNCTION audit_row_sd() RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER -- 以函数属主身份执行
SET search_path = public, pg_temp -- 锁定 search_path,防止劫持 ★
AS $$
BEGIN
INSERT INTO audit_log(op, changed_at, payload)
VALUES (TG_OP, now(), to_jsonb(NEW));
RETURN NULL;
END;
$$;
-- 收紧权限:普通用户不能直接写审计表
REVOKE INSERT ON audit_log FROM PUBLIC;SECURITY DEFINER 必须锁 search_path
不锁 search_path 时,攻击者可以创建一个同名对象并抢先解析,从而以函数属主身份执行恶意代码。始终显式设置 SET search_path = ...,且把 pg_temp 放在最后。
防递归:pg_trigger_depth()
触发器里再对同一张表做 DML,会再次触发触发器,形成级联甚至无限递归。官方指出级联触发器没有层级上限,需要程序员自行避免。用 pg_trigger_depth() 可以判断当前嵌套层级:
CREATE OR REPLACE FUNCTION safer_touch() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- 只在最外层触发时执行,避免自触发递归
IF pg_trigger_depth() > 1 THEN
RETURN NULL;
END IF;
UPDATE other_table SET touched = true WHERE id = NEW.id;
RETURN NEW;
END;
$$;pg_trigger_depth() 返回当前触发器嵌套层级(不在触发器内调用时为 0)。另一种防重入手段是用会话级变量或临时表做标记。
用户 UPDATE t
└─ 触发器 A 触发(depth = 1)
└─ 函数内 UPDATE t
└─ 触发器 A 再次触发(depth = 2) ← 若不加保护会无限递归不能直接调用:如何调试触发器函数
RETURNS trigger 的函数无法被普通 SQL 调用:
SELECT emp_stamp();ERROR: trigger functions can only be called as triggers
CONTEXT: compilation of PL/pgSQL function "emp_stamp" near line 1SQLSTATE 为 0A000(feature_not_supported)。调试办法:
- 在函数体里用
RAISE NOTICE/RAISE WARNING打印中间值; - 在事务内执行 DML 后
SELECT检查结果,再ROLLBACK:
BEGIN;
UPDATE products SET price = 100 WHERE id = 1;
SELECT * FROM audit_log ORDER BY id DESC LIMIT 5;
ROLLBACK;- 用
pg_get_functiondef(func_oid)反编译出当前函数定义,确认部署版本与预期一致:
SELECT pg_get_functiondef('emp_stamp()'::regprocedure);
SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgname = 'emp_stamp';AFTER 触发器抛错即回滚
AFTER 触发器无法改值、无法跳行,唯一干预方式是抛错——那会让整个语句连同触发器自身的副作用一起回滚:
CREATE OR REPLACE FUNCTION check_running_total() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_total numeric;
BEGIN
SELECT sum(amount) INTO v_total
FROM payment WHERE account_id = NEW.account_id;
IF v_total > 1000000 THEN
RAISE EXCEPTION '账户 % 累计付款 % 超限', NEW.account_id, v_total
USING ERRCODE = '23514';
END IF;
RETURN NULL;
END;
$$;触发器与原语句在同一事务内,任一出错则两者的效果都回滚:
BEGIN;
INSERT INTO payment ...; -- 成功
→ 触发器抛错
ROLLBACK; -- 上面的 INSERT 一并撤销RAISE 携带诊断信息
抛错时带上 COLUMN / CONSTRAINT / TABLE 等选项,能让 PHP 侧精确定位字段:
RAISE EXCEPTION '数量必须为正: %', NEW.qty
USING ERRCODE = '23514', -- check_violation
COLUMN = 'qty',
TABLE = TG_TABLE_NAME,
CONSTRAINT = 'qty_positive';USING 支持的选项包括 MESSAGE、DETAIL、HINT、ERRCODE、COLUMN、CONSTRAINT、DATATYPE、TABLE、SCHEMA。注意消息占位符只有 % 与 %%,没有 %s:
RAISE NOTICE '表 % 上触发 %', TG_TABLE_NAME, TG_NAME;
RAISE NOTICE '折扣被截断为 50%%'; -- %% 输出一个字面 %用内置触发器函数
官方提供几个现成的触发器函数(Table 9.110),不必自己写:
| 函数 | 用途 |
|---|---|
suppress_redundant_updates_trigger() | 行级 BEFORE UPDATE,抑制无实际数据变化的更新 |
tsvector_update_trigger() | 从纯文本列自动更新 tsvector 列 |
tsvector_update_trigger_column() | 同上,但分词配置来自 regconfig 列 |
-- 抑制「值没变」的伪更新(减少 WAL 与索引维护)
CREATE TRIGGER z_suppress_redundant
BEFORE UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION suppress_redundant_updates_trigger();
-- 自动维护全文检索列
CREATE TRIGGER tsvectorupdate
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(tsv, 'pg_catalog.simple', title, body);触发器按名称字母序执行
同一张表上、同一事件、同时机的多个触发器,按触发器名称的字母序依次触发。官方建议把抑制冗余更新的触发器命名为 z_ 前缀,使其排在最后执行。
编写模板汇总
-- 模板一:BEFORE 行级,校验 + 补值 + 戳记
CREATE OR REPLACE FUNCTION trg_before_row() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.amount < 0 THEN
RAISE EXCEPTION '金额不能为负: %', NEW.amount USING ERRCODE = '23514';
END IF;
NEW.updated_at := now();
RETURN NEW;
END;
$$;
-- 模板二:AFTER 行级,审计 + 联动(固定 RETURN NULL)
CREATE OR REPLACE FUNCTION trg_after_row() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_log(op, at, payload)
VALUES (TG_OP, now(),
CASE WHEN TG_OP = 'DELETE' THEN to_jsonb(OLD) ELSE to_jsonb(NEW) END);
RETURN NULL;
END;
$$;
-- 模板三:语句级 + 过渡表,批量处理(固定 RETURN NULL)
CREATE OR REPLACE FUNCTION trg_after_stmt() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_log(op, at, payload)
SELECT 'U', now(), to_jsonb(n) FROM new_rows n;
RETURN NULL;
END;
$$;实战示例
场景一:通用审计函数(三事件共用)
需求:把 emp 表的增删改逐行写入 emp_audit,记录操作类型、时间、用户与整行快照。
CREATE TABLE emp (
empname text NOT NULL,
salary integer
);
CREATE TABLE emp_audit (
operation char(1) NOT NULL,
stamp timestamp NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer
);
CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;
ELSE
INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;
END IF;
RETURN NULL; -- AFTER 触发器,返回值被忽略
END;
$$;
CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW EXECUTE FUNCTION process_emp_audit();运行验证:
INSERT INTO emp VALUES ('alice', 40000);
UPDATE emp SET salary = 45000 WHERE empname = 'alice';
DELETE FROM emp WHERE empname = 'alice';
SELECT operation, userid, empname, salary FROM emp_audit ORDER BY stamp; operation | userid | empname | salary
-----------+--------+---------+--------
I | ch | alice | 40000
U | ch | alice | 45000
D | ch | alice | 45000
(3 rows)场景二:配置驱动的列戳记(TG_ARGV)
需求:同一个函数,通过 TG_ARGV 指定要戳记的列,绑定到多张表。
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text,
created_at timestamptz,
updated_at timestamptz
);
CREATE OR REPLACE FUNCTION stamp_col() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_NARGS <> 1 THEN
RAISE EXCEPTION '触发器 % 需要一个列名参数', TG_NAME;
END IF;
IF TG_ARGV[0] = 'created_at' THEN
NEW.created_at := COALESCE(NEW.created_at, now());
ELSIF TG_ARGV[0] = 'updated_at' THEN
NEW.updated_at := now();
ELSE
RAISE EXCEPTION '不支持的列: %', TG_ARGV[0] USING ERRCODE = '22023';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_articles_created
BEFORE INSERT ON articles
FOR EACH ROW EXECUTE FUNCTION stamp_col('created_at');
CREATE TRIGGER trg_articles_updated
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION stamp_col('updated_at');INSERT INTO articles (title) VALUES ('PostgreSQL 触发器入门');
SELECT id, title, created_at IS NOT NULL AS has_created,
updated_at IS NOT NULL AS has_updated
FROM articles; id | title | has_created | has_updated
----+--------------------------+-------------+-------------
1 | PostgreSQL 触发器入门 | t | t
(1 row)场景三:状态机流转校验
需求:只允许 draft → published → archived 的单向流转,其他跃迁一律报错并给出可定位的诊断。
CREATE TABLE docs (
id bigserial PRIMARY KEY,
status text NOT NULL DEFAULT 'draft',
title text NOT NULL
);
CREATE OR REPLACE FUNCTION check_status_transition() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- 空值保护:状态不可置空
IF NEW.status IS NULL THEN
RAISE EXCEPTION 'status 不能为空'
USING ERRCODE = '23502', COLUMN = 'status', TABLE = TG_TABLE_NAME;
END IF;
-- 无变化的更新直接放行
IF NEW.status = OLD.status THEN
RETURN NEW;
END IF;
IF NOT (
(OLD.status = 'draft' AND NEW.status = 'published')
OR (OLD.status = 'published' AND NEW.status = 'archived')
) THEN
RAISE EXCEPTION '非法状态流转: % -> %', OLD.status, NEW.status
USING ERRCODE = '23514',
COLUMN = 'status',
TABLE = TG_TABLE_NAME,
CONSTRAINT = 'valid_status_transition';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_docs_status
BEFORE UPDATE OF status ON docs
FOR EACH ROW EXECUTE FUNCTION check_status_transition();INSERT INTO docs (title) VALUES ('触发器设计');
UPDATE docs SET status = 'published' WHERE id = 1; -- 允许
UPDATE docs SET status = 'draft' WHERE id = 1; -- 拒绝UPDATE 1
ERROR: 非法状态流转: published -> draft
DETAIL: ...
CONTEXT: PL/pgSQL function check_status_transition() line 15 at RAISE场景四:汇总表维护(幂等 + 异常兜底)
需求:维护按维度汇总的表,用「先 UPDATE,无命中则 INSERT,竞态时容忍唯一冲突」的幂等模式。这是官方汇总表示例的核心手法:
CREATE TABLE sales_summary (
time_key integer PRIMARY KEY,
amount_sold numeric(15,2) NOT NULL,
units_sold numeric(12) NOT NULL
);
CREATE OR REPLACE FUNCTION maint_summary() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
d_time integer;
d_amount numeric(15,2);
d_units numeric(12);
BEGIN
IF TG_OP = 'DELETE' THEN
d_time := OLD.time_key;
d_amount := -1 * OLD.amount_sold;
d_units := -1 * OLD.units_sold;
ELSIF TG_OP = 'UPDATE' THEN
IF OLD.time_key <> NEW.time_key THEN
RAISE EXCEPTION '不允许修改 time_key: % -> %', OLD.time_key, NEW.time_key;
END IF;
d_time := OLD.time_key;
d_amount := NEW.amount_sold - OLD.amount_sold;
d_units := NEW.units_sold - OLD.units_sold;
ELSE
d_time := NEW.time_key;
d_amount := NEW.amount_sold;
d_units := NEW.units_sold;
END IF;
<<insert_update>>
LOOP
UPDATE sales_summary
SET amount_sold = amount_sold + d_amount,
units_sold = units_sold + d_units
WHERE time_key = d_time;
EXIT insert_update WHEN found;
BEGIN
INSERT INTO sales_summary (time_key, amount_sold, units_sold)
VALUES (d_time, d_amount, d_units);
EXIT insert_update;
EXCEPTION
WHEN UNIQUE_VIOLATION THEN
-- 并发下另一事务刚插入同一行:回到循环重试 UPDATE
NULL;
END;
END LOOP insert_update;
RETURN NULL; -- AFTER 触发器
END;
$$;
CREATE TRIGGER maint_summary
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
FOR EACH ROW EXECUTE FUNCTION maint_summary();INSERT INTO sales_fact VALUES (1, 1, 10, 3);
INSERT INTO sales_fact VALUES (1, 2, 20, 5);
SELECT time_key, amount_sold, units_sold FROM sales_summary; time_key | amount_sold | units_sold
----------+-------------+------------
1 | 30.00 | 8
(1 row)这个模式的三个要点:增量而非全量重算、<<insert_update>> 循环配合 EXIT ... WHEN found、EXCEPTION WHEN UNIQUE_VIOLATION 兜底并发。
场景五:防递归的层级校验
需求:employees.manager_id 指向本表,禁止形成环路。插入/更新时向上回溯祖先链,同时用 pg_trigger_depth() 防止自触发递归。
CREATE TABLE employees (
id bigserial PRIMARY KEY,
name text NOT NULL,
manager_id bigint REFERENCES employees(id)
);
CREATE OR REPLACE FUNCTION check_no_cycle() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_cur bigint;
v_hops int := 0;
BEGIN
IF NEW.manager_id IS NULL THEN
RETURN NEW;
END IF;
-- 直接自指
IF NEW.manager_id = NEW.id THEN
RAISE EXCEPTION '员工不能是自己的上级' USING ERRCODE = '23514';
END IF;
-- 向上回溯,检测环路;上限兜底,避免脏数据导致死循环
v_cur := NEW.manager_id;
WHILE v_cur IS NOT NULL AND v_hops < 1000 LOOP
IF v_cur = NEW.id THEN
RAISE EXCEPTION '检测到管理关系环路: id=%', NEW.id USING ERRCODE = '23514';
END IF;
SELECT manager_id INTO v_cur FROM employees WHERE id = v_cur;
v_hops := v_hops + 1;
END LOOP;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_no_cycle
BEFORE INSERT OR UPDATE OF manager_id ON employees
FOR EACH ROW
WHEN (pg_trigger_depth() < 5) -- 兜底:嵌套过深直接跳过
EXECUTE FUNCTION check_no_cycle();INSERT INTO employees (name, manager_id) VALUES ('CEO', NULL);
INSERT INTO employees (name, manager_id) VALUES ('CTO', 1);
INSERT INTO employees (name, manager_id) VALUES ('DEV', 2);
UPDATE employees SET manager_id = 3 WHERE id = 1; -- 形成环,拒绝INSERT 0 1
INSERT 0 1
INSERT 0 1
ERROR: 检测到管理关系环路: id=1递归触发的兜底不可省
WHILE 里的 v_hops < 1000 与 WHEN (pg_trigger_depth() < 5) 都属于防御性兜底。即便业务逻辑上看不到环路,也要防止脏数据或并发写入导致回溯永不终止。
注意事项
- 给触发器函数加形参:
CREATE TRIGGER的参数走TG_ARGV,函数签名必须为空。-- 错误!sqlCREATE FUNCTION f(a text) RETURNS trigger ...; -- 建触发器时会失败 - 返回类型不是
trigger:数据变更触发器只接受trigger。-- 错误!sqlCREATE FUNCTION f() RETURNS void ... BEFORE行级忘记RETURN:函数末尾无RETURN时返回NULL,所有行被静默跳过。-- 错误!sqlBEGIN NEW.updated_at := now(); END; -- 缺 RETURN NEW; → UPDATE 0- 在
AFTER行级里改NEW并期望生效:返回值被完全忽略。-- 不推荐 - 在
DELETE触发器里返回NEW:NEW为NULL,会被当成「跳过」。-- 错误!sqlRETURN NEW; -- DELETE 中 NEW 是 NULL → 该行不删除 - 把触发器函数声明为
STABLE/IMMUTABLE:看不到调用语句的修改。-- 错误! - 直接
SELECT调用触发器函数:ERROR: trigger functions can only be called as triggers(0A000)。-- 错误! SECURITY DEFINER不锁search_path:存在权限提升风险。-- 错误!sqlCREATE FUNCTION f() RETURNS trigger SECURITY DEFINER AS $$ ... $$; -- 缺 SET search_path- 在触发器里对同表做 DML 且无递归保护:会级联触发甚至无限递归。
-- 错误! - 用
%s做占位符:PL/pgSQL 的RAISE只认%与%%。-- 错误!sqlRAISE NOTICE '%s', TG_TABLE_NAME; -- 会把 %s 原样输出 - 依赖触发器的执行顺序做业务:只按名称字母序,且可变。
-- 不推荐 - 在源码里动态拼接
TG_ARGV当列名:必须走白名单 +format(... %I)。-- 错误!
| 错误写法 | 报错 / 后果 | 正确做法 |
|---|---|---|
RETURNS void | CREATE TRIGGER 失败 | RETURNS trigger |
CREATE FUNCTION f(a text) | 签名不匹配 | 零参数 + TG_ARGV |
BEFORE 末尾无 RETURN | 静默跳过全部行 | RETURN NEW(DELETE 用 OLD) |
AFTER 中改 NEW | 无效果 | 只能改「将要写入的行」在 BEFORE 里做 |
SELECT f() | 0A000 | 通过 DML 间接触发 |
SECURITY DEFINER 无 SET search_path | 安全风险 | 显式锁定 search_path |
| 同表 DML 无递归保护 | 级联 / 无限递归 | pg_trigger_depth() / 标记 |
RAISE ... '%s' | 占位符不生效 | 用 % |
最佳实践
- 函数名与触发器名都带语义前缀:
trg_<表>_<用途>,便于在pg_trigger中检索。 - 行级函数结尾固定
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW;,作为团队强制模板。 - 语句级与
AFTER行级统一RETURN NULL,并在注释里写明「返回值被忽略」。 - 校验写
BEFORE,联动写AFTER:BEFORE只能保证本行此刻合法,跨行/跨表要看最终值。sqlCREATE TRIGGER trg_v BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION validate(); CREATE TRIGGER trg_a AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION propagate(); AFTER行级一律配WHEN:条件不满足时连事件都不排队,大批量 DML 下收益显著。sqlWHEN (OLD.price IS DISTINCT FROM NEW.price)- 保持默认
VOLATILE,不要给触发器函数加STABLE/IMMUTABLE。 SECURITY DEFINER必配SET search_path,并REVOKE底层表的直接写权限。- 抛错一律带
ERRCODE+COLUMN/CONSTRAINT/TABLE,让应用侧能定位字段。sqlRAISE EXCEPTION '数量必须为正: %', NEW.qty USING ERRCODE = '23514', COLUMN = 'qty', TABLE = TG_TABLE_NAME; - 重活留给语句级 + 过渡表,行级函数体必须廉价。
- 同一函数服务多表时,优先用显式分支而非动态 SQL;确需动态时用白名单 +
%I。 - 用
pg_trigger_depth()或标记表防递归,并给回溯/循环加操作次数上限。 - 用
COMMENT ON FUNCTION/COMMENT ON TRIGGER文档化,注明时机、事件、改写的列:sqlCOMMENT ON FUNCTION check_status_transition() IS '状态机:只允许 draft→published→archived'; COMMENT ON TRIGGER trg_docs_status ON docs IS 'BEFORE UPDATE OF status:校验状态流转合法性'; - 用
pg_get_functiondef()/pg_get_triggerdef()做部署比对,确认线上版本与预期一致。sqlSELECT pg_get_functiondef('check_status_transition()'::regprocedure); SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgname = 'trg_docs_status'; - 能用约束表达的不要用触发器:
CHECK、唯一索引、外键、生成列都比触发器廉价且声明式。 - 优先用内置触发器函数(
suppress_redundant_updates_trigger、tsvector_update_trigger),避免重复造轮子。
| 场景 | 推荐 | 反例 |
|---|---|---|
| 逐行改写 / 校验 | BEFORE 行级 + RETURN NEW | AFTER 里改 NEW |
| 逐行审计 | AFTER 行级 + RETURN NULL | 试图用返回值阻止操作 |
| 批量联动 | 语句级 + 过渡表 | 行级里逐行 UPDATE 汇总表 |
| 参数化行为 | TG_ARGV + 白名单分支 | 动态拼列名 |
| 多表复用 | TG_TABLE_NAME 分支 | 每张表复制一份函数(可选,视复杂度) |
| 防递归 | pg_trigger_depth() / 标记 | 无保护地对同表 DML |
| 权限隔离 | SECURITY DEFINER + SET search_path | 直接 SECURITY DEFINER |
下一节
下一节我们学习:事件触发器概述,了解如何把触发器机制从「表上的 DML」扩展到「整个数据库的 DDL 命令」,以及事件触发器函数与普通触发器函数在签名、返回值与可用变量上的差异。
参考链接
- PostgreSQL 18 官方文档 - Trigger Functions(PL/pgSQL)
- PostgreSQL 18 官方文档 - Overview of Trigger Behavior
- PostgreSQL 18 官方文档 - CREATE TRIGGER
- PostgreSQL 18 官方文档 - CREATE FUNCTION
- PostgreSQL 18 官方文档 - Function Volatility Categories
- PostgreSQL 18 官方文档 - Visibility of Data Changes
- PostgreSQL 18 官方文档 - Built-in Trigger Functions
- PostgreSQL 18 官方文档 - System Information Functions
- PostgreSQL 18 官方文档 - A Complete Trigger Example
- PostgreSQL 18 官方文档 - pg_trigger
- PostgreSQL 18 官方文档 - pg_proc