delete规则易致语义错乱、不稳定函数多次求值、with/returning等结构不兼容;真要拦截/改写应优先用触发器,仅透明分区等严格可控场景才考虑rule。

DELETE 上的规则不是“增强删除逻辑”的推荐路径,它容易导致语义错乱、重复执行或不可预期的副作用。真要拦截/改写 DELETE,优先用触发器;只有在极少数需重写整个查询树(比如透明分区路由)且能严格控制输入场景时,才考虑 RULE。
为什么 DELETE 规则常被误用
规则系统对 DELETE 的处理是“查询树替换”,而非“事件钩子”。这意味着:
- 原始
DELETE FROM orders WHERE id = 1可能被替换成完全不同的查询树,比如DELETE FROM orders_2024 WHERE ...+DELETE FROM orders_2025 WHERE ...,但 WHERE 条件若含now()、random()等不稳定函数,会被多次求值 -
WITH子句、RETURNING中的表达式、窗口函数等结构无法安全复制进规则动作,PostgreSQL 会直接报错或静默忽略 - 规则不感知事务上下文 —— 如果你用
INSTEAD把DELETE改成UPDATE is_deleted = true,那RETURNING返回的仍是原始DELETE语义(即零行),而不是你实际更新的行
INSTEAD DELETE 规则必须绕开的语法坑
想用 INSTEAD 实现软删,不能直接在规则里写 UPDATE 并指望 RETURNING 正常工作。常见错误写法:
CREATE RULE soft_delete_orders AS
ON DELETE TO orders
DO INSTEAD
UPDATE orders SET deleted_at = now() WHERE id = OLD.id;
这会导致两个问题:
-
OLD在DELETE规则中可用,但仅限于单行操作;批量DELETE(如WHERE status = 'archived')会让OLD行为未定义 - 上面的
UPDATE不带RETURNING,而原始DELETE若有RETURNING *,调用方收不到任何结果 —— PostgreSQL 不会自动把UPDATE的返回映射过去 - 若表有外键引用,
INSTEAD后的UPDATE不触发级联行为,约束检查仍按原始DELETE路径走,可能报错
ALSO DELETE 规则的实际用途很窄
ALSO 模式适合审计日志这类“附加动作”,但必须满足:动作无副作用、不依赖原始查询的中间状态、可接受重复执行风险。例如:
CREATE RULE log_order_deletes AS
ON DELETE TO orders
DO ALSO
INSERT INTO orders_log (do_type, old_value, do_time)
VALUES ('delete', OLD.id::text || ',' || OLD.name, now());
- 它只在原始
DELETE执行后追加一条日志,不干扰主逻辑 - 但若原始语句是
DELETE FROM orders WHERE created_at ,规则里的 <code>OLD是逐行绑定的,而RETURNING结果集与日志插入无关联 - 如果日志表本身有唯一约束或触发器,这里可能因并发导致插入失败,而主
DELETE已成功 —— 违反原子性预期
真正可控的替代方案是触发器
所有需要精确控制、访问旧值/新值、保证执行顺序、兼容 RETURNING 或事务一致性的场景,都该用 BEFORE/AFTER 触发器。例如软删:
CREATE OR REPLACE FUNCTION soft_delete_trigger() RETURNS TRIGGER AS $$ BEGIN NEW.deleted_at := now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER orders_soft_delete BEFORE DELETE ON orders FOR EACH ROW EXECUTE FUNCTION soft_delete_trigger();
注意:BEFORE DELETE 触发器中,你只能修改 NEW(用于 INSERT)或跳过操作(RETURN NULL),真正做软删得配合 INSTEAD OF 视图,或用 AFTER + 显式 UPDATE。关键点在于:触发器能拿到完整 OLD 记录、支持 FOR EACH ROW 精确控制、与 RETURNING 协同正常。
规则系统里最危险的,是以为它像触发器一样“可靠” —— 实际上,它更像一个查询预处理器,复杂度藏在底层树重写里,出问题时很难调试。










