Skip to content

INSTEAD 规则 ​

INSTEAD 规则用规则动作取代原命令,而不是在原命令之外附加执行。它最常见的用途是让视图「看起来可写」:在视图上定义 ON INSERT / ON UPDATE / ON DELETE 的 INSTEAD 规则,把对视图的写操作改写到真实基表上。另一个用途是保护性屏蔽:用 DO INSTEAD NOTHING 让某些操作静默失效。

前置知识

阅读本节前,建议先了解:CREATE RULE 规则、可更新视图

基础概念 ​

INSTEAD 的含义 ​

text
DO ALSO     :原命令 + 规则动作         (1 + N)
DO INSTEAD  :规则动作取代原命令         (只有规则动作,原命令消失)

对视图的写操作:

  INSERT INTO v_shoelace ...   ──►  应用 INSTEAD 规则   ──►  改写为
                                                             INSERT INTO shoelace_data ...
  (视图本身无存储,"写视图"这个动作实际不存在)

为什么需要 INSTEAD 规则 ​

场景为什么需要
复杂视图(含 JOIN、聚合、表达式)不能自动更新系统无法推断如何把写入映射回基表,需手工指定
需要把一个视图的写操作分发到多张表一条规则可含多个动作
需要拦截并改写写入语义(如写日志表代替写原表)用 INSTEAD 替换行为
需要「只读保护」某些操作DO INSTEAD NOTHING

与 INSTEAD OF 触发器对比 ​

维度INSTEAD 规则INSTEAD OF 触发器
执行时机查询重写阶段(执行前)语句执行阶段,按行回调
粒度一次命令改写FOR EACH ROW(视图上必须按行)
写法复杂度改写规则,较隐晦过程函数,直观
支持 RETURNING需要在规则里写 RETURNING通过触发器返回新行
官方倾向视图定义(_RETURN)必用视图更新推荐

官方建议

官方文档明确指出:视图更新「用 INSTEAD OF 触发器实现通常比用规则更容易」。原因是触发器语义简单、可控;规则容易有意外。但对于简单映射(单表、字段一一对应),规则的性能通常更好(一条额外命令 vs 逐行回调)。

自动可更新视图的优先级 ​

一个重要的前提:本身可自动更新的视图不需要用户建规则。

text
视图能否自动更新?

  简单视图(单表、无聚合/窗口/DISTINCT/集合运算,
           无 GROUP BY/HAVING/LIMIT,列均为直接引用)
      │
      ├─ 是 → 系统自动改写,无需自建规则
      │        (显式建规则也能用,但自动改写通常更快)
      │
      └─ 否 → 需要 INSTEAD 规则或 INSTEAD OF 触发器
sql
-- 简单视图:自动可更新,无需规则
CREATE VIEW v_simple AS
SELECT order_id, buyer_id, amount FROM orders;

INSERT INTO v_simple (order_id, buyer_id, amount) VALUES (9001, 1001, 88.00);
-- 系统自动改写为 INSERT INTO orders ...

显式规则 vs 自动改写

虽然可以给「本身已可自动更新」的视图再显式建规则,但自动更新变换通常性能更好(它会尽量下推条件、生成更优的计划)。能靠自动更新就不要建规则:

sql
-- 不推荐:给简单视图画蛇添足地建 INSTEAD 规则
CREATE RULE v_simple_ins AS ON INSERT TO v_simple
  DO INSTEAD INSERT INTO orders (order_id, buyer_id, amount)
              VALUES (NEW.order_id, NEW.buyer_id, NEW.amount);

语法与用法 ​

text
CREATE [ OR REPLACE ] RULE name AS ON event
    TO view_name [ WHERE condition ]
    DO INSTEAD { NOTHING | command | ( command ; command ... ) }

-- event 取值:INSERT | UPDATE | DELETE
-- INSTEAD 规则动作里可用 NEW / OLD 引用视图的行

最简示例——让一个「含派生的视图」可插入:

sql
CREATE TABLE shoelace_data (
  sl_name  text PRIMARY KEY,
  sl_avail integer,
  sl_color text
);

