Skip to content

触发器概述 ​

触发器(trigger)是「在表上某个事件发生时,自动回调一段函数代码」的机制。它把业务规则下沉到数据库层:无论数据来自 PHP 应用、后台脚本还是手工 SQL,只要发生 INSERT / UPDATE / DELETE / TRUNCATE,规则都会一致地生效。PostgreSQL 把触发器(绑定在表上的事件回调)与触发器函数(可复用的过程代码)分开设计,理解这套分离模型、TG_ 系列变量与权限模型,是正确使用触发器的前提。

前置知识

阅读本节前,建议先了解:RAISE 与错误上报、规则与触发器对比

基础概念 ​

触发器在数据库里的位置 ​

text
应用 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 触发器函数:为什么要分离 ​

text
触发器(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_ 系列变量总览 ​

触发器函数内自动可用的特殊变量:

变量类型含义
NEWrecord行级触发器中 INSERT/UPDATE 的新行;语句级与 DELETE 时为 NULL
OLDrecord行级触发器中 UPDATE/DELETE 的旧行;语句级与 INSERT 时为 NULL
TG_NAMEname触发的触发器名
TG_WHENtextBEFORE / AFTER / INSTEAD OF
TG_LEVELtextROW / STATEMENT
TG_OPtextINSERT / UPDATE / DELETE / TRUNCATE
TG_RELIDoid触发表在 pg_class 中的 OID
TG_RELNAMEname触发表名(已弃用,用 TG_TABLE_NAME)
TG_TABLE_NAMEname触发表名
TG_TABLE_SCHEMAname触发表所在 schema
TG_NARGSintegerCREATE TRIGGER 传入的参数个数
TG_ARGVtext[]参数数组,下标从 0 开始;越界返回 NULL

TG_RELNAME 已弃用

TG_RELNAME 官方标注为 deprecated,未来版本可能消失。新代码统一用 TG_TABLE_NAME。

语法与用法 ​

CREATE TRIGGER 完整语法 ​

text
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):要调用的函数与参数(参数以字符串常量形式传入)。

触发器函数骨架 ​

text
CREATE FUNCTION f_name() RETURNS trigger AS $$
BEGIN
    -- 逻辑
    RETURN NEW;      -- 返回值语义随时机/级别变化
END;
$$ LANGUAGE plpgsql;

函数必须无参数且返回 trigger

即使 CREATE TRIGGER 里传了参数,函数签名也必须是零参数——参数通过 TG_ARGV 传递。RETURNS trigger 的函数不能被普通 SQL 直接调用。

一个最小可用示例 ​

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();
text
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

查看触发器 ​

sql
-- 方式一: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
text
       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 语句级 ​

text
UPDATE big_table SET status = 'x';   -- 影响 100000 行

FOR EACH ROW        → 函数被调用 100000 次
FOR EACH STATEMENT  → 函数被调用 1 次(即使影响 0 行也会调用 1 次)
维度FOR EACH ROWFOR EACH STATEMENT
调用次数每行一次每语句一次
能访问 NEW/OLD✅❌(除非用过渡表)
能用于 TRUNCATE❌✅
能用于视图 INSTEAD OF✅(必须)❌
大批量 DML 成本正比行数常数

事件与可用变量的组合 ​

事件OLDNEW
INSERTNULL有值
UPDATE有值有值
DELETE有值NULL
TRUNCATENULLNULL(且只能语句级)

指定列的 UPDATE 触发器 ​

text
UPDATE OF column_name [, ...]

只有列出现在 UPDATE ... SET 的目标列表中才触发:

sql
CREATE TRIGGER log_balance_change
  AFTER UPDATE OF balance ON accounts
  FOR EACH ROW EXECUTE FUNCTION log_balance();
text
-- 会触发: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 条件 ​

text
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 且只对少数行触发时能带来显著加速。
sql
CREATE TRIGGER log_real_change
  AFTER UPDATE ON accounts
  FOR EACH ROW
  WHEN (OLD.balance IS DISTINCT FROM NEW.balance)
  EXECUTE FUNCTION log_balance();

同一事件的多个触发器:按名称字母序 ​

如果同一个事件上定义了多个同类触发器,它们按触发器名的字母顺序执行。

sql
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 ​

text
EXECUTE FUNCTION  ← 推荐的现代写法(PostgreSQL 11+)
EXECUTE PROCEDURE ← 历史写法,已弃用

