规则与触发器对比
规则(Rule)与触发器(Trigger)都能实现「某条 SQL 发生时顺带做点别的事」,因此经常被拿来比较。但两者处于查询处理流水线的不同阶段:规则工作在重写期(Rewrite Phase),拿到的是查询树而不是数据;触发器工作在执行期(Execution Phase),拿到的是真实的行。
这个阶段差异决定了一切:规则无法报错、无法做数据相关分支、无法实现外键,但改写成本与影响行数无关;触发器能报错、能查任意表、能逐行决策,但每行都要回调一次。理解这条分界线,就能在「可更新视图」「审计日志」「级联清理」等场景中一次选对工具。
前置知识
阅读本节前,建议先了解:CREATE RULE 规则、INSTEAD 规则、可更新视图
基础概念
两条不同的执行路径
SQL 文本 → Parser(语法分析) → Rewriter(重写期,规则在此生效)
→ Planner(生成计划) → Executor(执行期,触发器在此生效)
规则(Rule) :改写查询树 → 生成额外查询树 → 原查询可被丢弃
触发器(Trigger) :逐行 / 按语句回调函数 → 查询数据、抛错、改行一句话概括:规则改写语句,触发器处理行。
| 工具 | 何时介入 | 看到什么 | 能做什么 |
|---|---|---|---|
| 规则 | 重写期 | 查询树(还没执行) | 改写语句、生成额外语句、丢弃语句 |
| 触发器 | 执行期 | 真实的行与事务状态 | 改行、查表、报错、条件分支、延迟到事务末 |
为什么「阶段」如此重要
规则在重写期运行时,数据库里一行数据都还没有被处理。因此规则不可能根据「另一张表里有没有对应记录」来决定是执行还是报错——它只能看查询树里的 NEW / OLD 伪关系。这正是规则实现不了外键(Foreign Key)语义的根本原因。
触发器在执行期被调用,此时当前行已在内存中、当前事务里此前语句的修改也已可见,因此它可以:查询任意表并据结果决定行为、RAISE EXCEPTION 让语句回滚、在 BEFORE 阶段改写 NEW、调用带循环与异常处理的过程逻辑、延迟到事务末执行(约束触发器)、在 TRUNCATE 上触发、与 ON CONFLICT 共存。这些都是规则做不到的。
规则与触发器的能力对照表
| 能力 | 规则(Rule) | 触发器(Trigger) |
|---|---|---|
| 触发事件 | SELECT/INSERT/UPDATE/DELETE | INSERT/UPDATE/DELETE/TRUNCATE |
| 能否报告错误 | ❌ 只能静默 NOTHING | ✅ RAISE EXCEPTION |
| 能否查询其它表 | 只能在动作里写子查询(执行期) | ✅ 每个回调内随时查询 |
| 能否按行决策 | ❌ 一次改写 | ✅ FOR EACH ROW |
| 能否实现外键 | ❌ | ✅(或直接用 FOREIGN KEY) |
| 影响行数相关性 | 改写成本与行数无关 | 每行回调一次,成本正比行数 |
| 参数化 / 计划复用 | 无「每行」概念 | SPI 计划可缓存复用 |
| 能否延迟执行 | ❌ | ✅ 约束触发器 SET CONSTRAINTS |
RETURNING 支持 | 有限(单动作无条件 INSTEAD) | ✅ 通过返回值提供行 |
与 ON CONFLICT 共存 | ❌ 官方明确不支持 | ✅ |
| 定义位置 | 仅关系(表/视图) | 表/视图/外部表 |
| 元数据目录 | pg_rewrite / pg_rules | pg_trigger |
语法与用法
一条「级联清理」需求,两种写法。
规则写法:
CREATE [ OR REPLACE ] RULE name AS ON event
TO table_name [ WHERE condition ]
DO [ ALSO | INSTEAD ] { NOTHING | command | ( command ; command ... ) }
-- event:SELECT | INSERT | UPDATE | DELETECREATE TABLE computer (
hostname text PRIMARY KEY, -- 索引
manufacturer text
);
CREATE TABLE software (
software text,
hostname text REFERENCES computer(hostname) ON DELETE CASCADE
);
-- 用规则实现「删除主机时清理其软件记录」
CREATE RULE computer_del AS ON DELETE TO computer
DO ALSO DELETE FROM software WHERE hostname = OLD.hostname;INSERT INTO computer VALUES ('pc1', 'bim'), ('pc2', 'bim');
INSERT INTO software VALUES ('editor', 'pc1'), ('player', 'pc1');
DELETE FROM computer WHERE hostname = 'pc1';
SELECT * FROM software;
software | hostname
----------+----------
player | pc1 -- 只剩 1 行,另一行被规则动作删掉了触发器写法:
CREATE [ OR REPLACE ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE | DELETE | TRUNCATE }
ON table_name
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )CREATE FUNCTION trg_computer_del() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
DELETE FROM software WHERE hostname = OLD.hostname; -- 可看到当前行
RETURN OLD;
END;
$$;
CREATE TRIGGER computer_del_trg
AFTER DELETE ON computer
FOR EACH ROW EXECUTE FUNCTION trg_computer_del();DELETE FROM computer WHERE hostname = 'pc2';
SELECT * FROM software WHERE hostname = 'pc2';
software | hostname
----------+----------
(0 rows) -- 同样清理干净两份写法结果一致,但代价模型完全不同:规则生成一条额外 DELETE,触发器执行每行一次 DELETE。
详细说明
规则与触发器的本质差异:重写期 vs 执行期
规则是查询树变换。它的输入是语法树,输出是一个「查询树列表」,系统拿到这个列表后逐个优化、执行。
CREATE TABLE shoelace_data (sl_name text PRIMARY KEY, sl_avail int, sl_color text);
CREATE TABLE shoelace_log (sl_name text, sl_avail int, log_who text, log_when timestamptz);
CREATE RULE log_shoelace AS ON UPDATE TO shoelace_data
WHERE NEW.sl_avail <> OLD.sl_avail
DO ALSO INSERT INTO shoelace_log
VALUES (NEW.sl_name, NEW.sl_avail, current_user, current_timestamp);UPDATE shoelace_data SET sl_avail = 6 WHERE sl_name = 'sl7';-- 规则系统实际产出两条查询树(示意):
[1] INSERT INTO shoelace_log VALUES (shoelace_data.sl_name, 6, current_user, now())
FROM shoelace_data
WHERE 6 <> shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
[2] UPDATE shoelace_data SET sl_avail = 6 WHERE sl_name = 'sl7';触发器是执行期回调。它不改写语句,而是在每条语句、每一行上被调用一次,函数体里是真正的过程代码。
CREATE FUNCTION trg_log_shoelace() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.sl_avail IS DISTINCT FROM OLD.sl_avail THEN
INSERT INTO shoelace_log
VALUES (NEW.sl_name, NEW.sl_avail, current_user, current_timestamp);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER log_shoelace_trg
AFTER UPDATE OF sl_avail ON shoelace_data
FOR EACH ROW EXECUTE FUNCTION trg_log_shoelace();关键结论
规则把「做额外事情」表达为多一条 SQL 语句;触发器把它表达为一段过程代码。前者在优化器视野内(可被整体优化),后者在优化器视野外(逐行执行)。
触发器能做什么而规则不能:约束与报错
官方文档明确指出:规则无法实现某些类型的约束,尤其是外键。规则可以把不满足条件的命令重写成 NOTHING,但数据会被静默丢弃,调用方毫不知情——语义上完全不等同于「报错并回滚」。
-- 规则版「伪外键」:值不存在时静默丢弃 —— 危险
CREATE RULE order_buyer_check AS ON INSERT TO orders
WHERE NOT EXISTS (SELECT 1 FROM buyers WHERE id = NEW.buyer_id)
DO INSTEAD NOTHING;INSERT INTO orders (buyer_id, amount) VALUES (99999, 100.00);
INSERT 0 0 -- 什么都没插入,也没有任何错误!触发器可以做真正的校验:
CREATE FUNCTION trg_check_buyer() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM buyers WHERE id = NEW.buyer_id) THEN
RAISE EXCEPTION 'buyer % does not exist', NEW.buyer_id
USING ERRCODE = 'foreign_key_violation';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER check_buyer_trg
BEFORE INSERT OR UPDATE OF buyer_id ON orders
FOR EACH ROW EXECUTE FUNCTION trg_check_buyer();INSERT INTO orders (buyer_id, amount) VALUES (99999, 100.00);
ERROR: buyer 99999 does not exist
CONTEXT: PL/pgSQL function trg_check_buyer() line 4 at RAISE能声明约束就别自己写
需要外键就用 FOREIGN KEY 声明,而不是手写触发器或规则。手写方案会丢失 NOT VALID、SET CONSTRAINTS、pg_dump 还原顺序、分区表克隆等一整套基础设施。
规则不能实现外键的原因
根本原因仍是阶段差异:规则只看得到查询树,看不到数据。外键检查的语义是「插入的 buyer_id 必须在 buyers 中存在」,而「是否存在」只有执行期用索引探测才能知道。
规则可引用的信息:NEW.<列>、OLD.<列>、由两者构造的表达式
✗ 无法表达「另一张表里是否存在该值」
触发器可引用:NEW、OLD,以及任意时刻的 SELECT 结果规则动作里当然可以写子查询,但那已经是执行期的语句了——规则本身无法据其分支去「决定要不要报错」。
-- 子查询在执行期求值,但规则无法据此改变行为
CREATE RULE x AS ON DELETE TO computer
DO ALSO DELETE FROM software
WHERE hostname = OLD.hostname
AND EXISTS (SELECT 1 FROM audit WHERE obj = OLD.hostname);逐行执行成本 vs 一次改写
这是选型中最常被问到的一点,两者的成本结构完全不同:
影响 N 行的一条语句:
规则: Rewriter 改写 1 次 → 生成 1 条额外 SQL → 该 SQL 由优化器统一计划并执行 1 次
成本 ≈ O(1) 次额外语句执行(与 N 无关)
触发器(FOR EACH ROW):
执行器逐行回调 N 次 → 每次调用都经 SPI 管理器进入函数
成本 ≈ O(N) 次函数调用 + N 次内部语句执行什么时候规则更快
一条语句影响大量行、且规则的额外动作能生成可索引的等值连接时,规则通常明显更快:规则只需执行一条 SQL,优化器会为它统一选择嵌套循环或哈希连接;触发器则要把同样的逻辑跑成千上万次。
什么时候触发器更快
每行逻辑复杂(查多张表、条件分支、报错),或规则的额外动作导致大范围且限定不佳的连接(优化器选错计划)时,触发器反而更快也更可控。官方总结:规则只有在「动作产生大规模且限定不佳的连接」这一优化器失效的情形下,才会显著慢于触发器。
官方 computer / software 性能例子
官方文档用 computer / software 两张表演示了这个对比。两表都有数千行,hostname 上都有唯一索引,需求是「删除主机时连带删除其软件记录」。
CREATE TABLE computer (
hostname text PRIMARY KEY,
manufacturer text
);
CREATE INDEX comp_manufidx ON computer (manufacturer);
CREATE TABLE software (
software text,
hostname text
);
CREATE INDEX soft_hostidx ON software (hostname);
-- 规则版
CREATE RULE computer_del AS ON DELETE TO computer
DO ALSO DELETE FROM software WHERE hostname = OLD.hostname;情形一:只删一行(DELETE ... WHERE hostname = 'mypc.local.net')。规则生成的额外语句退化为「两个索引扫描的嵌套循环」,与触发器差别不大:
Nested Loop
-> Index Scan using computer_pkey on computer
-> Index Scan using soft_hostidx on software情形二:删除 2000 台 hostname 以 old 开头的主机。 条件的写法直接决定规则版的计划:
-- 写法 A:两个不等式,规划器无法把范围条件下推到 software
DELETE FROM computer WHERE hostname >= 'old' AND hostname < 'ole';
-- → Hash Join(对 software 做 Seq Scan)
-- 写法 B:正则形式反而能下推,两次索引扫描
DELETE FROM computer WHERE hostname ~ '^old';
-- → Nested Loop(两次 Index Scan)情形三:按厂商删除(WHERE manufacturer = 'bim')走 comp_manufidx,同样是两次索引扫描的 Nested Loop。
| 情形 | 规则 | 触发器 |
|---|---|---|
| 单行删除 | 1 次改写 + 1 条语句 | 1 次 SPI 调用 |
| 2000 行删除(范围条件) | 1 条语句,但可能退化为对 software 的 Seq Scan | 2000 次索引扫描 + 2000 次 SPI 调用 |
| 2000 行删除(正则可下推) | 1 条语句,两次索引扫描 | 同上,逐行开销 |
官方结论:规则的额外命令成本基本与影响行数无关;规则只在额外动作产生「大规模且限定不佳的连接」(优化器失效)时才会显著慢于触发器。用 EXPLAIN (ANALYZE, BUFFERS) 分别跑两种实现即可验证:规则版只出现一条额外语句的计划,触发器版的 Execution Time 会随删除行数线性增长。
数据可见性与 volatile 函数
规则在重写期运行,此时毫无数据可言;而规则生成的额外语句在执行期运行时,又受语句执行顺序影响。官方两处要点:
规则与原始语句的执行顺序:
ON INSERT 规则:原语句先执行 → 规则动作能看到刚插入的行
ON UPDATE / ON DELETE 规则:规则动作先执行 → 动作能看到待改/待删的行
触发器顺序:
BEFORE 触发器:行改变之前,可改 NEW
AFTER 触发器:行改变之后,所有修改(含其它触发器效果)可见-- ON DELETE 规则需先执行,才能看到待删的行
CREATE RULE order_del AS ON DELETE TO orders
DO ALSO INSERT INTO orders_archive
SELECT OLD.order_id, OLD.buyer_id, OLD.amount, OLD.status, now();volatile 函数是规则最容易被忽视的坑。官方明确警告:当原始查询包含 volatile 函数时,规则的行为会出人意料——函数可能被执行比预期更多的次数,因为规则动作与原始查询会各自对同一表达式求值一次。
-- 规则版:random() 可能被算两次
CREATE RULE r AS ON INSERT TO t
DO ALSO INSERT INTO t_log (id, rnd) VALUES (NEW.id, random());
INSERT INTO t (id, val) VALUES (1, random()); -- 改写后 random() 可能多次求值volatile 函数是规则的雷区
nextval()、random()、clock_timestamp()、带副作用的函数在规则下都可能被多次求值。对这类逻辑优先用触发器(函数体内调用,次数与行数一一对应);规则适合纯关系代数式的改写。
另外,规则类型还不支持原查询中的 WITH 子句,以及 UPDATE ... SET 列表中的多赋值子查询(SET x = (SELECT ...)),因为这些构造被复制进规则查询后会导致子查询被多次求值。
-- 有 INSERT/UPDATE 规则时,这条会报错
INSERT INTO v_orders (order_id, amount) VALUES (1, 10.00)
ON CONFLICT (order_id) DO NOTHING;ERROR: INSERT with ON CONFLICT clause cannot be used with table that has INSERT or UPDATE rulesINSTEAD OF 触发器 vs INSTEAD 规则:写法对比
两者都能让复杂视图「可写」。先用 INSTEAD 规则:
CREATE VIEW v_orders AS
SELECT o.order_id, o.buyer_id, b.name AS buyer_name, o.amount, o.status
FROM orders o JOIN buyers b ON b.id = o.buyer_id;
CREATE RULE v_orders_ins AS ON INSERT TO v_orders
DO INSTEAD INSERT INTO orders (order_id, buyer_id, amount, status)
VALUES (NEW.order_id, NEW.buyer_id, NEW.amount, NEW.status);再用 INSTEAD OF 触发器:
CREATE FUNCTION v_orders_insert() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO orders (order_id, buyer_id, amount, status)
VALUES (NEW.order_id, NEW.buyer_id, NEW.amount, NEW.status);
RETURN NEW; -- 返回 NEW,供 RETURNING 使用
END;
$$;
CREATE TRIGGER v_orders_ins_trg
INSTEAD OF INSERT ON v_orders
FOR EACH ROW EXECUTE FUNCTION v_orders_insert();差异一览:
| 维度 | INSTEAD 规则 | INSTEAD OF 触发器 |
|---|---|---|
| 定义限制 | 无特殊限制 | 必须 FOR EACH ROW,且只能建在视图上 |
WHEN 条件 | 支持规则 WHERE 条件(基于 NEW/OLD) | 不支持 WHEN 条件 |
| 报错 / 校验 | 只能静默 NOTHING | ✅ RAISE EXCEPTION |
| 过程逻辑 | 无(只有 SQL 改写) | ✅ 循环、分支、异常处理 |
RETURNING | 需单动作 + 无条件 INSTEAD,且 OLD/NEW 语义有坑 | 直接 RETURN NEW(或 OLD)即可 |
| 多表分发 | 一条规则可含多动作(但失去 RETURNING) | 函数体内任意多条语句 |
| 性能(单表映射、大批量) | 通常更优(一条额外语句) | 逐行回调 |
| 可读性 | 较隐晦 | 直观、官方推荐 |
官方倾向
官方原文指出:本章所有更新规则示例都可以用视图上的 INSTEAD OF 触发器实现,而且编写这种触发器通常比编写规则更容易,尤其在更新逻辑复杂时。所以:简单映射用规则求性能,复杂逻辑用触发器求正确与可维护。
一个必须记住的联动规则:
视图上的写操作由谁处理,决定谁会触发:
① 由行级 INSTEAD OF 触发器处理
→ 视图上的语句级触发器会被触发
② 由 INSTEAD 规则处理
→ 视图上的触发器不会被触发;
触发的是规则生成的替换语句所涉及的表上的触发器
③ 视图本身可自动更新
→ 触发的是基表的语句级触发器可更新视图两种实现方式的优缺点
-- 先检查视图是否本身就能自动更新
SELECT table_name, is_updatable, is_insertable_into
FROM information_schema.views
WHERE table_name = 'v_orders'; table_name | is_updatable | is_insertable_into
------------+--------------+--------------------
v_orders | NO | NO
(1 row)| 方案 | 优点 | 缺点 |
|---|---|---|
| 自动可更新视图 | 零维护、优化器可整体优化、无额外对象 | 仅限单表简单视图,列没有派生/聚合 |
INSTEAD 规则 | 改写成本与影响行数无关;一条额外语句即可;多动作可分发 | 无法报错;RETURNING 限制多;OLD/NEW 语义隐晦;volatile 函数有坑 |
INSTEAD OF 触发器 | 逻辑直观;可校验报错;RETURNING 简单;能调任意其它表 | 逐行回调,行多时慢;需维护函数与触发器对象 |
选型动作序列:
① 视图能否自动更新? → 是:什么都不做(最优)
② 写操作是「单表字段一一映射」且批量大? → 规则(DO INSTEAD)
③ 需要校验、报错、分支、跨表副作用? → INSTEAD OF 触发器
④ 只需「只读保护」? → 规则 NOTHING(静默)
或 INSTEAD OF 触发器 RAISE(要报错时)典型场景选型:日志、审计、跨表写入
场景一:审计日志(记录谁改了什么)。
-- 规则版:动作先于 UPDATE,条件基于 NEW/OLD
CREATE RULE orders_audit AS ON UPDATE TO orders
WHERE NEW.status IS DISTINCT FROM OLD.status
DO ALSO INSERT INTO orders_status_audit
VALUES (OLD.order_id, OLD.status, NEW.status, current_user, clock_timestamp());-- 触发器版:可精确到列,且用 WHEN 提前过滤
CREATE FUNCTION trg_status_audit() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO orders_status_audit
VALUES (OLD.order_id, OLD.status, NEW.status, current_user, clock_timestamp());
RETURN NULL; -- AFTER 触发器返回值被忽略
END;
$$;
CREATE TRIGGER orders_audit_trg
AFTER UPDATE OF status ON orders
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status) -- 提前过滤,减少回调
EXECUTE FUNCTION trg_status_audit();何时选触发器写审计
需要记录操作者会话信息、需要在日志里再查一次表、需要区分 INSERT 与 ON CONFLICT DO UPDATE、或需要延迟到事务末写入时,用触发器。纯「复制几个旧值到日志表」且批量很大时,规则更省。
场景二:跨表分发写入。
-- 规则:一条语句写两张表(多动作),但失去 RETURNING
CREATE RULE v_push_ins AS ON INSERT TO v_push
DO INSTEAD (
INSERT INTO t_cn (id, v) VALUES (NEW.id, NEW.v);
INSERT INTO t_other (id, v) VALUES (NEW.id, NEW.v);
);-- 触发器:可在函数内按数据决定写哪张表
CREATE FUNCTION v_push_insert() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.region = 'cn' THEN
INSERT INTO t_cn (id, v) VALUES (NEW.id, NEW.v);
ELSE
INSERT INTO t_other (id, v) VALUES (NEW.id, NEW.v);
END IF;
RETURN NEW;
END;
$$;别用规则或触发器实现外键
任何「级联清理」如果用规则或触发器手写,都无法复现外键的完整语义(并发下的锁、NOT VALID、SET CONSTRAINTS、分区克隆、pg_dump 还原顺序)。真正的需求是外键时,直接声明 FOREIGN KEY ... ON DELETE CASCADE。
场景三:只需「只读保护」。 静默屏蔽用规则,需要报错用触发器:
-- 静默丢弃(规则)
CREATE RULE v_x_del AS ON DELETE TO v_x DO INSTEAD NOTHING;
-- 明确报错(INSTEAD OF 触发器,更安全)
CREATE OR REPLACE FUNCTION v_x_no_delete() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'view % is read-only', TG_TABLE_NAME;
END;
$$;
CREATE TRIGGER v_x_del_trg
INSTEAD OF DELETE ON v_x
FOR EACH ROW EXECUTE FUNCTION v_x_no_delete();选型决策树与混合使用
问:这件事能不能用声明式约束(CHECK / UNIQUE / FK / EXCLUDE)解决?
├─ 能 → 用约束(最好;不写规则也不写触发器)
└─ 不能 ↓
问:需要「报错 / 分支 / 查其它表 / 过程逻辑」吗?
├─ 需要 → 用触发器
│ 问:需要延迟到事务末吗?
│ ├─ 需要 → CONSTRAINT TRIGGER(AFTER ROW + DEFERRABLE)
│ └─ 不需要 → BEFORE/AFTER(行级或语句级)
└─ 不需要 ↓
问:是不是「一条语句映射成另一条等值条件语句」?
├─ 是(大批量、字段一一对应)→ 用规则(DO ALSO / DO INSTEAD)
└─ 否 ↓
问:只需静默屏蔽某操作?
├─ 是 → DO INSTEAD NOTHING(规则,静默)
└─ 否(要报错)→ 回到触发器混合使用的典型模式:
-- ① 规则负责「写归档」(纯搬值、批量友好)
CREATE RULE orders_log AS ON DELETE TO orders
DO ALSO INSERT INTO orders_archive
SELECT OLD.*, current_user, clock_timestamp();
-- ② 触发器负责「校验并报错」(需要过程逻辑)
CREATE FUNCTION orders_check_amount() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.amount <= 0 THEN
RAISE EXCEPTION 'amount must be positive, got %', NEW.amount;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER orders_check_amount_trg
BEFORE INSERT OR UPDATE OF amount ON orders
FOR EACH ROW EXECUTE FUNCTION orders_check_amount();同一事件上规则与触发器的执行关系
在表上:规则产生的语句被当作独立语句执行,因此会正常触发目标表上的触发器——级联效果是「规则定结构、触发器定细节」。 在视图上:若写操作由 INSTEAD 规则接管,视图自身的语句级触发器不会被触发(见前一节对照表)。
实战示例
用一套「订单 + 订单明细」模型,把校验、审计、级联三件事分别交给合适的工具。
第一步:建表。
CREATE TABLE buyers (
id integer PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE orders (
order_id integer PRIMARY KEY,
buyer_id integer NOT NULL REFERENCES buyers(id),
amount numeric(12,2) NOT NULL,
status text NOT NULL DEFAULT 'new',
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE orders_status_audit (
order_id integer, old_status text, new_status text,
changed_by text, changed_at timestamptz
);
INSERT INTO buyers VALUES (1, 'Alice'), (2, 'Bob');第二步:用触发器做校验与自动填值(需要过程逻辑)。
CREATE FUNCTION orders_before_write() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.amount <= 0 THEN
RAISE EXCEPTION 'amount must be positive, got %', NEW.amount;
END IF;
NEW.updated_at := clock_timestamp();
RETURN NEW; -- BEFORE 触发器可改写 NEW
END;
$$;
CREATE TRIGGER orders_before_write_trg
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION orders_before_write();INSERT INTO orders (order_id, buyer_id, amount, status) VALUES (1, 1, 199.00, 'new'); -- INSERT 0 1
INSERT INTO orders (order_id, buyer_id, amount, status) VALUES (2, 1, -5.00, 'new');
-- ERROR: amount must be positive, got -5.00第三步:用规则做状态审计(纯搬值、批量友好)。
CREATE RULE orders_status_log AS ON UPDATE TO orders
WHERE NEW.status IS DISTINCT FROM OLD.status
DO ALSO INSERT INTO orders_status_audit
VALUES (OLD.order_id, OLD.status, NEW.status, current_user, clock_timestamp());UPDATE orders SET status = 'paid' WHERE order_id = 1; -- UPDATE 1
SELECT order_id, old_status, new_status, changed_by FROM orders_status_audit;
order_id | old_status | new_status | changed_by
----------+------------+------------+------------
1 | new | paid | postgres第四步:验证规则「动作先于 UPDATE」的可见性——只改金额不改状态,不应产生审计行。
UPDATE orders SET amount = 259.00 WHERE order_id = 1;
SELECT count(*) FROM orders_status_audit;
-- UPDATE 1,count 仍为 1:规则条件 NEW.status <> OLD.status 不成立第五步:级联清理——规则版与触发器版的成本差异。
CREATE TABLE order_items (
order_id integer NOT NULL REFERENCES orders(order_id),
sku text NOT NULL,
qty integer NOT NULL
);
CREATE INDEX order_items_order_id_idx ON order_items (order_id);
INSERT INTO order_items
SELECT 1, 'sku-' || g, 1 FROM generate_series(1, 500) AS g;
-- 规则版:一条额外语句 DELETE ... WHERE order_id = OLD.order_id
CREATE RULE orders_del_items AS ON DELETE TO orders
DO ALSO DELETE FROM order_items WHERE order_id = OLD.order_id;规则版:一次计划、一次执行下面这条额外语句
触发器版:AFTER DELETE ON orders FOR EACH ROW 的函数体内执行同样的 DELETE,
但它是「1 次 DELETE 订单」+「N 次上述语句」,N = 被删订单数
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
DELETE FROM order_items WHERE order_id = 1;
Delete on order_items (actual rows=500 loops=1)
-> Index Scan using order_items_order_id_idx on order_items (actual rows=500 loops=1)第六步:核对元数据。
SELECT tablename, rulename
FROM pg_rules WHERE tablename IN ('orders', 'order_items')
ORDER BY tablename, rulename;
SELECT tgname, tgrelid::regclass AS tbl, tgtype, tgisinternal
FROM pg_trigger WHERE NOT tgisinternal
ORDER BY tbl, tgname; tablename | rulename
-----------+--------------------
orders | orders_status_log
(1 row)
tgname | tbl | tgtype | tgisinternal
----------------------------+------------+--------+--------------
orders_before_write_trg | orders | 7 | f
(1 row)实战小结
- 校验与自动填值 → 触发器(
BEFORE,可改NEW、可报错); - 纯值搬运的审计 → 规则(
DO ALSO,批量友好); - 级联清理 → 优先声明式外键;手写时大批量用规则、需延迟/复杂条件用约束触发器;
- 视图可写化 → 简单映射用
INSTEAD规则,复杂逻辑用INSTEAD OF触发器。
注意事项
错误!用规则实现外键或唯一性校验。 规则只能把命令重写成
NOTHING,数据被静默丢弃,调用方拿到的是「成功但零行」的假象。sql-- 危险:非法值被静默吞掉 CREATE RULE r AS ON INSERT TO t WHERE NOT EXISTS (SELECT 1 FROM ref WHERE id = NEW.ref_id) DO INSTEAD NOTHING;错误!在规则动作里依赖 volatile 函数只求值一次。 官方明确警告 volatile 函数在规则下可能被执行多次。
sql-- random() / nextval() 可能被重复求值 CREATE RULE r AS ON INSERT TO t DO ALSO INSERT INTO log VALUES (random());错误!指望规则能做数据相关的分支。 规则条件只能引用
NEW/OLD,不能基于其它表的状态分支。sql-- 规则里无法表达「若 buyers 中不存在则拒绝」不推荐:给本身可自动更新的视图再建
INSTEAD规则或INSTEAD OF触发器。 自动改写通常计划更优、维护成本为零。sqlSELECT is_updatable, is_insertable_into FROM information_schema.views WHERE table_name = 'v_simple';错误!在有
INSERT/UPDATE规则的表上使用ON CONFLICT。sqlINSERT INTO v_orders (order_id, amount) VALUES (1, 10) ON CONFLICT (order_id) DO NOTHING;textERROR: INSERT with ON CONFLICT clause cannot be used with table that has INSERT or UPDATE rules错误!在有规则的关系上使用带
WITH子句或SET x = (SELECT ...)多赋值的原查询。 这两类构造官方明确不支持。错误!给构建在视图上的写操作只配条件
INSTEAD规则。 缺少无条件INSTEAD时会报错。textERROR: cannot perform INSERT RETURNING on relation "v_orders" HINT: You need an unconditional ON INSERT DO INSTEAD rule with a RETURNING clause.错误!给
INSTEAD OF触发器写WHEN条件。 官方明确不支持INSTEAD OF触发器的WHEN。sql-- 非法 CREATE TRIGGER x INSTEAD OF INSERT ON v FOR EACH ROW WHEN (NEW.amount > 0) EXECUTE FUNCTION f();错误!在表上建
INSTEAD OF触发器,或反过来在视图上建行级BEFORE/AFTER触发器。INSTEAD OF只能建在视图上且必须FOR EACH ROW;视图上的BEFORE/AFTER只能是语句级。text视图上:BEFORE/AFTER ... FOR EACH STATEMENT ✅ INSTEAD OF ... FOR EACH ROW ✅ 表上 :BEFORE/AFTER ... FOR EACH ROW|STMT ✅ INSTEAD OF ❌错误!以为规则生成的替换语句不会触发触发器。 规则生成的语句是独立语句,会正常触发其目标表上的触发器。
不推荐:只统计「有没有发生写入」却用行级触发器。 用
FOR EACH STATEMENT即可,避免 N 次回调。sqlCREATE TRIGGER orders_stmt_trg AFTER INSERT ON orders FOR EACH STATEMENT EXECUTE FUNCTION bump_table_counter();
最佳实践
能用声明式约束就用约束。
CHECK、UNIQUE、FOREIGN KEY、EXCLUDE优先于任何规则/触发器方案。sqlALTER TABLE software ADD CONSTRAINT software_computer_fk FOREIGN KEY (hostname) REFERENCES computer(hostname) ON DELETE CASCADE;需要「报错」的场景一律用触发器,并用
RAISE ... USING ERRCODE给出规范错误码。sqlRAISE EXCEPTION 'buyer % does not exist', NEW.buyer_id USING ERRCODE = 'foreign_key_violation';大批量、纯改写、无分支的场景用规则,享受「一条额外语句」的常数成本。
sqlCREATE RULE computer_del AS ON DELETE TO computer DO ALSO DELETE FROM software WHERE hostname = OLD.hostname;规则动作务必写成可索引的等值条件,避免大范围且限定不佳的连接导致优化器选错计划。
sql-- 好:等值条件可下推成索引扫描 DO ALSO DELETE FROM software WHERE hostname = OLD.hostname;触发器用
WHEN提前过滤,并用AFTER UPDATE OF <列>限定触发时机。sqlCREATE TRIGGER orders_audit_trg AFTER UPDATE OF status ON orders FOR EACH ROW WHEN (OLD.status IS DISTINCT FROM NEW.status) EXECUTE FUNCTION trg_status_audit();视图可写化优先
INSTEAD OF触发器(逻辑直观、可报错、RETURNING简单);只有简单映射且批量大时才用INSTEAD规则。sqlCREATE TRIGGER v_orders_ins_trg INSTEAD OF INSERT ON v_orders FOR EACH ROW EXECUTE FUNCTION v_orders_insert();只读保护若要报错就用触发器,别用静默
DO INSTEAD NOTHING。sqlRAISE EXCEPTION 'view % is read-only', TG_TABLE_NAME;避免在规则动作中使用 volatile 函数;如确需使用,改到触发器里执行。
sql-- 触发器内调用 random()/clock_timestamp(),调用次数与行数一一对应对性能敏感的改写用
EXPLAIN (ANALYZE, BUFFERS)实测两条路径,不要凭直觉断言「触发器一定慢」或「规则一定快」。sqlEXPLAIN (ANALYZE, BUFFERS) DELETE FROM computer WHERE hostname = 'mypc.local.net';用
pg_rules/pg_trigger建立上线检查清单,防止规则与触发器相互叠加导致行为不可预期,并在文档中记录选型理由。sqlSELECT tablename, rulename FROM pg_rules WHERE schemaname = 'public'; SELECT tgname, tgrelid::regclass FROM pg_trigger WHERE NOT tgisinternal;
下一节
继续学习:PL/pgSQL 概述