触发事件:INSERT / UPDATE / DELETE / TRUNCATE
CREATE TRIGGER 的第二个必填要素是触发事件(event):在哪些 DML 操作上回调函数。PostgreSQL 支持四类事件——INSERT、UPDATE、DELETE、TRUNCATE,可以用 OR 把多个事件合并到同一个触发器定义里,也可以用 UPDATE OF col1, col2 把触发范围收窄到特定列的改动。事件决定了 OLD / NEW 哪个可用、能否指定列、以及能否与某些时机组合。
前置知识
阅读本节前,建议先了解:触发时机:BEFORE / AFTER / INSTEAD OF
基础概念
四类事件
| 事件 | 语义 | 可用于行级 | 可用于语句级 | 有 OLD | 有 NEW |
|---|---|---|---|---|---|
INSERT | 插入新行 | 是 | 是 | 否(NULL) | 是 |
UPDATE | 修改已有行 | 是 | 是 | 是 | 是 |
DELETE | 删除已有行 | 是 | 是 | 是 | 否(NULL) |
TRUNCATE | 清空整表 | 否 | 是 | 否(NULL) | 否(NULL) |
TRUNCATE 是唯一的例外:它不针对任何具体行,因此只能定义语句级触发器。
-- 正确:TRUNCATE 触发器只能是语句级
CREATE TRIGGER trg_after_truncate
AFTER TRUNCATE ON staging_rows
FOR EACH STATEMENT EXECUTE FUNCTION log_truncate();
-- 错误!TRUNCATE 没有行级触发器
CREATE TRIGGER trg_bad
BEFORE TRUNCATE ON staging_rows
FOR EACH ROW EXECUTE FUNCTION f();OLD / NEW 的可用性
INSERT: OLD ─────────────► NEW [可读,BEFORE 中可写]
UPDATE: OLD [可读] ──────► NEW [可读,BEFORE 中可写]
DELETE: OLD [可读] ──────► NEW ─────────►(无)
TRUNCATE:OLD ────► NEW ────►(都不可用,语句级)在不适用的场景访问会直接报错:
-- 错误!INSERT 触发器里没有 OLD
CREATE OR REPLACE FUNCTION bad_insert() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE '%', OLD.id; -- ERROR: record "old" is not assigned yet
RETURN NEW;
END;
$$;条件严格的写法是先判 TG_OP,再决定读谁:
IF TG_OP = 'DELETE' THEN
v_row := to_jsonb(OLD);
ELSE
v_row := to_jsonb(NEW);
END IF;事件与可用对象的组合
| 对象类型 | INSERT | UPDATE | DELETE | TRUNCATE |
|---|---|---|---|---|
| 普通表 | 可用 | 可用 | 可用 | 可用 |
| 分区表 / 继承父表 | 可用 | 可用 | 可用 | 可用 |
| 视图 | 仅 INSTEAD OF | 仅 INSTEAD OF | 仅 INSTEAD OF | 不可用 |
| 外部表 | 可用(BEFORE/AFTER 行级 + 语句级) | 同左 | 同左 | 不可用 |
语法与用法
事件在语法中的位置
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF }
{ INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE }
[ OR { INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE } ] ...
[ OR { INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE } ] ...
ON { table_name | view_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 )要点:
- 事件至少写一个,多个事件用
OR连接,顺序任意; UPDATE OF col, ...中的列只在UPDATE事件上有效,不能写成INSERT OF col;- 同一个触发器里可以混合事件,例如
AFTER INSERT OR UPDATE OR DELETE; TRUNCATE可以和INSERT/UPDATE/DELETE写在同一触发器里,但那样该触发器只能是语句级。
多事件合并的典型写法
-- 审计:三类行级事件合并,函数内用 TG_OP 区分
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_products_truncate
AFTER TRUNCATE ON products
FOR EACH STATEMENT EXECUTE FUNCTION audit_truncate();UPDATE OF 指定列
-- 只在 price 或 discount 出现在 SET 子句里时才触发
CREATE TRIGGER trg_price_changed
AFTER UPDATE OF price, discount ON products
FOR EACH ROW EXECUTE FUNCTION log_price_change();注意 UPDATE OF 判断的是「列出现在 SET 子句中」,而不是「列的值真的变了」:
-- 即使把 price 设成同一个值,触发器依然会触发
UPDATE products SET price = price WHERE sku = 'SKU-001'; -- 触发器触发
-- 而 SET 里没提到 price 时不会触发
UPDATE products SET name = '新名字' WHERE sku = 'SKU-001'; -- 触发器不触发如果要「值真的变了才处理」,需要在 WHEN 或函数体里再判断一次:
CREATE TRIGGER trg_price_changed
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change();详细说明
INSERT 事件
INSERT 触发器在插入新行时触发。行级 BEFORE INSERT 可以向 NEW 里补值——这是最常见的用法,包括补时间戳、补规范化字段、补默认业务字段。
CREATE OR REPLACE FUNCTION fill_insert_defaults() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.created_at := COALESCE(NEW.created_at, now());
NEW.created_by := COALESCE(NEW.created_by, current_user);
NEW.code := upper(trim(NEW.code));
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_fill_insert_defaults
BEFORE INSERT ON articles
FOR EACH ROW EXECUTE FUNCTION fill_insert_defaults();几个容易踩的点:
DEFAULT与BEFORE INSERT的先后:DEFAULT表达式先求值填充进NEW,随后BEFORE触发器才被调用,因此触发器看到的是「已应用默认值」的NEW,并可以覆盖它。BEFORE INSERT里没有OLD:OLD是未赋值的 record。INSERT ... ON CONFLICT DO UPDATE的复杂情况:同一行可能先走BEFORE INSERT再走BEFORE UPDATE,且BEFORE INSERT的改动会被BEFORE UPDATE看到(包括对EXCLUDED列的改动)。若触发器不幂等,结果会出乎意料。
-- ON CONFLICT 场景下,INSERT 与 UPDATE 触发器都会被调用
INSERT INTO products (sku, name, price) VALUES ('SKU-001', '苹果', 19.90)
ON CONFLICT (sku) DO UPDATE SET price = EXCLUDED.price;另外,官方明确指出:带 ON CONFLICT DO UPDATE 的 INSERT 语句,语句级 UPDATE 触发器一定会被触发,无论最终是否真的走了 UPDATE 分支、无论是否影响任何行。
UPDATE 事件
UPDATE 是唯一同时拥有可读 OLD 与可写 NEW 的事件,因此业务上用得最多。
CREATE OR REPLACE FUNCTION track_status_change() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF OLD.status IS DISTINCT FROM NEW.status THEN
INSERT INTO status_history (entity_id, from_status, to_status, changed_by)
VALUES (NEW.id, OLD.status, NEW.status, current_user);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_track_status_change
AFTER UPDATE ON tickets
FOR EACH ROW EXECUTE FUNCTION track_status_change();关键语义:
OLD是更新前的行,NEW是即将写入的行(BEFORE中可改写NEW);- 未出现在
SET子句里的列,NEW与OLD相同; OLD是只读的:OLD.price := 1;会报错。sql-- 错误!OLD 不可赋值 OLD.price := 1; -- ERROR: record "old" is not assignable- 主键/分区键变化可能触发「移动分区」语义:分区表上
UPDATE导致行跨分区移动时,内部按「从原分区DELETE+ 向新分区INSERT」执行。此时行级BEFORE UPDATE与BEFORE DELETE在原分区触发,BEFORE INSERT在目标分区触发;AFTER层面触发的是AFTER DELETE与AFTER INSERT,不触发AFTER UPDATE;语句级只触发UPDATE相关触发器。
DELETE 事件
DELETE 触发器只有 OLD 可用,且 BEFORE 行级返回非 NULL 才允许删除。
CREATE OR REPLACE FUNCTION archive_before_delete() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO orders_archive (id, sku, qty, archived_at)
VALUES (OLD.id, OLD.sku, OLD.qty, now());
RETURN OLD; -- DELETE 触发器的惯用返回值
END;
$$;
CREATE TRIGGER trg_archive_before_delete
BEFORE DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION archive_before_delete();外键级联删除也会触发 DELETE 触发器
如果外键声明了 ON DELETE CASCADE,级联删除是通过引用表上的普通 DELETE 实现的,因此引用表上的 DELETE 触发器会被触发。若该触发器修改或阻止了这些删除,就可能破坏引用完整性。这是触发器编写者需要自己负责的边界。
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint REFERENCES orders(id) ON DELETE CASCADE
);
-- DELETE FROM orders 时,order_items 上的 DELETE 触发器会被触发TRUNCATE 事件
TRUNCATE 清空整表,不涉及具体行,因此:
- 只能是
FOR EACH STATEMENT; OLD/NEW都不可用;- 常用于「清表前拦一道」或「清表后记一笔」。
CREATE OR REPLACE FUNCTION guard_truncate() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_TABLE_NAME LIKE 'prod_%' THEN
RAISE EXCEPTION '禁止 TRUNCATE 生产表 %', TG_TABLE_NAME
USING ERRCODE = '42501',
HINT = '请改用 DELETE ... WHERE 条件';
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_guard_truncate
BEFORE TRUNCATE ON staging_rows
FOR EACH STATEMENT EXECUTE FUNCTION guard_truncate();TRUNCATE 触发器拿不到「被清了多少行」的信息,也无法知道是否有 TRUNCATE ... CASCADE 连带清空了其他表——不过被级联清空的表自身的 TRUNCATE 触发器同样会被触发。
-- parent 有外键被 child 引用;TRUNCATE parent CASCADE 会连带清空 child
-- 此时 child 上的 AFTER TRUNCATE 触发器也会被触发
TRUNCATE parent CASCADE;多事件合并时如何在函数里区分
TG_OP 是唯一的判别依据,取值恰为 'INSERT' / 'UPDATE' / 'DELETE' / 'TRUNCATE':
CREATE OR REPLACE FUNCTION op_branch() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
CASE TG_OP
WHEN 'INSERT' THEN
RAISE NOTICE '插入: %', NEW.id;
RETURN NEW;
WHEN 'UPDATE' THEN
RAISE NOTICE '更新: % → %', OLD.id, NEW.id;
RETURN NEW;
WHEN 'DELETE' THEN
RAISE NOTICE '删除: %', OLD.id;
RETURN OLD;
WHEN 'TRUNCATE' THEN
RAISE NOTICE '清表: %', TG_TABLE_NAME;
RETURN NULL;
ELSE
RAISE WARNING '未知操作 %', TG_OP;
RETURN NULL;
END CASE;
END;
$$;TG_OP 与 TG_LEVEL 配合判断
合并了 TRUNCATE 的触发器必然是语句级,函数里可以用 TG_LEVEL = 'STATEMENT' 先分流,避免访问 NEW/OLD。
UPDATE OF 的判断依据
UPDATE OF 的判定发生在语句解析层,依据是 SET 子句里是否显式提到了该列,与值是否变化无关。几种边界情况:
SET 子句 | UPDATE OF price 是否触发 |
|---|---|
SET price = 100 | 触发 |
SET price = price | 触发(提到就算) |
SET name = 'x' | 不触发 |
SET name = 'x', price = price | 触发 |
SET (name, price) = (...) | 触发 |
SET name = 'x' 但表上有 BEFORE 触发器改了 price | 不触发(不重新判断) |
-- 值没变也会触发,若只想处理「真变化」,需再判一次
CREATE TRIGGER trg_of ON products
AFTER UPDATE OF price
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION f();UPDATE OF 与视图触发器、分区表配合时,列名必须是触发器所依附的表/视图上的列名。
-- 错误!列不存在时创建即报错
CREATE TRIGGER trg_of ON products
AFTER UPDATE OF nonexistent_col
FOR EACH ROW EXECUTE FUNCTION f();MERGE 与触发事件的关系
MERGE 命令没有专属触发器,它会按自身动作派发到 INSERT / UPDATE / DELETE 触发器上:
MERGE 语句
├─ WHEN MATCHED THEN UPDATE → 触发 UPDATE 触发器
├─ WHEN MATCHED THEN DELETE → 触发 DELETE 触发器
└─ WHEN NOT MATCHED THEN INSERT → 触发 INSERT 触发器判定规则细读如下:
| 层级 | 触发条件 |
|---|---|
| 语句级 | 只要 MERGE 的动作里声明了该类动作就触发,无论最终是否真的执行了 |
| 行级 | 只有真的执行了该行操作才触发 |
MERGE INTO products p
USING staging_products s ON p.sku = s.sku
WHEN MATCHED THEN UPDATE SET price = s.price -- 触发语句级 UPDATE 触发器(总是)
WHEN NOT MATCHED THEN INSERT (sku, name, price) -- 触发语句级 INSERT 触发器(总是)
VALUES (s.sku, s.name, s.price);因此可能出现「语句级 UPDATE 触发器触发了,但没有任何行级 UPDATE 触发器触发」的合法情形。
事件与时机/层级的合法组合速查
| 事件 | BEFORE 语句级 | BEFORE 行级 | AFTER 语句级 | AFTER 行级 | INSTEAD OF 行级(视图) |
|---|---|---|---|---|---|
INSERT | 是 | 是 | 是 | 是 | 是 |
UPDATE | 是 | 是 | 是 | 是 | 是 |
UPDATE OF col | 是 | 是 | 是 | 是 | 是(列的语义类似 UPDATE OF) |
DELETE | 是 | 是 | 是 | 是 | 是 |
TRUNCATE | 是 | 否 | 是 | 否 | 否(视图不支持) |
事件与过渡表的关系
过渡表(REFERENCING OLD TABLE AS ... / NEW TABLE AS ...)的可请求范围由事件决定:
| 事件 | OLD TABLE | NEW TABLE |
|---|---|---|
INSERT | 不可请求(报错) | 可用 |
UPDATE | 可用 | 可用 |
DELETE | 可用 | 不可请求(报错) |
-- 正确:UPDATE 同时请求两张过渡表
CREATE TRIGGER trg_diff
AFTER UPDATE ON products
REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows
FOR EACH STATEMENT EXECUTE FUNCTION diff_rows();-- 错误!INSERT 没有 OLD TABLE
CREATE TRIGGER trg_bad
AFTER INSERT ON products
REFERENCING OLD TABLE AS old_rows
FOR EACH STATEMENT EXECUTE FUNCTION f();事件能否被禁用
触发器可以按名称或类别禁用,这在大批量导入时很常用:
| 命令 | 作用范围 |
|---|---|
ALTER TABLE t DISABLE TRIGGER name | 指定触发器 |
ALTER TABLE t DISABLE TRIGGER USER | 所有用户定义的触发器 |
ALTER TABLE t DISABLE TRIGGER ALL | 含内部约束触发器(需超级用户) |
ALTER TABLE t ENABLE TRIGGER name | 启用 |
ALTER TABLE big_table DISABLE TRIGGER USER;
COPY big_table FROM '/data/dump.csv' WITH (FORMAT csv);
ALTER TABLE big_table ENABLE TRIGGER USER;禁用 TRIGGER ALL 的风险
DISABLE TRIGGER ALL 会连外键的内部约束触发器一起禁用,期间可写入破坏引用完整性的数据。除极少数数据修复场景外不要使用。
实战示例
场景:一个审计函数覆盖三张表的增删改
需求:customers、products、orders 三张表都要审计「谁在什么时候对哪一行做了什么」,同时禁不掉 TRUNCATE 的审计。
CREATE TABLE audit_trail (
id bigserial PRIMARY KEY,
schema_name text NOT NULL,
table_name text NOT NULL,
op text NOT NULL,
db_user text NOT NULL,
at timestamptz NOT NULL DEFAULT now(),
row_before jsonb,
row_after jsonb
);
CREATE TABLE customers (
id bigserial PRIMARY KEY,
name text NOT NULL,
email text NOT NULL
);
CREATE TABLE products (
sku text PRIMARY KEY,
name text NOT NULL,
price numeric(10,2) NOT NULL
);
CREATE TABLE orders (
id bigserial PRIMARY KEY,
sku text NOT NULL,
qty int NOT NULL
);
CREATE OR REPLACE FUNCTION audit_row_change() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_before jsonb;
v_after jsonb;
BEGIN
IF TG_OP IN ('UPDATE', 'DELETE') THEN
v_before := to_jsonb(OLD);
END IF;
IF TG_OP IN ('INSERT', 'UPDATE') THEN
v_after := to_jsonb(NEW);
END IF;
INSERT INTO audit_trail (schema_name, table_name, op, db_user, row_before, row_after)
VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_OP, current_user, v_before, v_after);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
-- 行级:三个事件合并
CREATE TRIGGER trg_audit_customers
AFTER INSERT OR UPDATE OR DELETE ON customers
FOR EACH ROW EXECUTE FUNCTION audit_row_change();
CREATE TRIGGER trg_audit_products
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW EXECUTE FUNCTION audit_row_change();
CREATE TRIGGER trg_audit_orders
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_row_change();
-- 语句级:TRUNCATE 单独定义(无 OLD/NEW)
CREATE OR REPLACE FUNCTION audit_truncate() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_trail (schema_name, table_name, op, db_user)
VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_OP, current_user);
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_audit_orders_truncate
AFTER TRUNCATE ON orders
FOR EACH STATEMENT EXECUTE FUNCTION audit_truncate();INSERT INTO customers (name, email) VALUES ('张三', 'z@example.com');
INSERT 0 1
INSERT INTO products VALUES ('SKU-001', '苹果', 19.90);
INSERT 0 1
UPDATE products SET price = 21.90 WHERE sku = 'SKU-001';
UPDATE 1
INSERT INTO orders (sku, qty) VALUES ('SKU-001', 2);
INSERT 0 1
DELETE FROM orders WHERE qty = 2;
DELETE 1
TRUNCATE orders;
TRUNCATE TABLE
SELECT table_name, op, db_user,
row_before -> 'price' AS old_price,
row_after -> 'price' AS new_price
FROM audit_trail ORDER BY id;
table_name | op | db_user | old_price | new_price
------------+----------+----------+-----------+-----------
customers | INSERT | postgres | |
products | INSERT | postgres | | 19.90
products | UPDATE | postgres | 19.90 | 21.90
orders | INSERT | postgres | |
orders | DELETE | postgres | |
orders | TRUNCATE | postgres | |
(6 rows)同一个函数服务三张表、三类行级事件,TRUNCATE 因为拿不到行数据而单独用一个语句级触发器。
场景:用 UPDATE OF 把昂贵的联动收窄到必要列
需求:商品价格变化时需要重算该商品在所有价目表中的展示价,这是个昂贵操作;但 products 表频繁因改名、改描述而更新,若不收窄事件会浪费大量计算。
CREATE TABLE price_books (
id bigserial PRIMARY KEY,
book text NOT NULL,
sku text NOT NULL,
final_price numeric(12,2) NOT NULL
);
-- 昂贵的重算函数
CREATE OR REPLACE FUNCTION recalc_price_books() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_rule price_books_rule%ROWTYPE;
BEGIN
FOR v_rule IN SELECT * FROM price_books_rule WHERE active LOOP
UPDATE price_books pb
SET final_price = round(NEW.price * v_rule.factor, 2)
WHERE pb.sku = NEW.sku AND pb.book = v_rule.book;
IF NOT FOUND THEN
INSERT INTO price_books (book, sku, final_price)
VALUES (v_rule.book, NEW.sku, round(NEW.price * v_rule.factor, 2));
END IF;
END LOOP;
RETURN NULL;
END;
$$;
CREATE TABLE price_books_rule (
book text PRIMARY KEY,
factor numeric(6,4) NOT NULL,
active boolean NOT NULL DEFAULT true
);
-- 只在 price 列出现在 SET 中时才重算,且要求值确实变了
CREATE TRIGGER trg_recalc_price_books
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION recalc_price_books();INSERT INTO price_books_rule VALUES ('零售', 1.0000, true), ('会员', 0.9500, true);
INSERT INTO products VALUES ('SKU-001', '苹果', 20.00);
-- 插入不触发(触发器只挂在 UPDATE OF price 上)
UPDATE products SET name = '红富士苹果' WHERE sku = 'SKU-001';
UPDATE 1
-- 触发器未触发:SET 里没有 price
UPDATE products SET price = 25.00 WHERE sku = 'SKU-001';
UPDATE 1
-- 触发器触发,重算两个价目表
UPDATE products SET price = 25.00 WHERE sku = 'SKU-001';
UPDATE 1
-- 值没变,被 WHEN 条件拦住,函数未执行
SELECT book, sku, final_price FROM price_books ORDER BY book;
book | sku | final_price
--------+---------+-------------
会员 | SKU-001 | 23.75
零售 | SKU-001 | 25.00
(2 rows)UPDATE OF price 负责「减少不必要的排队」,WHEN (OLD.price IS DISTINCT FROM NEW.price) 负责「排除值未变的伪更新」,两层收窄叠加后,昂贵的重算只在真正需要时执行。
注意事项
- 在
INSERT事件里访问OLD:OLD未赋值,运行时报错。-- 错误!sqlCREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW EXECUTE FUNCTION f(); -- f 里用 OLD.col 会报错 - 在
DELETE事件里访问NEW:同理,NEW是 NULL。-- 错误!sqlIF TG_OP = 'DELETE' THEN RETURN NEW; END IF; -- 会跳过删除 - 把
TRUNCATE和行级事件写进同一个触发器:会因层级不合法而创建失败。-- 错误!sql正确做法是拆成两个触发器:行级一个、CREATE TRIGGER t AFTER INSERT OR TRUNCATE ON x FOR EACH ROW ...; -- ERRORTRUNCATE语句级一个。 - 误以为
UPDATE OF col判断的是「值变化」:它判断的是「列出现在SET里」。-- 不推荐sql需要值变化判断时必须补UPDATE products SET price = price; -- 触发器照样触发WHEN (OLD.col IS DISTINCT FROM NEW.col)。 UPDATE OF写了不存在的列:创建直接报错。-- 错误!sqlCREATE TRIGGER t AFTER UPDATE OF no_such_col ON x ...; -- ERROR- 用
INSERT OF语法:不存在这种写法,OF只属于UPDATE。-- 错误!sqlCREATE TRIGGER t AFTER INSERT OF name ON x ...; -- ERROR: syntax error - 在视图上建
INSERT/UPDATE/DELETE事件却用BEFORE/AFTER行级:视图行级只能用INSTEAD OF。-- 错误! - 忽略
MERGE的语句级触发行为:只要MERGE的动作里写了UPDATE,语句级UPDATE触发器就会触发,即使一行都没更新。依赖「语句级触发器意味着有行被改」的假设会出错。 TRUNCATE ... CASCADE连带清空其他表时忘了清理联动数据:被级联表的触发器会被触发,但主表触发器拿不到「连带清了谁」的信息。DISABLE TRIGGER ALL用于大批量导入:会同时禁用约束触发器,导入期间数据可违反引用完整性。-- 危险
最佳实践
- 一个函数服务多个事件:用
TG_OP分流,减少函数数量与维护面。sqlCASE TG_OP WHEN 'INSERT' THEN RETURN NEW; WHEN 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END CASE; - 审计类需求把
TRUNCATE单独建语句级触发器,不要试图与行级事件合并。 - 昂贵联动务必用
UPDATE OF收窄事件:把「所有 UPDATE」变成「只有关键列被写时才处理」。sqlCREATE TRIGGER t AFTER UPDATE OF price, cost ON products FOR EACH ROW EXECUTE FUNCTION f(); UPDATE OF一律配WHEN ... IS DISTINCT FROM:OF拦的是「语句提到了列」,WHEN拦的是「值真的变了」,两者叠加才精准。- 事件列表按
INSERT OR UPDATE OR DELETE的固定顺序书写:便于代码审查时快速比对。 - 审计表里同时记录
row_before与row_after:用to_jsonb(OLD)/to_jsonb(NEW)保存完整快照,比逐列记录更耐表结构变更。 TRUNCATE触发器优先做「拦截」而非「记录」:清表是危险操作,用RAISE EXCEPTION拦住生产表的误操作价值最高。sqlRAISE EXCEPTION '禁止 TRUNCATE %', TG_TABLE_NAME USING ERRCODE = '42501';- 外键引用的表上写
DELETE触发器时,记得级联删除也会触发:不要在DELETE触发器里假设「只有显式 DELETE 才进这里」。 - 大批量导入前评估是否禁用触发器,并对禁用窗口内的数据一致性做补偿校验。sql
ALTER TABLE t DISABLE TRIGGER USER; -- COPY ... ALTER TABLE t ENABLE TRIGGER USER; - 为每个触发器写清楚它监听哪些事件:用
COMMENT ON TRIGGER标注,避免后人漏加事件。sqlCOMMENT ON TRIGGER trg_recalc_price_books ON products IS 'AFTER UPDATE OF price:价格变化时重算各价目表展示价'; - 在测试里覆盖「零行受影响」的场景:语句级触发器在 0 行时也会执行,行级不会;这是常见误解点。sql
UPDATE products SET price = 1 WHERE sku = 'NOT-EXIST'; -- UPDATE 0,但语句级触发器已执行 - 优先用
CHECK/ 唯一约束代替「拦 INSERT/UPDATE」的触发器:约束是声明式的、可被优化器利用,且不会带来隐式函数调用开销。
下一节
继续学习:行级触发器 FOR EACH ROW