-- 带表达式的视图(sl_len_cm 是派生列),本身不可自动更新
CREATE VIEW v_shoelace AS
SELECT sl_name,
       sl_avail,
       sl_color,
       sl_avail * 2 AS sl_avail_double
FROM shoelace_data;

-- 用 INSTEAD 规则让插入可用
CREATE RULE v_shoelace_ins AS ON INSERT TO v_shoelace
  DO INSTEAD INSERT INTO shoelace_data (sl_name, sl_avail, sl_color)
              VALUES (NEW.sl_name, NEW.sl_avail, NEW.sl_color);

插入并验证:

sql
INSERT INTO v_shoelace (sl_name, sl_avail, sl_color) VALUES ('sl1', 5, 'black');

SELECT * FROM v_shoelace ORDER BY sl_name;
text
 sl_name | sl_avail | sl_color | sl_avail_double
---------+----------+----------+-----------------
 sl1     |        5 | black    |              10
(1 row)

详细说明 ​

用规则实现可更新视图 ​

要让一个复杂视图支持 INSERT/UPDATE/DELETE,需要为每种想支持的操作各建一条 INSTEAD 规则。三条规则分别对应三种写入:

sql
CREATE TABLE shoelace_data (
  sl_name  text PRIMARY KEY,
  sl_avail integer,
  sl_color text,
  sl_len   integer,
  sl_unit  text
);

CREATE VIEW shoelace AS
SELECT sl_name, sl_avail, sl_color, sl_len, sl_unit,
       sl_len * CASE sl_unit WHEN 'cm' THEN 1 WHEN 'inch' THEN 2.54 ELSE 0 END AS sl_len_cm
FROM shoelace_data;

-- INSERT
CREATE RULE shoelace_ins AS ON INSERT TO shoelace
  DO INSTEAD INSERT INTO shoelace_data (sl_name, sl_avail, sl_color, sl_len, sl_unit)
              VALUES (NEW.sl_name, NEW.sl_avail, NEW.sl_color, NEW.sl_len, NEW.sl_unit);

-- UPDATE
CREATE RULE shoelace_upd AS ON UPDATE TO shoelace
  DO INSTEAD UPDATE shoelace_data
              SET sl_name  = NEW.sl_name,
                  sl_avail = NEW.sl_avail,
                  sl_color = NEW.sl_color,
                  sl_len   = NEW.sl_len,
                  sl_unit  = NEW.sl_unit
              WHERE sl_name = OLD.sl_name;

-- DELETE
CREATE RULE shoelace_del AS ON DELETE TO shoelace
  DO INSTEAD DELETE FROM shoelace_data WHERE sl_name = OLD.sl_name;

验证三种操作都作用到了基表:

sql
INSERT INTO shoelace VALUES ('sl9', 10, 'white', 80, 'cm');
UPDATE shoelace SET sl_avail = 20 WHERE sl_name = 'sl9';
DELETE FROM shoelace WHERE sl_name = 'sl9';

SELECT count(*) FROM shoelace_data WHERE sl_name = 'sl9';
text
 count
-------
     0
(1 row)

定位规则的要点

  • INSERT/UPDATE 规则用 NEW.<列> 取新值。
  • UPDATE/DELETE 规则用 OLD.<列> 定位要操作的行。
  • UPDATE 规则的 WHERE 通常用 OLD.<主键>,而非 NEW.<主键>,否则改名后可能定位不到。

RETURNING 子句的处理 ​

如果希望视图支持 INSERT RETURNING / UPDATE RETURNING / DELETE RETURNING,规则动作里必须包含一个合适的 RETURNING 子句:

sql
CREATE OR REPLACE RULE shoelace_ins AS ON INSERT TO shoelace
  DO INSTEAD INSERT INTO shoelace_data (sl_name, sl_avail, sl_color, sl_len, sl_unit)
              VALUES (NEW.sl_name, NEW.sl_avail, NEW.sl_color, NEW.sl_len, NEW.sl_unit)
              RETURNING
                shoelace_data.sl_name,
                shoelace_data.sl_avail,
                shoelace_data.sl_color,
                shoelace_data.sl_len,
                shoelace_data.sl_unit,
                shoelace_data.sl_len * CASE shoelace_data.sl_unit
                  WHEN 'cm' THEN 1 WHEN 'inch' THEN 2.54 ELSE 0 END AS sl_len_cm;

