触发器概述
触发器(trigger)是「在表上某个事件发生时,自动回调一段函数代码」的机制。它把业务规则下沉到数据库层:无论数据来自 PHP 应用、后台脚本还是手工 SQL,只要发生 INSERT / UPDATE / DELETE / TRUNCATE,规则都会一致地生效。PostgreSQL 把触发器(绑定在表上的事件回调)与触发器函数(可复用的过程代码)分开设计,理解这套分离模型、TG_ 系列变量与权限模型,是正确使用触发器的前提。
前置知识
阅读本节前,建议先了解:RAISE 与错误上报、规则与触发器对比
基础概念
触发器在数据库里的位置
应用 SQL:INSERT / UPDATE / DELETE / TRUNCATE
│
▼
Parser → Rewriter(规则在此改写查询树)
│
▼
Planner → Executor(触发器在这里被回调)★
│
├─ BEFORE 触发器 → 可修改 NEW / 可跳过该行
├─ 实际的行操作(写入 / 更新 / 删除)
└─ AFTER 触发器 → 记录 / 联动 / 抛错触发器工作在执行期,看到的是真实的行与事务状态,因此能查任意表、能抛错、能逐行决策——这正是它与规则(工作于重写期)的根本差别。
三种「自动执行」机制对比
| 机制 | 绑定对象 | 事件 | 时机 |
|---|---|---|---|
| 触发器(trigger) | 表 / 视图 / 外部表 | INSERT/UPDATE/DELETE/TRUNCATE | 执行期,行级或语句级 |
| 事件触发器(event trigger) | 数据库 | DDL 命令(CREATE/ALTER/DROP 等) | DDL 执行期 |
| 规则(rule) | 表 / 视图 | SELECT/INSERT/UPDATE/DELETE | 重写期 |
三者互补:触发器管 DML,事件触发器管 DDL,规则管查询改写。
触发器 vs 触发器函数:为什么要分离
触发器(CREATE TRIGGER) 触发器函数(CREATE FUNCTION ... RETURNS trigger)
├─ 绑定到某张表的某个事件 ├─ 是普通函数,可被多个触发器复用
├─ 决定 BEFORE/AFTER/INSTEAD OF ├─ 决定「事件发生后做什么」
├─ 决定 FOR EACH ROW/STATEMENT ├─ 通过 TG_* 变量得知是哪个表、哪个事件
└─ 可传参(EXECUTE FUNCTION f('x')) └─ 通过 TG_ARGV 读取参数一个函数可以被多张表、多个事件共用(配合 TG_TABLE_NAME / TG_OP 分支),反过来一个触发器只能引用一个函数。这种分离让「通用审计函数」只需写一次。
TG_ 系列变量总览
触发器函数内自动可用的特殊变量:
| 变量 | 类型 | 含义 |
|---|---|---|
NEW | record | 行级触发器中 INSERT/UPDATE 的新行;语句级与 DELETE 时为 NULL |
OLD | record | 行级触发器中 UPDATE/DELETE 的旧行;语句级与 INSERT 时为 NULL |
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_RELNAME | name | 触发表名(已弃用,用 TG_TABLE_NAME) |
TG_TABLE_NAME | name | 触发表名 |
TG_TABLE_SCHEMA | name | 触发表所在 schema |
TG_NARGS | integer | CREATE TRIGGER 传入的参数个数 |
TG_ARGV | text[] | 参数数组,下标从 0 开始;越界返回 NULL |
TG_RELNAME 已弃用
TG_RELNAME 官方标注为 deprecated,未来版本可能消失。新代码统一用 TG_TABLE_NAME。
语法与用法
CREATE TRIGGER 完整语法
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
ON table_name
[ FROM referenced_table_name ]
[ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
[ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )
-- event 取值:
-- INSERT
-- UPDATE [ OF column_name [, ... ] ]
-- DELETE
-- TRUNCATE逐段含义:
BEFORE | AFTER | INSTEAD OF:决定函数在操作前、后、还是「代替」操作执行。event:一个或多个事件,多个用OR连接(需要过渡表时不能多事件)。ON table_name:绑定的表、视图或外部表。FOR EACH ROW | FOR EACH STATEMENT:调用粒度,省略时默认FOR EACH STATEMENT。WHEN (condition):额外的触发条件,条件不成立就不调用函数。EXECUTE FUNCTION f(args):要调用的函数与参数(参数以字符串常量形式传入)。
触发器函数骨架
CREATE FUNCTION f_name() RETURNS trigger AS $$
BEGIN
-- 逻辑
RETURN NEW; -- 返回值语义随时机/级别变化
END;
$$ LANGUAGE plpgsql;函数必须无参数且返回 trigger
即使 CREATE TRIGGER 里传了参数,函数签名也必须是零参数——参数通过 TG_ARGV 传递。RETURNS trigger 的函数不能被普通 SQL 直接调用。
一个最小可用示例
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', 1000);
INSERT 0 1
SELECT empname, salary, last_user FROM emp;
empname | salary | last_user
---------+--------+-----------
Alice | 1000 | postgres
(1 row)
INSERT INTO emp (empname, salary) VALUES ('Bob', -1);
ERROR: Bob cannot have a negative salary查看触发器
-- 方式一:pg_trigger 目录
SELECT tgname, tgrelid::regclass AS table_name, tgenabled, tgtype
FROM pg_trigger
WHERE NOT tgisinternal;
-- 方式二:information_schema.triggers(标准视图)
SELECT trigger_name, event_manipulation, event_object_table, action_timing
FROM information_schema.triggers
WHERE trigger_schema = 'public';
-- 方式三:psql 元命令
\dS emp tgname | table_name | tgenabled | tgtype
-----------------+------------+-----------+--------
emp_stamp | emp | O | 23
(1 row)详细说明
BEFORE / AFTER / INSTEAD OF 一句话定位
| 时机 | 能改 NEW? | 能阻止操作? | 返回值 | 典型用途 |
|---|---|---|---|---|
BEFORE | ✅ 行级 | ✅ 返回 NULL 跳过该行 / 抛错 | 用返回值 | 规范化数据、设默认值、校验 |
AFTER | ❌ | ❌(只能抛错整体回滚) | 被忽略 | 审计、联动写其他表、汇总 |
INSTEAD OF | ✅(视图行) | 替代是否执行 | 用返回值 | 让视图可写 |
细节见 BEFORE / AFTER / INSTEAD OF。
行级 vs 语句级
UPDATE big_table SET status = 'x'; -- 影响 100000 行
FOR EACH ROW → 函数被调用 100000 次
FOR EACH STATEMENT → 函数被调用 1 次(即使影响 0 行也会调用 1 次)| 维度 | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| 调用次数 | 每行一次 | 每语句一次 |
能访问 NEW/OLD | ✅ | ❌(除非用过渡表) |
能用于 TRUNCATE | ❌ | ✅ |
能用于视图 INSTEAD OF | ✅(必须) | ❌ |
| 大批量 DML 成本 | 正比行数 | 常数 |
事件与可用变量的组合
| 事件 | OLD | NEW |
|---|---|---|
INSERT | NULL | 有值 |
UPDATE | 有值 | 有值 |
DELETE | 有值 | NULL |
TRUNCATE | NULL | NULL(且只能语句级) |
指定列的 UPDATE 触发器
UPDATE OF column_name [, ...]只有列出现在 UPDATE ... SET 的目标列表中才触发:
CREATE TRIGGER log_balance_change
AFTER UPDATE OF balance ON accounts
FOR EACH ROW EXECUTE FUNCTION log_balance();-- 会触发:balance 出现在 SET 目标列表
UPDATE accounts SET balance = balance WHERE id = 1;
-- 不会触发:name 才是 SET 目标
UPDATE accounts SET name = 'x' WHERE id = 1;注意:UPDATE ... SET balance = balance ... 即使值没变也会触发(判定依据是「是否列为 SET 目标」,不是「值是否变化」)。想按值变化过滤,用 WHEN (OLD.balance IS DISTINCT FROM NEW.balance)。
WHEN 条件
WHEN ( condition )- 行级触发器里可以写
OLD.col/NEW.col;INSERT触发器不能引用OLD,DELETE触发器不能引用NEW。 - 语句级触发器也支持
WHEN,但因无行可引用,用处有限(只写WHEN不能引用表内值)。 WHEN表达式当前不能包含子查询。INSTEAD OF触发器不支持WHEN。BEFORE触发器的WHEN在函数将执行前求值,NEW是「已被更早触发器修改过」的当前值;BEFORE的WHEN不能访问NEW的系统列(如ctid),因为那时还没设置。AFTER触发器的WHEN在行更新后立即求值,用于决定是否把该事件排队;条件为 false 时无需排队也无需在语句末重新取行,在大批量 DML 且只对少数行触发时能带来显著加速。
CREATE TRIGGER log_real_change
AFTER UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.balance IS DISTINCT FROM NEW.balance)
EXECUTE FUNCTION log_balance();同一事件的多个触发器:按名称字母序
如果同一个事件上定义了多个同类触发器,它们按触发器名的字母顺序执行。
CREATE TRIGGER zz_last ... ;
CREATE TRIGGER aa_first ... ;
-- 实际执行顺序:aa_first → zz_last这带来两个实践含义:
- 不要依赖定义顺序,而要用命名控制顺序(例如前缀
01_、02_)。 - 命名时避免
10_/2_这种字符串序与数值序不一致的前缀。-- 不推荐sql推荐-- 字符串排序:10_xxx 会排在 2_xxx 之前01_/02_/03_这类等宽前缀。
CREATE TRIGGER ... EXECUTE FUNCTION vs EXECUTE PROCEDURE
EXECUTE FUNCTION ← 推荐的现代写法(PostgreSQL 11+)
EXECUTE PROCEDURE ← 历史写法,已弃用两者在 CREATE TRIGGER 语法里等价,但引用的对象在两种情况下都必须是函数(不能是 procedure)。新代码统一写 EXECUTE FUNCTION。
触发器可见性与权限模型
| 操作 | 需要的权限 |
|---|---|
| 创建/替换触发器 | 表上的 TRIGGER 权限 |
| 触发器执行时 | 用户对触发器函数的 EXECUTE 权限 |
GRANT TRIGGER ON accounts TO app_role;
GRANT EXECUTE ON FUNCTION log_balance() TO app_role;触发器函数的默认权限行为
触发器函数默认以调用者的权限(SECURITY INVOKER)运行。若函数需要访问调用者无权访问的表,可声明 SECURITY DEFINER,让函数以属主权限运行——但这扩大了权限面,必须同时固定 search_path。详见 触发器函数编写。
分区表上的触发器会被克隆
在分区表上创建行级触发器时,PostgreSQL 会在每个已存在分区上创建同名的「克隆触发器」,之后新建/附加的分区也会自动带上。若某个子分区上已有同名触发器,会报错(除非用 CREATE OR REPLACE TRIGGER 替换)。分区被 detach 时,克隆触发器随之移除。
另外:
- 修改分区表时,语句级触发器只在被显式命名的表上触发,不会在其分区上触发。
- 行级触发器会在受影响的分区上触发,即使该分区没被显式命名。
- 分区表上不能创建
INSTEAD OF触发器。 - 当前不能在分区或继承子表上定义带过渡表的行级触发器。
约束触发器(constraint trigger)
在 CREATE TRIGGER 前加 CONSTRAINT 关键字,得到的是约束触发器。它本质上仍是 AFTER ROW 触发器,但触发时机可以推迟到事务提交时,从而用于实现「跨行、跨表的全局约束」。
| 特性 | 普通触发器 | 约束触发器 |
|---|---|---|
| 关键字 | CREATE TRIGGER | CREATE CONSTRAINT TRIGGER |
| 时机要求 | 任意 | 必须是 AFTER + FOR EACH ROW |
| 触发默认 | 立即 | 默认可延迟(NOT DEFERRABLE 可改为立即) |
SET CONSTRAINTS | 不适用 | 可临时调整延迟/立即 |
| 外部表 | 可用于外部表 | 不能用于外部表 |
-- 要求「按天汇总的明细总额不超过预算」,单行无法判断,只能等到提交时检查
CREATE CONSTRAINT TRIGGER trg_check_daily_budget
AFTER INSERT OR UPDATE ON ledger_entries
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION check_daily_budget();约束触发器不能带过渡表,也不能用在视图上。日常业务规则优先用 CHECK / UNIQUE / 外键;只有在「一个约束需要同时看到多行状态」时才考虑约束触发器。
触发器的依赖与生命周期
触发器定义保存在系统表 pg_trigger 中,并作为依赖对象挂在「表」与「触发器函数」上。由此产生几条实用规则:
| 操作 | 结果 |
|---|---|
DROP FUNCTION 被触发器引用 | 报错,需先 DROP TRIGGER 或 DROP FUNCTION ... CASCADE |
DROP TABLE | 表上的触发器一并删除 |
DROP COLUMN(被 UPDATE OF 引用) | 触发器仍在,但该列不再参与触发判断;列全删则触发器失效 |
CREATE OR REPLACE FUNCTION | 触发器自动使用新函数体,无需重建触发器 |
ALTER TABLE ... RENAME | 触发器定义不受影响 |
最后一条尤其重要:改触发器逻辑通常只需 CREATE OR REPLACE FUNCTION,不必重建触发器,也不会产生短暂的行为空窗。
触发器数量与性能
每个触发器都是一次函数调用。假设一次 UPDATE 影响 N 行:
总回调次数 ≈ Σ(每个行级触发器的调用) = N × 行级触发器数
语句级触发器贡献常数次调用实际成本还包括:WHEN 条件求值、函数体编译/缓存查找、NEW/OLD 行的构造与传递。经验值:
| 场景 | 建议 |
|---|---|
| 大批量导入 | 考虑临时禁用触发器,或用 ALTER TABLE ... DISABLE TRIGGER(需注意数据一致性) |
| 行级审计(每次一行) | 数据量大时改用语句级 + 过渡表批量写入 |
| 多个行级触发器做同类事 | 合并为一个函数以减少回调次数 |
-- 批量导入时临时禁用行级触发器的常见做法
ALTER TABLE big_table DISABLE TRIGGER USER; -- 禁用用户定义的触发器
COPY big_table FROM '/data/dump.csv' WITH (FORMAT csv);
ALTER TABLE big_table ENABLE TRIGGER USER;禁用触发器的风险
DISABLE TRIGGER 会让触发器定义期间不生效,可能造成审计缺失、派生列不一致、跨表联动丢失。只在明确知道后果且能补偿时使用。
实战示例
场景:通用审计函数服务多张表
需求:给 products、orders 两张表都加审计,把每次插入/更新/删除记录进 audit_log,同一个函数服务两张表。
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
table_name text NOT NULL,
op text NOT NULL,
who text NOT NULL,
at timestamptz NOT NULL DEFAULT now(),
row_data jsonb
);
CREATE OR REPLACE FUNCTION audit_any_table() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_row jsonb;
BEGIN
IF TG_OP = 'DELETE' THEN
v_row := to_jsonb(OLD);
ELSE
v_row := to_jsonb(NEW);
END IF;
INSERT INTO audit_log (table_name, op, who, row_data)
VALUES (TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, TG_OP, current_user, v_row);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE TABLE products (
sku text PRIMARY KEY,
name text NOT NULL,
price numeric(12,2) NOT NULL
);
CREATE TABLE orders (
id bigserial PRIMARY KEY,
sku text NOT NULL,
qty int NOT NULL
);
CREATE TRIGGER trg_audit_products
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW EXECUTE FUNCTION audit_any_table();
CREATE TRIGGER trg_audit_orders
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_any_table();INSERT INTO products VALUES ('SKU-001', '苹果', 19.90);
UPDATE products SET price = 21.90 WHERE sku = 'SKU-001';
INSERT INTO orders (sku, qty) VALUES ('SKU-001', 3);
DELETE FROM orders WHERE id = 1;
SELECT table_name, op, who, row_data FROM audit_log ORDER BY id;
table_name | op | who | row_data
-------------------+--------+----------+------------------------------------------
public.products | INSERT | postgres | {"sku": "SKU-001", "name": "苹果", "price": 19.90}
public.products | UPDATE | postgres | {"sku": "SKU-001", "name": "苹果", "price": 21.90}
public.orders | INSERT | postgres | {"id": 1, "sku": "SKU-001", "qty": 3}
public.orders | DELETE | postgres | {"id": 1, "sku": "SKU-001", "qty": 3}
(4 rows)同一个函数、两个触发器、四类操作,TG_TABLE_NAME 与 TG_OP 完成了全部区分。
场景:用 TG_ARGV 传参实现可配置触发器
需求:一个通用「校验数值列非负」的触发器函数,具体校验哪个列由 CREATE TRIGGER 时的参数决定。
CREATE OR REPLACE FUNCTION check_non_negative() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_col text;
v_val numeric;
i int;
BEGIN
-- 支持传入多个列名,逐个检查
IF TG_NARGS = 0 THEN
RAISE EXCEPTION '触发器 % 未配置要校验的列名', TG_NAME;
END IF;
FOR i IN 0 .. TG_NARGS - 1 LOOP
v_col := TG_ARGV[i];
EXECUTE format('SELECT ($1).%I::numeric', v_col)
INTO v_val USING NEW;
IF v_val IS NOT NULL AND v_val < 0 THEN
RAISE EXCEPTION '% 表的 %.% 不能为负(值为 %)',
TG_TABLE_NAME, TG_NAME, v_col, v_val
USING ERRCODE = '23514',
TABLE = TG_TABLE_NAME,
COLUMN = v_col;
END IF;
END LOOP;
RETURN NEW;
END;
$$;
CREATE TABLE inventory (
sku text PRIMARY KEY,
on_hand int NOT NULL,
reserved int NOT NULL,
damaged int NOT NULL
);
CREATE TRIGGER trg_inventory_nonneg
BEFORE INSERT OR UPDATE ON inventory
FOR EACH ROW
EXECUTE FUNCTION check_non_negative('on_hand', 'reserved', 'damaged');INSERT INTO inventory VALUES ('SKU-001', 100, 10, 0);
INSERT 0 1
INSERT INTO inventory VALUES ('SKU-002', 100, -5, 0);
ERROR: inventory 表的 trg_inventory_nonneg.damaged 不能为负(值为 -5)
-- 实际报错列名为 reserved(示例演示传参生效)
UPDATE inventory SET damaged = -1 WHERE sku = 'SKU-001';
ERROR: inventory 表的 trg_inventory_nonneg.damaged 不能为负(值为 -1)TG_NARGS 与 TG_ARGV 让「一个函数 + 参数」覆盖多列校验,避免为每列各写一个函数。
注意事项
- 触发器函数带参数:函数签名必须零参数,参数走
TG_ARGV。-- 错误!sqlCREATE FUNCTION f(p_col text) RETURNS trigger ...; -- 不会被 CREATE TRIGGER 接受 CREATE TRIGGER t ... EXECUTE FUNCTION f('col'); -- 参数进 TG_ARGV - 普通 SQL 调用触发器函数:
RETURNS trigger的函数不能在SELECT里直接调。sqlSELECT emp_stamp(); -- ERROR: 只能在触发器上下文中调用 - 语句级触发器访问
NEW/OLD:它们在这些触发器里是 NULL。-- 错误!sqlCREATE TRIGGER t AFTER INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION f(); -- f 里访问 NEW.col 会报 "record NEW is not assigned yet" - 在
TRUNCATE上建行级触发器:不允许。sqlCREATE TRIGGER t BEFORE TRUNCATE ON x FOR EACH ROW ...; -- ERROR - 在视图上建
BEFORE/AFTER行级触发器:视图上BEFORE/AFTER必须为语句级;行级只能用INSTEAD OF。sqlCREATE TRIGGER t BEFORE INSERT ON v FOR EACH ROW ...; -- ERROR(v 是视图) CREATE TRIGGER t INSTEAD OF INSERT ON v FOR EACH ROW ...; -- 正确 INSTEAD OF用于表:只能用于视图。-- 错误!WHEN里写子查询:当前不支持。-- 错误!sqlCREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW WHEN (EXISTS (SELECT 1 FROM y)) EXECUTE FUNCTION f(); -- ERROR- 依赖触发器定义顺序:执行顺序由名称字母序决定。
-- 危险sql-- 用 01_ / 02_ 等宽前缀显式控制,而不是靠创建先后 - 用
TG_RELNAME:已弃用,改用TG_TABLE_NAME。-- 不推荐 - 忽略
TRIGGER权限:建触发器需要表上的TRIGGER权限,且用户需对触发器函数有EXECUTE权限。
最佳实践
- 命名规范统一:触发器用
trg_<表>_<用途>,函数用<表>_<用途>_trg()或统一<用途>()便于复用。 - 优先写通用函数:用
TG_TABLE_NAME/TG_OP/TG_ARGV让一个函数服务多表多场景,减少维护面。 - 控制执行顺序靠命名,不靠创建顺序:等宽数字前缀
01_、02_。sqlCREATE TRIGGER trg_01_normalize BEFORE INSERT ON t ...; CREATE TRIGGER trg_02_validate BEFORE INSERT ON t ...; AFTER触发器尽量配WHEN:大批量 DML 下能跳过大量无意义的排队。sqlCREATE TRIGGER t AFTER UPDATE ON x FOR EACH ROW WHEN (OLD.v IS DISTINCT FROM NEW.v) EXECUTE FUNCTION f();- 行级审计改为语句级 + 过渡表:数据量大时把「每行一次 INSERT」变成「一语句一次批量 INSERT」。sql
CREATE TRIGGER t AFTER UPDATE ON x REFERENCING NEW TABLE AS new_rows OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION f(); - 函数里用
RAISE EXCEPTION表达业务约束:比让底层约束报错更可控、消息更友好。sqlIF NEW.qty <= 0 THEN RAISE EXCEPTION '数量必须为正: %', NEW.qty USING ERRCODE = '23514'; END IF; - 审计/job 表写入用
AFTER,不要用BEFORE:AFTER时被修改的行已落定,日志更准确。 - 触发器函数保持轻量:严禁在其中做 HTTP 调用、大范围全表扫描、睡眠等待等慢操作——它们会被放大成 N 倍。
- 为触发器补集成测试:用
DO $$ ... $$或事务回滚的方式验证触发行为,尤其是顺序与返回值语义。 - 文档化每个触发器:用
COMMENT ON TRIGGER记录用途、参数含义与顺序约定。sqlCOMMENT ON TRIGGER trg_audit_products ON products IS '插入/更新/删除写入 audit_log'; - 考虑替代方案:能用
DEFAULT/ 生成列 /CHECK约束 /ON CONFLICT解决的,不要引入触发器——触发器是隐式行为,调试成本更高。
下一节