两者在 CREATE TRIGGER 语法里等价,但引用的对象在两种情况下都必须是函数(不能是 procedure)。新代码统一写 EXECUTE FUNCTION。

触发器可见性与权限模型 ​

操作需要的权限
创建/替换触发器表上的 TRIGGER 权限
触发器执行时用户对触发器函数的 EXECUTE 权限
sql
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 TRIGGERCREATE CONSTRAINT TRIGGER
时机要求任意必须是 AFTER + FOR EACH ROW
触发默认立即默认可延迟(NOT DEFERRABLE 可改为立即)
SET CONSTRAINTS不适用可临时调整延迟/立即
外部表可用于外部表不能用于外部表
sql
-- 要求「按天汇总的明细总额不超过预算」,单行无法判断,只能等到提交时检查
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 行:

text
总回调次数 ≈ Σ(每个行级触发器的调用) = N × 行级触发器数
语句级触发器贡献常数次调用

实际成本还包括:WHEN 条件求值、函数体编译/缓存查找、NEW/OLD 行的构造与传递。经验值:

场景建议
大批量导入考虑临时禁用触发器,或用 ALTER TABLE ... DISABLE TRIGGER(需注意数据一致性)
行级审计(每次一行)数据量大时改用语句级 + 过渡表批量写入
多个行级触发器做同类事合并为一个函数以减少回调次数
sql
-- 批量导入时临时禁用行级触发器的常见做法
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,同一个函数服务两张表。

sql
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();
text
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 时的参数决定。

sql
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');
text
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。-- 错误!
    sql
    CREATE FUNCTION f(p_col text) RETURNS trigger ...;   -- 不会被 CREATE TRIGGER 接受
    CREATE TRIGGER t ... EXECUTE FUNCTION f('col');      -- 参数进 TG_ARGV
  • 普通 SQL 调用触发器函数:RETURNS trigger 的函数不能在 SELECT 里直接调。
    sql
    SELECT emp_stamp();   -- ERROR: 只能在触发器上下文中调用
  • 语句级触发器访问 NEW / OLD:它们在这些触发器里是 NULL。-- 错误!
    sql
    CREATE TRIGGER t AFTER INSERT ON x FOR EACH STATEMENT
      EXECUTE FUNCTION f();   -- f 里访问 NEW.col 会报 "record NEW is not assigned yet"
  • 在 TRUNCATE 上建行级触发器:不允许。
    sql
    CREATE TRIGGER t BEFORE TRUNCATE ON x FOR EACH ROW ...;   -- ERROR
  • 在视图上建 BEFORE / AFTER 行级触发器:视图上 BEFORE/AFTER 必须为语句级;行级只能用 INSTEAD OF。
    sql
    CREATE 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 里写子查询:当前不支持。-- 错误!
    sql
    CREATE 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_。
    sql
    CREATE TRIGGER trg_01_normalize BEFORE INSERT ON t ...;
    CREATE TRIGGER trg_02_validate  BEFORE INSERT ON t ...;
  • AFTER 触发器尽量配 WHEN:大批量 DML 下能跳过大量无意义的排队。
    sql
    CREATE 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 表达业务约束:比让底层约束报错更可控、消息更友好。
    sql
    IF NEW.qty <= 0 THEN
      RAISE EXCEPTION '数量必须为正: %', NEW.qty USING ERRCODE = '23514';
    END IF;
  • 审计/job 表写入用 AFTER,不要用 BEFORE:AFTER 时被修改的行已落定,日志更准确。
  • 触发器函数保持轻量:严禁在其中做 HTTP 调用、大范围全表扫描、睡眠等待等慢操作——它们会被放大成 N 倍。
  • 为触发器补集成测试:用 DO $$ ... $$ 或事务回滚的方式验证触发行为,尤其是顺序与返回值语义。
  • 文档化每个触发器:用 COMMENT ON TRIGGER 记录用途、参数含义与顺序约定。
    sql
    COMMENT ON TRIGGER trg_audit_products ON products IS '插入/更新/删除写入 audit_log';
  • 考虑替代方案:能用 DEFAULT / 生成列 / CHECK 约束 / ON CONFLICT 解决的,不要引入触发器——触发器是隐式行为,调试成本更高。

下一节

继续学习:BEFORE / AFTER / INSTEAD OF

参考链接 ​