RETURNING 的三条硬性规则:

规则说明
只有无条件 INSTEAD 规则可含 RETURNING条件规则或 ALSO 规则都不行
同一事件最多一个 RETURNING保证只有一个候选来算结果
若所有可用规则都没有 RETURNING视图上的 RETURNING 查询会被拒绝
text
-- 没有为视图提供 RETURNING 时:
INSERT INTO shoelace (sl_name, sl_avail) VALUES ('x', 1) RETURNING *;
-- ERROR:  cannot perform INSERT RETURNING on relation "shoelace"
-- HINT:  You need an unconditional ON INSERT DO INSTEAD rule with a RETURNING clause.

OLD/NEW 在规则 RETURNING 中的特殊含义

在规则的 RETURNING 子句中,OLD 和 NEW 指的是被加入重写查询的额外范围表条目,而不是结果关系的旧/新行。因此对 UPDATE 规则,若 RETURNING 里写 old.sl_name,返回的永远是旧名,无论查询里写的是 OLD 还是 NEW。

推荐做法:RETURNING 里直接引用结果关系的列(如 shoelace_data.sl_name),避免歧义:

sql
CREATE OR REPLACE RULE shoelace_upd AS ON UPDATE TO shoelace
  DO INSTEAD UPDATE shoelace_data
              SET sl_name = NEW.sl_name, sl_avail = NEW.sl_avail
              WHERE sl_name = OLD.sl_name
              RETURNING shoelace_data.sl_name, shoelace_data.sl_avail;

条件规则与无条件兜底规则 ​

这是 INSTEAD 规则最微妙、最容易踩坑的一点。对于复杂视图:

每一个你想允许的写操作,都必须存在一条无条件的 INSTEAD 规则。

原因:如果规则是有条件的或不是 INSTEAD,系统会认为在某些情况下可能仍会尝试在视图的哑表上执行写操作,于是直接拒绝。

text
视图 v 上只有条件 INSTEAD 规则:

  CREATE RULE r AS ON UPDATE TO v
    WHERE NEW.status = 'active'
    DO INSTEAD UPDATE base SET ...;

  UPDATE v SET ... ;   -- 若某行条件不成立,系统担心落到哑表上 → 直接拒绝整个 UPDATE

解决方案:加一条无条件 DO INSTEAD NOTHING 兜底,再让条件规则非 INSTEAD(即 ALSO),在条件成立时"加上"动作:

sql
-- ① 无条件兜底:保证系统知道哑表永远不会被写
CREATE RULE shoelace_protect AS ON UPDATE TO shoelace
  DO INSTEAD NOTHING;

-- ② 条件规则改为 ALSO(非 INSTEAD),条件成立时追加动作
CREATE RULE shoelace_cond_upd AS ON UPDATE TO shoelace
  WHERE NEW.sl_avail > 0
  DO ALSO UPDATE shoelace_data SET sl_avail = NEW.sl_avail
            WHERE sl_name = OLD.sl_name;

条件规则不支持 RETURNING

上述「无条件 INSTEAD NOTHING + 条件 ALSO」的组合目前不支持 RETURNING 查询。如果需要 RETURNING,就只能在无条件 INSTEAD 规则里写 RETURNING。

text
-- 用 INSTEAD NOTHING 也能"无条件屏蔽"某个操作:
CREATE RULE shoe_ins_protect AS ON INSERT TO shoe DO INSTEAD NOTHING;
CREATE RULE shoe_upd_protect AS ON UPDATE TO shoe DO INSTEAD NOTHING;
CREATE RULE shoe_del_protect AS ON DELETE TO shoe DO INSTEAD NOTHING;

-- 之后对 shoe 的这三种操作都会变成"什么都不做"
DELETE FROM shoe WHERE ...;   -- 返回 DELETE 0,基表无变化

