触发时机:BEFORE / AFTER / INSTEAD OF
定义触发器时必须回答三个问题:在什么事件上触发(INSERT / UPDATE / DELETE / TRUNCATE)、按行还是按语句触发(FOR EACH ROW / FOR EACH STATEMENT)、以及在操作之前还是之后触发(BEFORE / AFTER / INSTEAD OF)。本节专讲第三个问题——时机决定了触发器能否修改数据、返回值是否被采纳、以及它能看到什么样的行状态,是本组文档里最需要吃透的一节。
前置知识
阅读本节前,建议先了解:触发器概述
基础概念
时机在语句生命周期中的位置
一次 UPDATE 从下达到完成,各种触发器的相对顺序如下:
客户端发出:UPDATE t SET ... WHERE ...;
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 语句级 BEFORE 触发器 (语句开始前,一次) │
└─────────────────────────────────────────────────────────────┘
│
▼ 开始逐行处理
┌─────────────────────────────────────────────────────────────┐
│ 行级 BEFORE 触发器(按名称字母序,可链式改行) │
│ ├─ 返回 NULL → 跳过本行,后续同行的 BEFORE 不再执行 │
│ └─ 返回非 NULL → 以返回值作为本行最终要写入的值 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 实际写行 + 生成列计算(stored generated columns) │
│ ★ 生成列在 BEFORE 之后、AFTER 之前算出 │
└─────────────────────────────────────────────────────────────┘
│
▼ 整个语句的所有行都处理完
┌─────────────────────────────────────────────────────────────┐
│ 行级 AFTER 触发器(按名称字母序,每行一次,返回值被忽略) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 语句级 AFTER 触发器 (语句结束时,一次) │
└─────────────────────────────────────────────────────────────┘
│
▼
(若为可延迟的约束触发器)推迟到 COMMIT 前再执行关键结论:行级 AFTER 不是在「这一行写完」时立刻执行,而是在整个语句结束时才执行。这一点经常被误解,也直接影响「AFTER 触发器里查表能看到什么」。
时机 × 层级 组合矩阵
| 时机 | FOR EACH ROW | FOR EACH STATEMENT | 可用对象 |
|---|---|---|---|
BEFORE | 可改 NEW、可跳过该行 | 语句开始前执行一次 | 表、外部表、分区表 |
AFTER | 行值已定,返回值被忽略 | 语句结束时执行一次 | 表、外部表、分区表 |
INSTEAD OF | 唯一允许的层级 | 不允许 | 仅视图 |
不合法组合在创建时就会报错:
-- 错误!视图上的 BEFORE / AFTER 只能是语句级
CREATE TRIGGER t BEFORE INSERT ON my_view FOR EACH ROW EXECUTE FUNCTION f();
-- 错误!INSTEAD OF 不支持语句级
CREATE TRIGGER t INSTEAD OF INSERT ON my_view FOR EACH STATEMENT EXECUTE FUNCTION f();
-- 错误!INSTEAD OF 只能用于视图,不能用于表
CREATE TRIGGER t INSTEAD OF INSERT ON my_table FOR EACH ROW EXECUTE FUNCTION f();一句话分工
| 需求 | 首选时机 | 理由 |
|---|---|---|
| 校验数据合法性 | BEFORE | 能在写入前抛错,可用 NEW 直接判断 |
| 计算派生列、补默认值 | BEFORE | 可以直接改 NEW 并返回 |
| 有条件地忽略某行写入 | BEFORE | 返回 NULL 即可跳过 |
| 写审计日志 | AFTER | 此时行值已最终确定,不会记到「被后续触发器改过的中间态」 |
| 维护汇总表 / 计数器 | AFTER | 需要确认主操作确实发生了 |
| 跨表一致性核查 | AFTER | 能看到最终值,避免误判 |
| 让视图可写 | INSTEAD OF | 唯一选择 |
| 跨行、跨表的全局约束 | AFTER + CONSTRAINT | 可推迟到事务提交时判断 |
语法与用法
三种时机的语法位置
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 } ] ...
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 )时机(①)与层级(②)是两个独立维度,但它们的组合受对象类型约束,见上表。
触发器函数返回值一览
不同时机的返回值语义差异巨大,值得单独成表:
| 触发器类型 | 返回值是否被采纳 | 返回 NULL 的含义 |
|---|---|---|
BEFORE 行级(INSERT/UPDATE) | 是,作为该行最终值 | 跳过本行操作 |
BEFORE 行级(DELETE) | 无直接影响,但必须非 NULL | 跳过本行删除 |
AFTER 行级 | 否,被忽略 | 无影响 |
BEFORE 语句级 | 否,被忽略 | 无影响 |
AFTER 语句级 | 否,被忽略 | 无影响 |
INSTEAD OF | 是,影响 RETURNING 与影响行数 | 表示「未做任何修改」,该行不计数 |
一个对照示例
CREATE TABLE item (
id bigserial PRIMARY KEY,
name text,
price numeric(10,2),
name_up text, -- 由 BEFORE 触发器补全
price_vat numeric(12,2) -- 由生成列算出
);
-- BEFORE 行级:补全 name_up,并校验价格
CREATE OR REPLACE FUNCTION item_normalize() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.price IS NULL OR NEW.price < 0 THEN
RAISE EXCEPTION '价格非法: %', NEW.price USING ERRCODE = '23514';
END IF;
NEW.name_up := upper(NEW.name);
RETURN NEW; -- 必须返回,否则该行被跳过
END;
$$;
CREATE TRIGGER trg_item_normalize
BEFORE INSERT OR UPDATE ON item
FOR EACH ROW EXECUTE FUNCTION item_normalize();
-- AFTER 行级:审计(返回值被忽略,返回 NULL 即可)
CREATE OR REPLACE FUNCTION item_audit() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE 'item 发生 % : id=%', TG_OP, COALESCE(NEW.id, OLD.id);
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_item_audit
AFTER INSERT OR UPDATE OR DELETE ON item
FOR EACH ROW EXECUTE FUNCTION item_audit();详细说明
BEFORE 行级:唯一能改写数据的时机
BEFORE 行级触发器在「某一行即将被操作」的瞬间被调用。此时 NEW 是一个可写的记录变量,直接赋值即可改变最终写入的值:
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
NEW.updated_by := current_user;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_touch BEFORE UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION set_updated_at();也可以整体替换 NEW,或构造一个全新的行记录返回。但要注意:返回的行必须与目标表结构完全一致,否则运行时报错。
-- 整体替换 NEW 的写法(字段数量/类型必须与表一致)
NEW := ROW(NEW.id, trim(NEW.title), NEW.body, now())::articles;另外,BEFORE INSERT 触发器可以覆盖 DEFAULT 已生成的值;而对生成列(generated column)的修改会被忽略并覆盖:
-- 在 BEFORE 触发器里改生成列是无效的
NEW.price_vat := 0; -- 会被随后执行的生成列计算覆盖BEFORE 的返回值语义
返回值是 BEFORE 触发器最容易被忽略的细节:
-- 返回 NULL → 跳过本行
CREATE OR REPLACE FUNCTION skip_archived() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.status = 'archived' THEN
RETURN NULL; -- 本行不会被插入/更新,后续同行的 BEFORE 触发器也不再执行
END IF;
RETURN NEW;
END;
$$;- 返回
NULL:跳过当前行的操作,且后续同事件的BEFORE/INSTEAD OF触发器不再被执行。 - 返回非
NULL:操作继续,返回值作为该行的最终值传给下一个触发器或执行器。 - 忘记
RETURN:PL/pgSQL 函数结尾没有RETURN时返回NULL,效果等同「静默跳过所有行」——这是线上事故的常见成因。
-- 错误!忘记 RETURN,导致所有行都被跳过
CREATE OR REPLACE FUNCTION bad_touch() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
-- 少写了 RETURN NEW;
END;
$$;BEFORE 行级 DELETE 的特殊之处
DELETE 触发器里没有 NEW(它是 NULL),只有 OLD。虽然返回值对删除行为没有直接影响,但仍必须返回非 NULL,否则该行不会被删除:
CREATE OR REPLACE FUNCTION guard_delete() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF OLD.is_locked THEN
RETURN NULL; -- 跳过本行删除
END IF;
RETURN OLD; -- 惯用写法:DELETE 触发器返回 OLD
END;
$$;在 DELETE 触发器里返回 NEW
NEW 在 DELETE 触发器里是 NULL,返回它等价于「跳过本行删除」。这类 bug 不会报错,只会悄悄少删数据。
AFTER:值已落定,但只能抛错不能改值
AFTER 行级触发器在整个语句的所有行都处理完毕后,逐行被调用。此时:
- 行已经写入(或删除),
NEW/OLD都是最终确定的值; - 返回值被完全忽略;
- 不能修改
NEW(改了也不会生效); - 唯一的干预手段是
RAISE EXCEPTION,它会让整个语句连同触发器自身的副作用一起回滚。
-- AFTER 触发器修改 NEW 是无效操作
CREATE OR REPLACE FUNCTION useless_after() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.status := 'checked'; -- 不推荐:毫无效果
RETURN NEW; -- 返回值同样被忽略
END;
$$;由于 AFTER 看到的是最终值,跨表校验只能写在 AFTER:
-- 校验订单明细总额不超过订单头的额度(需要最终值)
CREATE OR REPLACE FUNCTION check_order_total() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_total numeric;
v_limit numeric;
BEGIN
SELECT sum(amount), max(o.credit_limit)
INTO v_total, v_limit
FROM order_items i
JOIN orders o ON o.id = i.order_id
WHERE i.order_id = NEW.order_id
GROUP BY o.id;
IF v_total > v_limit THEN
RAISE EXCEPTION '订单 % 总额 % 超出额度 %',
NEW.order_id, v_total, v_limit USING ERRCODE = '23514';
END IF;
RETURN NULL;
END;
$$;AFTER 行级触发器的执行时机
它不是在「本行写完」时执行,而是在语句末尾执行。因此在 AFTER 触发器里 SELECT 目标表,能看到本条语句影响的全部行(因为都已写入)。如果误以为它是「逐行即时执行」,很容易写出错误的聚合逻辑。
语句级 BEFORE / AFTER
语句级触发器与具体行无关,NEW/OLD 在其中都是 NULL:
| 类型 | 执行时刻 | 典型用途 |
|---|---|---|
BEFORE ... FOR EACH STATEMENT | 语句开始前,一次 | 准备临时数据、记录开始时间、检查前置条件 |
AFTER ... FOR EACH STATEMENT | 语句结束后,一次 | 批量审计(配过渡表)、刷新汇总、发通知 |
-- 语句级 AFTER:统计总影响行数(用 GET DIAGNOSTICS 拿不到跨触发器行数,此处演示计时)
CREATE OR REPLACE FUNCTION log_bulk_load() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- NEW/OLD 在语句级触发器里是 NULL,访问会报错
RAISE NOTICE '批量导入结束: op=% table=%',
TG_OP, TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_log_bulk_load
AFTER INSERT ON staging_rows
FOR EACH STATEMENT EXECUTE FUNCTION log_bulk_load();语句级 AFTER 总是排在行级 AFTER 之后,这是官方定义的固定次序。
INSTEAD OF:视图的可写代理
INSTEAD OF 触发器只用于视图,且只允许行级。它的语义不是「在操作之前/之后」,而是「取代这个操作」——视图本身没有可写存储,真正要改的是底层基表:
CREATE TABLE emp (
empname text PRIMARY KEY,
salary integer
);
CREATE TABLE emp_audit (
operation char(1) NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer,
stamp timestamp NOT NULL
);
CREATE VIEW emp_view AS
SELECT e.empname, e.salary, max(ea.stamp) AS last_updated
FROM emp e
LEFT JOIN emp_audit ea ON ea.empname = e.empname
GROUP BY 1, 2;
CREATE OR REPLACE FUNCTION update_emp_view() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
DELETE FROM emp WHERE empname = OLD.empname;
IF NOT FOUND THEN
RETURN NULL; -- 没删到任何行 → 本行不计入影响行数
END IF;
OLD.last_updated := now();
INSERT INTO emp_audit VALUES ('D', current_user, OLD.*);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
IF NOT FOUND THEN
RETURN NULL;
END IF;
NEW.last_updated := now();
INSERT INTO emp_audit VALUES ('U', current_user, NEW.*);
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp VALUES (NEW.empname, NEW.salary);
NEW.last_updated := now();
INSERT INTO emp_audit VALUES ('I', current_user, NEW.*);
RETURN NEW;
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
FOR EACH ROW EXECUTE FUNCTION update_emp_view();INSTEAD OF 的返回值有明确含义:
- 返回
NULL:声明「我没做任何改动」,该行不计入INSERT/UPDATE/DELETE的影响行数; - 返回非 NULL:声明「操作已完成」,影响行数
+1; - 对
INSERT/UPDATE,返回的NEW会决定RETURNING输出什么(可以在返回前修改NEW)。
视图上的语句级触发器有个前提
视图上可以定义语句级 BEFORE/AFTER 触发器,但它们只有在视图同时存在 INSTEAD OF 触发器时才会被触发。否则对视图的语句会被改写到基表,触发的是基表上的触发器。
INSTEAD OF 不支持 WHEN 条件
想按条件决定是否代写,只能在函数体里写 IF 判断。
约束触发器:把 AFTER 推迟到提交
在 CREATE TRIGGER 前加 CONSTRAINT 关键字,得到约束触发器。它必须是 AFTER ROW,并且可以声明为可延迟,从而在 COMMIT 前才逐个检查:
CREATE CONSTRAINT TRIGGER trg_budget_check
AFTER INSERT OR UPDATE ON ledger_entries
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION check_daily_budget_later();延迟的意义在于:事务中间态可能暂时违反约束,只要提交时满足即可。比如「先插入明细行、后更新汇总行」的两步操作,用即时检查会误报。
-- 事务内可临时切换延迟/立即
SET CONSTRAINTS trg_budget_check IMMEDIATE; -- 立刻检查(用于调试)
SET CONSTRAINTS ALL DEFERRED; -- 全部推迟到提交约束触发器可以配置为不延迟(NOT DEFERRABLE),此时行为与普通 AFTER ROW 触发器一致。
生成列与时机的关系
存储生成列(GENERATED ALWAYS AS (...) STORED)的计算发生在 BEFORE 触发器之后、AFTER 触发器之前:
BEFORE 触发器(改 NEW 的普通列)
↓
生成列计算(根据最终的普通列算出)
↓
AFTER 触发器(能读到生成列的值)★由此得到两条使用规则:
-- 正确:AFTER 触发器里可以放心读生成列
CREATE OR REPLACE FUNCTION read_generated() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.total_with_tax > 100000 THEN
RAISE WARNING '金额异常: %', NEW.total_with_tax;
END IF;
RETURN NULL;
END;
$$;-- 不推荐:BEFORE 触发器里 NEW 的生成列尚未计算,值是未定义的
CREATE OR REPLACE FUNCTION read_generated_early() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE NOTICE '%', NEW.total_with_tax; -- 不要这样写
RETURN NEW;
END;
$$;虚拟生成列(virtual generated column)在触发器执行时永远不会被计算,触发器函数中不应访问。
时机与 WHEN 条件的求值点
WHEN 条件的求值时机随时机不同而不同,这直接影响性能:
| 触发器类型 | WHEN 求值时机 | 不满足时的代价 |
|---|---|---|
BEFORE | 紧接在函数可能执行之前 | 省下一次函数调用 |
AFTER | 行更新刚完成时 | 不排队该事件,语句结束时不需回查该行 |
INSTEAD OF | 不支持 | — |
对 AFTER 触发器来说,WHEN 的收益远大于 BEFORE:
-- AFTER + WHEN:只有价格真的变了才排队审计,大批量 UPDATE 下提速明显
CREATE TRIGGER trg_price_audit
AFTER UPDATE ON products
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION audit_price_change();-- 等价但不推荐:把判断挪进函数体,事件仍会被排队
CREATE TRIGGER trg_price_audit
AFTER UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION audit_price_change(); -- 函数体里再判断,白排队了触发器执行顺序与链式传递
同一个事件上定义多个触发器时:
- 按触发器名称的字母序执行,与创建先后无关;
BEFORE与INSTEAD OF的返回值会链式传递给下一个触发器;- 任何一个
BEFORE/INSTEAD OF返回NULL,该行的后续触发器全部不再执行。
-- 字母序:trg_01_* → trg_02_* → trg_03_*
CREATE TRIGGER trg_01_trim BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION f1();
CREATE TRIGGER trg_02_lower BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION f2();
CREATE TRIGGER trg_03_validate BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION f3();用等宽数字前缀命名,是把「执行顺序」变成显式契约的最简单办法。
时机选择的经验法则
需要修改行数据? → BEFORE 行级
需要直接拒绝某一行? → BEFORE 行级返回 NULL
需要看到最终值? → AFTER
需要跨行/跨表校验? → AFTER(必要时加 CONSTRAINT 延迟)
需要让视图可写? → INSTEAD OF
没有特别理由? → BEFORE 更省(无需把事件排到语句末尾)官方文档对此的表述很直接:如果没有特定理由选择 AFTER,BEFORE 更高效,因为操作信息不必保留到语句末尾。
实战示例
场景:库存扣减的 BEFORE 校验 + AFTER 告警
需求:stock_moves 记录出入库流水,要求:
- 出库数量不能超过当前库存(
BEFORE校验,写入前拦住); - 写入后若某 SKU 库存低于安全线,记一条告警(
AFTER,此时库存已是最终值)。
CREATE TABLE inventory_sku (
sku text PRIMARY KEY,
on_hand int NOT NULL DEFAULT 0,
safety_line int NOT NULL DEFAULT 0
);
CREATE TABLE stock_moves (
id bigserial PRIMARY KEY,
sku text NOT NULL REFERENCES inventory_sku(sku),
qty int NOT NULL, -- 正数入库,负数出库
moved_at timestamptz NOT NULL DEFAULT now(),
note text
);
CREATE TABLE stock_alerts (
id bigserial PRIMARY KEY,
sku text NOT NULL,
on_hand int NOT NULL,
at timestamptz NOT NULL DEFAULT now()
);
-- BEFORE 行级:校验出库量
CREATE OR REPLACE FUNCTION stock_move_check() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_on_hand int;
BEGIN
IF NEW.qty = 0 THEN
RAISE EXCEPTION '库存变动数量不能为 0'
USING ERRCODE = '23514', COLUMN = 'qty', TABLE = 'stock_moves';
END IF;
SELECT on_hand INTO v_on_hand
FROM inventory_sku WHERE sku = NEW.sku FOR UPDATE;
IF NEW.qty < 0 AND v_on_hand + NEW.qty < 0 THEN
RAISE EXCEPTION 'SKU % 库存不足:现有 %,需出库 %',
NEW.sku, v_on_hand, -NEW.qty
USING ERRCODE = '23514', COLUMN = 'qty';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_stock_move_check
BEFORE INSERT ON stock_moves
FOR EACH ROW EXECUTE FUNCTION stock_move_check();
-- BEFORE 行级:同步库存
CREATE OR REPLACE FUNCTION stock_move_apply() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
UPDATE inventory_sku
SET on_hand = on_hand + NEW.qty
WHERE sku = NEW.sku;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_stock_move_apply
BEFORE INSERT ON stock_moves
FOR EACH ROW EXECUTE FUNCTION stock_move_apply();
-- AFTER 行级:低库存告警(读到的是最终库存)
CREATE OR REPLACE FUNCTION stock_low_alert() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_on_hand int;
v_line int;
BEGIN
SELECT on_hand, safety_line INTO v_on_hand, v_line
FROM inventory_sku WHERE sku = NEW.sku;
IF v_on_hand < v_line THEN
INSERT INTO stock_alerts (sku, on_hand) VALUES (NEW.sku, v_on_hand);
END IF;
RETURN NULL; -- AFTER 触发器返回值被忽略
END;
$$;
CREATE TRIGGER trg_stock_low_alert
AFTER INSERT ON stock_moves
FOR EACH ROW EXECUTE FUNCTION stock_low_alert();INSERT INTO inventory_sku VALUES ('SKU-A', 100, 20);
INSERT INTO stock_moves (sku, qty) VALUES ('SKU-A', 50);
INSERT 0 1
INSERT INTO stock_moves (sku, qty) VALUES ('SKU-A', -55);
ERROR: SKU SKU-A 库存不足:现有 150,需出库 55
-- (上一条已把库存加到 150,此条再出库 55 其实是合法的,示例演示校验路径)
INSERT INTO stock_moves (sku, qty) VALUES ('SKU-A', -200);
ERROR: SKU SKU-A 库存不足:现有 150,需出库 200
INSERT INTO stock_moves (sku, qty) VALUES ('SKU-A', -140);
INSERT 0 1
SELECT sku, on_hand, safety_line FROM inventory_sku;
sku | on_hand | safety_line
-------+---------+-------------
SKU-A | 10 | 20
SELECT sku, on_hand FROM stock_alerts;
sku | on_hand
-------+---------
SKU-A | 10
(1 row)注意两个 BEFORE 触发器按名称字母序执行:trg_stock_move_apply 先于 trg_stock_move_check(a < c)——这说明命名顺序即执行顺序,校验与落地操作的先后必须靠命名控制,不能靠猜测。
场景:用 INSTEAD OF 把多表联接视图变成可写接口
需求:给 PHP 应用暴露一个「用户 + 最后登录时间」的视图,应用直接往视图里 INSERT,由触发器分发到底层两张表。
CREATE TABLE app_user (
id bigserial PRIMARY KEY,
username text NOT NULL UNIQUE,
email text NOT NULL
);
CREATE TABLE user_login (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL REFERENCES app_user(id),
last_ip inet,
login_at timestamptz NOT NULL DEFAULT now()
);
CREATE VIEW v_user_login AS
SELECT u.id, u.username, u.email,
max(l.login_at) AS last_login_at
FROM app_user u
LEFT JOIN user_login l ON l.user_id = u.id
GROUP BY u.id, u.username, u.email;
CREATE OR REPLACE FUNCTION v_user_login_write() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
v_id bigint;
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO app_user (username, email)
VALUES (NEW.username, NEW.email)
RETURNING id INTO v_id;
INSERT INTO user_login (user_id, login_at)
VALUES (v_id, COALESCE(NEW.last_login_at, now()));
NEW.id := v_id;
RETURN NEW; -- 非 NULL → 计入影响行数,并决定 RETURNING 内容
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE app_user
SET username = NEW.username, email = NEW.email
WHERE id = OLD.id;
IF NOT FOUND THEN
RETURN NULL; -- 目标行不存在 → 声明「未做修改」
END IF;
INSERT INTO user_login (user_id, login_at)
VALUES (OLD.id, COALESCE(NEW.last_login_at, now()));
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
DELETE FROM user_login WHERE user_id = OLD.id;
DELETE FROM app_user WHERE id = OLD.id;
IF NOT FOUND THEN
RETURN NULL;
END IF;
RETURN OLD;
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_v_user_login_write
INSTEAD OF INSERT OR UPDATE OR DELETE ON v_user_login
FOR EACH ROW EXECUTE FUNCTION v_user_login_write();INSERT INTO v_user_login (username, email, last_login_at)
VALUES ('alice', 'alice@example.com', '2026-09-01 10:00:00')
RETURNING id, username, last_login_at;
id | username | last_login_at
----+----------+---------------------
1 | alice | 2026-09-01 10:00:00
(1 row)
INSERT 0 1
UPDATE v_user_login SET email = 'alice@new.com' WHERE username = 'alice';
UPDATE 1
DELETE FROM v_user_login WHERE username = 'alice';
DELETE 1
SELECT count(*) FROM app_user;
count
-------
0
(1 row)RETURNING 能返回正确结果,是因为 INSTEAD OF 触发器返回了非 NULL 的 NEW/OLD。若返回 NULL,会看到 INSERT 0 0,应用侧会误判为「一行都没写成功」。
注意事项
BEFORE行级忘记RETURN NEW:函数末尾无RETURN时返回NULL,会静默跳过所有行。-- 错误!sqlCREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at := now(); END; -- 少了 RETURN NEW; → 每次 UPDATE 都是 UPDATE 0 $$;- 在
AFTER触发器里试图修改NEW:改动不生效,且不会报错。-- 错误!sqlCREATE TRIGGER t AFTER UPDATE ON x FOR EACH ROW EXECUTE FUNCTION f(); -- f 里 NEW.a := 1 无效 DELETE触发器里返回NEW:NEW为 NULL,等价于跳过删除。-- 错误!sqlIF TG_OP = 'DELETE' THEN RETURN NEW; END IF; -- 应返回 OLD- 在视图上写
BEFORE/AFTER行级触发器:视图的行级只能INSTEAD OF。-- 错误!sqlCREATE TRIGGER t BEFORE INSERT ON v FOR EACH ROW ...; -- ERROR CREATE TRIGGER t INSTEAD OF INSERT ON v FOR EACH ROW ...; -- 正确 - 在语句级触发器里访问
NEW/OLD:它们是 NULL,运行时报record "new" is not assigned yet。-- 错误!sqlCREATE TRIGGER t AFTER INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION f(); -- f 里用 NEW.col 会报错 - 在视图上建语句级触发器却没建
INSTEAD OF:该语句级触发器不会被触发。-- 不推荐sql-- 单独存在时形同虚设 CREATE TRIGGER t AFTER INSERT ON v FOR EACH STATEMENT EXECUTE FUNCTION f(); - 依赖「行级
AFTER是逐行即时执行」的错误假设:它在语句末尾才跑,此时表里已有全部行的最终值,聚合结果与直觉可能不同。 INSTEAD OF与WHEN搭配:不支持,创建即报错。-- 错误!sqlCREATE TRIGGER t INSTEAD OF INSERT ON v FOR EACH ROW WHEN (NEW.a > 0) EXECUTE FUNCTION f(); -- ERRORTRUNCATE上建行级触发器:TRUNCATE只能语句级。-- 错误!sqlCREATE TRIGGER t BEFORE TRUNCATE ON x FOR EACH ROW ...; -- ERROR- 写生成列:
BEFORE触发器对存储生成列的赋值会被覆盖;虚拟生成列在触发器中未计算,不可访问。-- 错误!
最佳实践
- 能用
BEFORE就用BEFORE:没有明确理由时它更省,因为无需把事件信息保留到语句末尾。sqlCREATE TRIGGER t BEFORE UPDATE ON x FOR EACH ROW EXECUTE FUNCTION f(); - 校验放
BEFORE,联动放AFTER:BEFORE只保证「本行此刻合法」,AFTER才能看到最终值。 BEFORE函数结尾固定写RETURN NEW(DELETE 写RETURN OLD),把它当作模板强制项。sqlCREATE OR REPLACE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- ... 逻辑 ... IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$;AFTER触发器务必配WHEN:能在排队阶段就跳过大部分行,大批量 DML 下收益显著。sqlCREATE TRIGGER t AFTER UPDATE ON x FOR EACH ROW WHEN (OLD.v IS DISTINCT FROM NEW.v) EXECUTE FUNCTION f();- 用等宽数字前缀控制执行顺序:
trg_01_、trg_02_,让顺序成为可读的契约。 AFTER里不要做重活:它处在语句临界路径上,慢查询会成倍放大;批量逻辑改用语句级 + 过渡表。- 跨表一致性检查优先用
AFTER+ 延迟约束触发器:允许事务中间态,只在提交前判定。sqlCREATE CONSTRAINT TRIGGER t AFTER INSERT ON ledger DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION f(); - 视图可写化统一用
INSTEAD OF,并在INSERT/UPDATE分支返回NEW、DELETE分支返回OLD,保证RETURNING与影响行数正确。 INSTEAD OF里没改动任何数据时显式返回NULL,让影响行数如实反映结果。- 生成列相关逻辑只写在
AFTER,BEFORE里不要读取生成列。 - 把「时机」写进注释与文档:用
COMMENT ON TRIGGER记录为什么选BEFORE或AFTER,避免后人误改。sqlCOMMENT ON TRIGGER trg_stock_move_check ON stock_moves IS 'BEFORE:写入前校验库存充足,不合法直接抛 23514'; - 用事务回滚测试时机行为:起一个事务、执行 DML、观察
NOTICE与副作用,再ROLLBACK,可重复验证顺序与返回值语义。sqlBEGIN; UPDATE products SET price = 1 WHERE sku = 'SKU-001'; ROLLBACK;