用 DO INSTEAD NOTHING 保护视图 ​

DO INSTEAD NOTHING 让某个事件变成一个空操作——查询树列表为空,整个查询「什么也不做」。这是保护视图不被误写的轻量手段:

sql
-- 只允许查、不允许写
CREATE VIEW v_report AS
SELECT region_id, sum(amount)::numeric(14,2) AS sales_amt
FROM orders GROUP BY region_id;

CREATE RULE v_report_no_ins AS ON INSERT TO v_report DO INSTEAD NOTHING;
CREATE RULE v_report_no_upd AS ON UPDATE TO v_report DO INSTEAD NOTHING;
CREATE RULE v_report_no_del AS ON DELETE TO v_report DO INSTEAD NOTHING;
sql
DELETE FROM v_report WHERE region_id = '01';
text
DELETE 0

静默失败的风险

DO INSTEAD NOTHING 是静默的——操作返回成功但什么都没做,调用方可能误以为写入生效了。若需要明确报错,必须改用触发器(在触发器里 RAISE EXCEPTION):

sql
CREATE OR REPLACE FUNCTION deny_write() RETURNS trigger AS $$
BEGIN
  RAISE EXCEPTION 'view % is read-only', TG_TABLE_NAME;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER v_report_deny
  INSTEAD OF INSERT OR UPDATE OR DELETE ON v_report
  FOR EACH ROW EXECUTE FUNCTION deny_write();

一个视图连多表:多动作规则 ​

INSTEAD 规则可以包含多条命令,用括号和 ; 分隔。这让一个视图的写入可以分发到多张基表——这是 JOIN 视图更新的常见做法:

sql
CREATE TABLE buyers (
  buyer_id bigint PRIMARY KEY,
  name     text
);

CREATE TABLE orders (
  order_id bigint PRIMARY KEY,
  buyer_id bigint REFERENCES buyers(buyer_id),
  amount   numeric(14,2)
);

-- JOIN 视图:同时展示买家名与订单金额
CREATE VIEW v_order_detail AS
SELECT o.order_id, o.buyer_id, b.name AS buyer_name, o.amount
FROM orders o JOIN buyers b ON b.buyer_id = o.buyer_id;

-- 插入时同时写两张表(若买家已存在,则靠主键冲突处理,这里简化为直接写)
CREATE RULE v_order_detail_ins AS ON INSERT TO v_order_detail
  DO INSTEAD (
    INSERT INTO buyers (buyer_id, name) VALUES (NEW.buyer_id, NEW.buyer_name)
      ON CONFLICT (buyer_id) DO NOTHING;
    INSERT INTO orders (order_id, buyer_id, amount)
      VALUES (NEW.order_id, NEW.buyer_id, NEW.amount);
  );
sql
INSERT INTO v_order_detail (order_id, buyer_id, buyer_name, amount)
VALUES (5001, 1001, '张伟', 199.00);

SELECT * FROM v_order_detail WHERE order_id = 5001;
text
 order_id | buyer_id | buyer_name | amount
----------+----------+------------+--------
     5001 |     1001 | 张伟       | 199.00
(1 row)

规则动作里可以再触发规则

规则动作生成的查询树会再次被丢回重写系统,因此动作中的命令本身也可能激活其他规则。上例中写 buyers/orders 若另有规则,会继续被改写。这也意味着要小心循环。

规则 vs INSTEAD OF 触发器:选型 ​

两者都能实现视图更新,选型取决于:

判断选规则选触发器
逻辑是简单字段映射✅ 更简洁、性能更好也可以,但更啰嗦
需要复杂 PL/pgSQL 逻辑❌ 规则里写不了过程逻辑✅
需要基于多行/聚合做决策❌✅(FOR EACH ROW 内可查询)
需要 RETURNING 且逻辑复杂受限(只能无条件 INSTEAD)✅ 更自然
需要明确拒绝并报错❌ 只能静默 NOTHING✅ RAISE EXCEPTION
大批量受影响、逻辑简单✅ 一条额外命令,快❌ 逐行回调,慢

性能上的核心差异在官方文档中说明得很清楚:规则一次命令一条额外命令;触发器每个受影响行都要调用一次。因此当一条语句影响大量行时,规则通常更快;但若规则动作产生「大量且限定不良的 JOIN」,规划器处理不好,规则也可能更慢:

text
-- 规则方案(批量):一条 DELETE,一次性处理
CREATE RULE computer_del AS ON DELETE TO computer
  DO INSTEAD DELETE FROM software WHERE hostname = OLD.hostname;

-- 触发器方案(逐行):影响 2000 行 → 2000 次触发器调用
CREATE TRIGGER computer_del_trg
  AFTER DELETE ON computer
  FOR EACH ROW EXECUTE FUNCTION delete_software();
sql
-- 批量删除场景下,对比两种方案
DELETE FROM computer WHERE hostname ~ '^old';
text
规则方案:生成一条带 JOIN 的 DELETE,一次执行完毕
触发器方案:computer 侧 2000 次行删除 → software 侧 2000 次索引扫描

依赖规则做「只写日志」的拦截 ​

INSTEAD 规则可以把一次写操作完全改成写另一张表,例如「不真正删除,只记录删除请求」:

sql
CREATE TABLE delete_requests (
  id           bigserial PRIMARY KEY,
  order_id     bigint,
  requested_at timestamptz,
  requested_by text
);

CREATE RULE order_soft_delete AS ON DELETE TO orders
  DO INSTEAD INSERT INTO delete_requests (order_id, requested_at, requested_by)
              VALUES (OLD.order_id, now(), current_user);
sql
SELECT count(*) FROM orders;

DELETE FROM orders WHERE order_id = 5001;

SELECT count(*) FROM orders;
SELECT order_id, requested_by FROM delete_requests;
text
 count
-------
 84210
(1 row)

DELETE 0

 count
-------
 84210
(1 row)

 order_id | requested_by
----------+--------------
     5001 | appuser
(1 row)

注意返回的仍是 DELETE 0——原命令被取代,orders 一行未删,删除请求被记录了下来。

规则与权限的相互作用 ​

由于规则动作以规则所属表的所有者权限执行,用 INSTEAD 规则做视图更新时,可以做到「调用者无需基表权限」:

sql
-- 视图所有者:alice;调用者:bob
CREATE VIEW v_shoelace AS SELECT sl_name, sl_avail, sl_color FROM shoelace_data;

CREATE RULE v_shoelace_ins AS ON INSERT TO v_shoelace
  DO INSTEAD INSERT INTO shoelace_data (sl_name, sl_avail, sl_color)
              VALUES (NEW.sl_name, NEW.sl_avail, NEW.sl_color);

GRANT INSERT ON v_shoelace TO bob;   -- bob 只需视图的 INSERT 权限

-- bob 执行:
INSERT INTO v_shoelace (sl_name, sl_avail, sl_color) VALUES ('b1', 3, 'red');
-- 成功:对 shoelace_data 的写入以 alice(视图所有者)的权限检查
sql
-- 确认视图所有者
SELECT relname, pg_get_userbyid(relowner) AS owner
FROM pg_class WHERE relname = 'v_shoelace';

权限设计需谨慎

规则以所有者身份运行,意味着授予视图写权限等于授予规则动作所需的全部权限。若规则动作访问了敏感表,必须确保动作本身的语义不会被滥用(例如条件不严导致越权写入)。此外,若需要行级安全(RLS),视图应设置 security_barrier 选项:

sql
CREATE VIEW v_phone WITH (security_barrier) AS
SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%';

security_barrier 会阻止恶意函数在视图过滤之前被下推,代价是性能可能显著变差(最快的计划若威胁安全就必须被拒绝执行),因此默认不开启。

规则的系统视图排查 ​

用 pg_rules 与 pg_get_ruledef() 审查 INSTEAD 规则:

sql
SELECT tablename, rulename, definition
FROM pg_rules
WHERE tablename IN ('v_shoelace', 'shoelace')
ORDER BY tablename, rulename;
text
  tablename |    rulename     |                          definition
------------+-----------------+-----------------------------------------------------------------
 shoelace   | shoelace_del    | CREATE RULE shoelace_del AS ON DELETE TO public.shoelace (...)  +
 shoelace   | shoelace_ins    | CREATE RULE shoelace_ins AS ON INSERT TO public.shoelace (...)  +
 shoelace   | shoelace_upd    | CREATE RULE shoelace_upd AS ON UPDATE TO public.shoelace (...)  +
(3 rows)
sql
-- 只列出 INSTEAD 规则及其事件类型
SELECT r.rulename,
       CASE r.ev_type WHEN 1 THEN 'SELECT' WHEN 2 THEN 'UPDATE'
                      WHEN 3 THEN 'INSERT' WHEN 4 THEN 'DELETE' END AS event,
       r.is_instead,
       r.ev_enabled
FROM pg_rewrite r
WHERE r.ev_class = 'shoelace'::regclass
ORDER BY r.rulename;
text
   rulename   | event  | is_instead | ev_enabled
--------------+--------+------------+------------
 shoelace_del | DELETE | t          | O
 shoelace_ins | INSERT | t          | O
 shoelace_upd | UPDATE | t          | O
(3 rows)

临时禁用与替换规则 ​

排障时可以临时禁用规则,或直接替换:

sql
-- 临时禁用
ALTER TABLE shoelace DISABLE RULE shoelace_del;

-- 替换(等价于删了重建)
CREATE OR REPLACE RULE shoelace_del AS ON DELETE TO shoelace
  DO INSTEAD NOTHING;

-- 彻底删除
DROP RULE shoelace_del ON shoelace;
text
-- 注意:CREATE OR REPLACE RULE 不能改变事件类型
-- 下面这条会报错(原规则是 ON DELETE,这里改成 ON INSERT)
CREATE OR REPLACE RULE shoelace_del AS ON INSERT TO shoelace DO INSTEAD NOTHING;
-- ERROR:  cannot convert rule for relation "shoelace" to view

实战示例 ​

场景:把「多表 JOIN 的订单明细视图」做成可写视图,支持插入订单并自动补建买家,同时禁止删除。

第一步:准备基表与视图。

sql
CREATE TABLE buyers (
  buyer_id bigserial PRIMARY KEY,
  name     text NOT NULL
);

CREATE TABLE orders (
  order_id   bigserial PRIMARY KEY,
  buyer_id   bigint NOT NULL REFERENCES buyers(buyer_id),
  product_id bigint NOT NULL,
  amount     numeric(14,2) NOT NULL,
  status     text NOT NULL DEFAULT 'active'
);

CREATE VIEW v_orders AS
SELECT o.order_id, o.buyer_id, b.name AS buyer_name,
       o.product_id, o.amount, o.status
FROM orders o JOIN buyers b ON b.buyer_id = o.buyer_id;

第二步:让插入可用(多动作:补买家 + 建订单),并支持 RETURNING。

sql
CREATE RULE v_orders_ins AS ON INSERT TO v_orders
  DO INSTEAD (
    INSERT INTO buyers (name) VALUES (NEW.buyer_name)
      ON CONFLICT DO NOTHING;
    INSERT INTO orders (buyer_id, product_id, amount, status)
      VALUES (NEW.buyer_id, NEW.product_id, NEW.amount, NEW.status);
  );

多动作 INSTEAD 不支持 RETURNING

含多个动作的 INSTEAD 规则不能带 RETURNING。若需要 RETURNING,规则动作必须只有一条命令。

第三步:改为单动作 + RETURNING 的写法(推荐做法)。

sql
CREATE OR REPLACE RULE v_orders_ins AS ON INSERT TO v_orders
  DO INSTEAD
  INSERT INTO orders (buyer_id, product_id, amount, status)
  VALUES (NEW.buyer_id, NEW.product_id, NEW.amount, NEW.status)
  RETURNING orders.order_id, orders.buyer_id, orders.product_id,
            orders.amount, orders.status;
sql
INSERT INTO v_orders (buyer_id, buyer_name, product_id, amount, status)
VALUES (1001, '李娜', 2001, 199.00, 'active')
RETURNING order_id, amount;
text
 order_id | amount
----------+--------
        1 | 199.00
(1 row)

第四步:更新规则——只允许改金额。

sql
CREATE RULE v_orders_upd AS ON UPDATE TO v_orders
  DO INSTEAD
  UPDATE orders SET amount = NEW.amount
  WHERE order_id = OLD.order_id
  RETURNING orders.order_id, orders.buyer_id, orders.product_id,
            orders.amount, orders.status;
sql
UPDATE v_orders SET amount = 259.00 WHERE order_id = 1 RETURNING order_id, amount;
text
 order_id | amount
----------+--------
        1 | 259.00
(1 row)

第五步:禁止删除(保护性规则)。

sql
CREATE RULE v_orders_no_del AS ON DELETE TO v_orders DO INSTEAD NOTHING;
sql
DELETE FROM v_orders WHERE order_id = 1;
text
DELETE 0

第六步:验证多动作规则的分发效果。

sql
CREATE RULE v_orders_ins_multi AS ON INSERT TO v_orders
  DO INSTEAD (
    INSERT INTO buyers (name) VALUES (NEW.buyer_name);
    INSERT INTO orders (buyer_id, product_id, amount, status)
      VALUES (NEW.buyer_id, NEW.product_id, NEW.amount, NEW.status);
  );
sql
-- 检查某次插入是否同时写了两张表
SELECT (SELECT count(*) FROM buyers) AS buyers_cnt,
       (SELECT count(*) FROM orders) AS orders_cnt;
text
 buyers_cnt | orders_cnt
------------+------------
          1 |          1
(1 row)

第七步:规则清单核对。

sql
SELECT tablename, rulename, definition
FROM pg_rules
WHERE tablename = 'v_orders'
ORDER BY rulename;
text
 tablename |     rulename      |                         definition
-----------+-------------------+-------------------------------------------------------------
 v_orders  | v_orders_ins      | CREATE RULE v_orders_ins AS ON INSERT TO public.v_orders ...+
 v_orders  | v_orders_no_del   | CREATE RULE v_orders_no_del AS ON DELETE TO public.v_orders +
 v_orders  | v_orders_upd      | CREATE RULE v_orders_upd AS ON UPDATE TO public.v_orders ...+
(3 rows)

实战小结

  • 每个想支持的写操作都要有独立规则;
  • 需要 RETURNING → 规则动作必须是单条命令 + 无条件 INSTEAD;
  • 需要多动作分发 → 放弃 RETURNING;
  • 复杂逻辑(校验、报错、条件分支)→ 改用 INSTEAD OF 触发器。

注意事项 ​

  • 错误!给视图只建条件 INSTEAD 规则。 没有无条件 INSTEAD 规则时,系统会拒绝该写操作。

    sql
    -- 不推荐:只有条件规则
    CREATE RULE r AS ON UPDATE TO v WHERE NEW.status = 'active' DO INSTEAD UPDATE base SET ...;
    UPDATE v SET status = 'x';   -- 被拒绝
  • 错误!给多动作 INSTEAD 规则加 RETURNING。

    sql
    -- 非法
    CREATE RULE r AS ON INSERT TO v DO INSTEAD (
      INSERT INTO a (...) VALUES (...);
      INSERT INTO b (...) VALUES (...);
    ) RETURNING a.id;
  • 错误!在同事件上放多个 RETURNING。 同一事件的所有规则中最多只能有一个 RETURNING。

  • 错误!在 UPDATE 规则中用 NEW.<主键> 定位旧行。 若该列被更新,将定位不到原行。

    sql
    -- 不推荐:主键被改名后匹配失败
    CREATE RULE r AS ON UPDATE TO v DO INSTEAD
      UPDATE base SET sl_name = NEW.sl_name WHERE sl_name = NEW.sl_name;   -- ❌
  • 错误!以为 RETURNING 里的 OLD.<列> 会随查询里的 OLD/NEW 变化。 它永远指向额外范围表条目,行为固定,容易误解。

  • 不推荐:用 DO INSTEAD NOTHING 做需要报错的校验。 数据被静默丢弃,调用方无从察觉。

    sql
    -- 不推荐:静默丢弃非法插入
    CREATE RULE validate AS ON INSERT TO t WHERE NEW.amount < 0 DO INSTEAD NOTHING;
  • 不推荐:给「本身可自动更新」的简单视图再建规则。 自动改写通常计划更优。

    sql
    -- 不推荐:简单单表视图画蛇添足
    CREATE RULE v_simple_ins AS ON INSERT TO v_simple DO INSTEAD INSERT INTO t ...;
  • 不推荐:在 INSTEAD 规则里写复杂过程逻辑。 规则是查询改写,不是过程代码;复杂逻辑应交给 INSTEAD OF 触发器。

    sql
    -- 不推荐:试图在规则里做多步条件判断与异常处理
  • 错误!制造循环规则。 v1 的规则指向 v2,v2 的规则又指回 v1,查询时报「infinite recursion」。

  • 不推荐:在有 INSERT/UPDATE 规则的表/视图上使用 ON CONFLICT。 官方明确不支持。

    sql
    -- 报错
    INSERT INTO v_orders (...) VALUES (...) ON CONFLICT (order_id) DO NOTHING;
  • 不推荐:忽略规则与权限的组合效应。 规则以所有者权限运行,授予视图写权限即等于授予了规则动作所需的权限。

  • 错误!指望 CREATE OR REPLACE RULE 改变事件类型。 事件类型(ON INSERT/UPDATE/DELETE)不能通过 OR REPLACE 改变,需先 DROP。

最佳实践 ​

  • 优先考虑 INSTEAD OF 触发器。 官方建议:视图更新用触发器通常更容易、更不易出错。

    sql
    CREATE TRIGGER v_orders_ins_trg
      INSTEAD OF INSERT ON v_orders
      FOR EACH ROW EXECUTE FUNCTION v_orders_insert();
  • 能用自动可更新视图就别建规则。 先检查视图是否本身可更新。

    sql
    SELECT is_updatable, is_insertable_into
    FROM information_schema.views
    WHERE table_name = 'v_orders';
  • 为每个要支持的操作建一条规则,并在文档中列出「支持哪些写操作」。

    sql
    CREATE RULE v_x_ins AS ON INSERT TO v_x DO INSTEAD ...;
    CREATE RULE v_x_upd AS ON UPDATE TO v_x DO INSTEAD ...;
    CREATE RULE v_x_del AS ON DELETE TO v_x DO INSTEAD ...;
  • 需要 RETURNING 时保持规则单动作、无条件 INSTEAD,并在 RETURNING 中直接引用结果关系列名。

    sql
    DO INSTEAD
    INSERT INTO orders (...) VALUES (...)
    RETURNING orders.order_id, orders.amount;
  • UPDATE 规则用 OLD.<稳定键> 定位行,避免因主键被更新而定位失败。

    sql
    UPDATE base SET sl_name = NEW.sl_name WHERE sl_name = OLD.sl_name;
  • 条件规则务必配无条件 DO INSTEAD NOTHING 兜底,保证哑表永不被写。

    sql
    CREATE RULE v_protect AS ON UPDATE TO v DO INSTEAD NOTHING;
  • 需明确拒绝操作时改用触发器报错,不要依赖静默 NOTHING。

    sql
    RAISE EXCEPTION 'view % is read-only', TG_TABLE_NAME;
  • 用 pg_rules / pg_get_ruledef() 定期审查规则清单,纳入上线检查。

  • 大批量、逻辑简单的改写优先用规则(一条额外命令);复杂逻辑用触发器(逐行回调)。

  • 涉及安全隔离的视图加 security_barrier,并接受其性能代价。

    sql
    CREATE VIEW v_secure WITH (security_barrier) AS SELECT ... ;
  • 改动规则前先禁用而非删除,便于回滚与排障。

    sql
    ALTER TABLE v_orders DISABLE RULE v_orders_upd;

下一节

继续学习:规则与触发器对比

参考链接 ​