Skip to content

可更新视图 ​

普通视图默认「只读」,但 PostgreSQL 对满足特定条件的视图自动支持 INSERT / UPDATE / DELETE。本节讲清哪些视图能自动更新、WITH CHECK OPTION 如何防止越权写入、不可更新视图的补救手段(INSTEAD OF 触发器),以及可更新视图在权限与行级安全下的真实行为。

前置知识

阅读本节前,建议先了解:CREATE / DROP VIEW、INSERT / UPDATE / DELETE

基础概念 ​

视图的「可更新性」是一个自动推导的结论 ​

创建视图时,PostgreSQL 会分析视图的 SELECT 结构,判断它是否能唯一映射回一张基表的一行。能,就自动可更新;不能,就只有读写分离的两条路:加 INSTEAD OF 触发器,或改用 INSTEAD 规则。

text
视图 = SELECT ... FROM 一张表 WHERE 条件
        │
        ├── 结构是否「单表直取」?
        │     是 ──▶ 自动可更新(INSERT/UPDATE/DELETE 直接转发到基表)
        │     否 ──▶ 只读,需 INSTEAD OF 触发器

可更新的判定条件 ​

条件可更新说明
单张基表 / 单张可更新视图是最基础的情况
WHERE 过滤是只要过滤条件不改变行数映射即可
列是直接引用(t.col)是写入映射回 t.col
列是表达式 / 函数否(该列不可写)视图可整体更新,但该列不能出现在写操作中
含 DISTINCT否无法确定写回哪一行
含 GROUP BY / 聚合函数否行不由单行产生
含 HAVING否同上
含 UNION / INTERSECT / EXCEPT否行来源不唯一
含 LIMIT / OFFSET / FETCH FIRST否行集合不稳定
含窗口函数否行由多行计算
含 WITH(CTE)视情况引用了不可更新结构则不可更新
含 SELECT DISTINCT ON否同 DISTINCT
多表 JOIN否无法确定写哪张表(INSTEAD OF 触发器可救)
FROM 是子查询否行不由单表产生

关键点:即使视图整体可更新,含表达式的列仍是不可写的。往这种列上写会报 cannot insert into column "xxx"。

用 information_schema 判断可更新性 ​

不需要猜,直接查:

sql
SELECT table_name, is_updatable, is_insertable_into
FROM information_schema.views
WHERE table_schema = 'public'
ORDER BY table_name;
text
    table_name    | is_updatable | is_insertable_into
------------------+--------------+--------------------
 v_orders_active  | YES          | YES
 v_order_stats    | NO           | NO
 v_user_orders    | NO           | NO
(3 rows)
字段含义
is_updatable是否支持 UPDATE / DELETE
is_insertable_into是否支持 INSERT
is_trigger_updatable是否有 INSTEAD OF 触发器处理写操作

is_trigger_updatable 是区分「自动可更新」与「触发器可更新」的关键字段。

可更新视图的写操作语义 ​

写操作最终都转发到基表,语义完全透明:

视图操作实际发生
INSERT INTO v VALUES (...)把符合视图列的行插入基表
UPDATE v SET col = ...更新视图中可见行的基表对应行
DELETE FROM v删除视图中可见行的基表对应行

重要性:视图的 WHERE 条件会约束写入范围。UPDATE v SET x = 1 只影响视图里可见的行——但 INSERT 不会被 WHERE 约束(除非启用 WITH CHECK OPTION)。

语法与用法 ​

text
-- 创建可更新视图(不需要特殊语法,条件满足即自动可更新)
CREATE VIEW name [ ( column_name [, ...] ) ]
    AS query
    [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]

-- 通过视图写入
INSERT INTO view_name [ ( columns ) ] VALUES (...);
UPDATE view_name SET column = value [, ...] [ WHERE ... ];
DELETE FROM view_name [ WHERE ... ];

-- 触发器方式(针对不可自动更新的视图)
CREATE TRIGGER trig_name
    INSTEAD OF { INSERT | UPDATE | DELETE | INSERT OR UPDATE OR DELETE }
    ON view_name
    FOR EACH ROW
    EXECUTE FUNCTION trigger_function();

最简可运行示例:

sql
CREATE TABLE products (
  id       bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name     text NOT NULL,
  price    numeric(10,2) NOT NULL,
  stock    integer NOT NULL DEFAULT 0,
  is_deleted boolean NOT NULL DEFAULT false
);

-- 单表 + 直接列引用 + WHERE 过滤 → 自动可更新
CREATE VIEW v_products_active AS
SELECT id, name, price, stock
FROM products
WHERE is_deleted = false;

INSERT INTO v_products_active (name, price, stock) VALUES ('Keyboard', 199.00, 10);
text
CREATE TABLE
CREATE VIEW
INSERT 0 1
sql
SELECT * FROM products;
text
 id |   name   | price  | stock | is_deleted
----+----------+--------+-------+------------
  1 | Keyboard | 199.00 |    10 | f
(1 row)

详细说明 ​

什么情况下视图自动可更新 ​

一个「能自动更新」的视图,必须满足每一行都由基表的一行唯一决定。用三个层次验证:

sql
CREATE TABLE orders (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    bigint NOT NULL,
  amount     numeric(12,2) NOT NULL,
  status     text NOT NULL DEFAULT 'pending',
  is_deleted boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- 可更新
CREATE VIEW v_orders_ok AS
SELECT id, user_id, amount, status FROM orders WHERE is_deleted = false;

-- 不可更新:含聚合
CREATE VIEW v_orders_agg AS
SELECT user_id, count(*) AS cnt, sum(amount) AS total FROM orders GROUP BY user_id;

-- 不可更新:含 DISTINCT
CREATE VIEW v_orders_distinct AS
SELECT DISTINCT user_id FROM orders;

-- 不可更新:含 LIMIT
CREATE VIEW v_orders_top AS
SELECT id, amount FROM orders ORDER BY amount DESC LIMIT 10;
sql
SELECT table_name, is_updatable
FROM information_schema.views
WHERE table_schema = 'public' AND table_name LIKE 'v_orders%'
ORDER BY table_name;
text
     table_name      | is_updatable
---------------------+--------------
 v_orders_agg        | NO
 v_orders_distinct   | NO
 v_orders_ok         | YES
 v_orders_top        | NO
(4 rows)

WITH CHECK OPTION:防止「写进去就看不见」 ​

这是可更新视图最重要的安全机制。默认情况下,INSERT 视图时不检查视图的 WHERE 条件:

sql
-- 视图只暴露 is_deleted = false 的行
CREATE VIEW v_products_active AS
SELECT id, name, price, stock, is_deleted FROM products WHERE is_deleted = false;

-- 可以插入 is_deleted = true 的行 —— 插进去后从视图里根本看不见
INSERT INTO v_products_active (name, price, stock, is_deleted)
VALUES ('Ghost', 9.99, 1, true);
text
INSERT 0 1
sql
-- 视图里看不到刚插入的行
SELECT count(*) FROM v_products_active WHERE name = 'Ghost';
text
 count
-------
     0
(1 row)

加上 WITH CHECK OPTION 后,任何导致新行不满足视图条件的写入都会被拒绝:

sql
DROP VIEW v_products_active;

CREATE VIEW v_products_active AS
SELECT id, name, price, stock, is_deleted
FROM products
WHERE is_deleted = false
WITH CHECK OPTION;

INSERT INTO v_products_active (name, price, stock, is_deleted)
VALUES ('Ghost', 9.99, 1, true);
text
ERROR:  new row violates check option for view "v_products_active"
DETAIL:  Failing row contains (7, Ghost, 9.99, 1, t).

UPDATE 越界同样被拦:

sql
-- 试图把可见行改成不可见(软删除)→ 被拒绝
UPDATE v_products_active SET is_deleted = true WHERE name = 'Keyboard';
text
ERROR:  new row violates check option for view "v_products_active"
DETAIL:  Failing row contains (1, Keyboard, 199.00, 10, t).

LOCAL 与 CASCADED 的区别 ​

当视图嵌套时,检查范围由 LOCAL / CASCADED 决定:

关键字检查范围默认
LOCAL只检查当前视图的条件
CASCADED检查当前视图及所有底层视图的条件默认值
sql
-- 底层视图
CREATE VIEW v_base AS
SELECT id, name, price, is_deleted FROM products WHERE is_deleted = false
WITH CHECK OPTION;

-- 中层视图:LOCAL 只检查「price > 100」这一条
CREATE VIEW v_mid_local AS
SELECT * FROM v_base WHERE price > 100
WITH LOCAL CHECK OPTION;

-- 中层视图:CASCADED 会连 v_base 的 is_deleted = false 一起检查
CREATE VIEW v_mid_cascaded AS
SELECT * FROM v_base WHERE price > 100
WITH CASCADED CHECK OPTION;

对比效果:

sql
-- LOCAL:只查 price > 100,is_deleted = true 竟然能通过(但下层 v_base 有 CHECK OPTION 仍会拦)
INSERT INTO v_mid_local (name, price, is_deleted) VALUES ('A', 150, false);   -- OK
INSERT INTO v_mid_local (name, price, is_deleted) VALUES ('B', 50,  false);   -- 被 v_mid_local 拒绝
text
INSERT 0 1
ERROR:  new row violates check option for view "v_mid_local"
DETAIL:  Failing row contains (8, B, 50.00, f).

记忆方式:LOCAL 只管自己这一层;CASCADED 从自己往上(更靠近基表的方向)一路都管。生产环境建议显式写 CASCADED,语义最不容易搞错。

WITH CHECK OPTION 不适用于已有行的 UPDATE 之外的情况 ​

有三点容易误解:

  1. 只对 INSERT 和 UPDATE 生效,DELETE 不受影响(删除不可见行本来就不可能,因为视图里没有)。
  2. 不检查 WHERE 以外的条件——比如列上的 CHECK 约束仍由基表负责。
  3. UPDATE 时检查的是「更新后的完整行」,因此「改一半导致越界」也会被拦。
sql
-- 把可见行改成价格低于阈值 → 被 v_mid_local 拦下
UPDATE v_mid_local SET price = 10 WHERE name = 'A';
text
ERROR:  new row violates check option for view "v_mid_local"
DETAIL:  Failing row contains (7, A, 10.00, f).

含表达式的列不可写 ​

视图可以包含表达式列,但这些列不能被写入:

sql
CREATE VIEW v_orders_calc AS
SELECT
  id,
  user_id,
  amount,
  amount * 1.06 AS amount_with_tax   -- 表达式列
FROM orders;

\d v_orders_calc
text
          View "public.v_orders_calc"
     Column      |  Type   | Collation | Nullable | Default
-----------------+---------+-----------+----------+---------
 id              | bigint  |           |          |
 user_id         | bigint  |           |          |
 amount          | numeric |           |          |
 amount_with_tax | numeric |           |          |
sql
-- 错误!无法写入表达式列
INSERT INTO v_orders_calc (user_id, amount, amount_with_tax) VALUES (1, 100, 106);
text
ERROR:  cannot insert into column "amount_with_tax" of view "v_orders_calc"
DETAIL:  View columns that are not columns of their base relation are not updatable.

但其余列照常可写:

sql
INSERT INTO v_orders_calc (user_id, amount) VALUES (1, 100);
text
INSERT 0 1

同理,UPDATE 表达式列也会失败:

sql
-- 错误!
UPDATE v_orders_calc SET amount_with_tax = 200 WHERE id = 1;
text
ERROR:  cannot update column "amount_with_tax" of view "v_orders_calc"
DETAIL:  View columns that are not columns of their base relation are not updatable.

生成列与默认值在可更新视图上的行为 ​

基表上的默认值与生成列(GENERATED ... AS IDENTITY 或 GENERATED ALWAYS AS (...) STORED)在通过视图写入时照常生效:

sql
CREATE TABLE items (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       text NOT NULL,
  qty        integer NOT NULL DEFAULT 1,
  unit_price numeric(10,2) NOT NULL,
  total      numeric(12,2) GENERATED ALWAYS AS (qty * unit_price) STORED,
  is_deleted boolean NOT NULL DEFAULT false
);

CREATE VIEW v_items AS
SELECT id, name, qty, unit_price, total
FROM items WHERE is_deleted = false;

-- id 由序列生成,total 由生成列算出,都不需要(也不能)显式写入
INSERT INTO v_items (name, qty, unit_price) VALUES ('Widget', 3, 19.90);
text
INSERT 0 1
sql
SELECT * FROM v_items;
text
 id |  name  | qty | unit_price | total
----+--------+-----+------------+-------
  1 | Widget |   3 |      19.90 | 59.70
(1 row)

注意两个限制:

sql
-- 错误!生成列不能显式写入
INSERT INTO v_items (name, qty, unit_price, total) VALUES ('X', 1, 1.00, 1.00);
text
ERROR:  cannot insert a non-DEFAULT value into column "total"
DETAIL:  Column "total" is a generated column.
sql
-- IDENTITY 生成列(ALWAYS)同样不能写入
INSERT INTO v_items (id, name, qty, unit_price) VALUES (999, 'X', 1, 1.00);
text
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

可更新视图的权限要求 ​

通过视图写入时,权限检查是分两步的:

检查项说明
视图上的权限调用者需要对视图有 INSERT / UPDATE / DELETE 权限
基表上的权限视图所有者需要对应权限(默认按定义者权限执行)
sql
CREATE ROLE app_writer LOGIN PASSWORD 'secret';
GRANT USAGE ON SCHEMA public TO app_writer;
GRANT INSERT, UPDATE, DELETE, SELECT ON v_products_active TO app_writer;

SET ROLE app_writer;
INSERT INTO v_products_active (name, price, stock) VALUES ('Mouse', 99.00, 20);
text
INSERT 0 1

关键在于:app_writer 不需要对 products 表有任何权限——权限判定用的是视图所有者的身份。这与视图默认的 security_invoker = false 一致(见 CREATE / DROP VIEW)。

sql
RESET ROLE;

权限提升风险

如果视图所有者权限很高,且视图定义里包含可以被绕过的条件,普通角色就能借视图写入原本不该写的行。启用 WITH CHECK OPTION 是第一道防线;把 security_invoker 设为 true(PG 15+)是第二道。

可更新视图 + 行级安全(RLS) ​

行级安全(Row-Level Security)在基表上定义。当调用者通过视图写入时,RLS 策略仍会在基表上生效——但取决于视图按谁的权限执行:

视图 security_invokerRLS 按谁的身份判定
false(默认)视图所有者(若所有者是表 owner,常被 BYPASSRLS 或表 owner 豁免绕过)
true(PG 15+)调用者
sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY p_orders_owner ON orders
  FOR ALL
  USING (user_id = current_setting('app.user_id')::bigint)
  WITH CHECK (user_id = current_setting('app.user_id')::bigint);

若希望 RLS 真正约束通过视图写入的调用者,必须让视图以调用者身份执行:

sql
-- PostgreSQL 15+
CREATE VIEW v_orders_secure
WITH (security_invoker = true) AS
SELECT id, user_id, amount, status FROM orders;

否则表 owner 通过视图写入时,RLS 可能完全不起作用(表 owner 默认绕过自己的策略,除非显式 ALTER TABLE ... FORCE ROW LEVEL SECURITY)。

不可更新视图的补救:INSTEAD OF 触发器 ​

多表 JOIN、聚合等视图无法自动更新,标准解法是挂 INSTEAD OF 触发器:

sql
-- 多表 JOIN 视图:不可自动更新
CREATE VIEW v_order_detail AS
SELECT
  o.id         AS order_id,
  o.user_id,
  o.amount,
  u.name       AS user_name
FROM orders o
JOIN users u ON u.id = o.user_id;

SELECT is_updatable FROM information_schema.views WHERE table_name = 'v_order_detail';
text
 is_updatable
--------------
 NO
(1 row)
sql
-- 写一个把写操作转发到 orders 的触发器函数
CREATE OR REPLACE FUNCTION tf_order_detail_insert()
RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO orders (user_id, amount)
  VALUES (NEW.user_id, NEW.amount);
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_order_detail_insert
  INSTEAD OF INSERT ON v_order_detail
  FOR EACH ROW
  EXECUTE FUNCTION tf_order_detail_insert();

-- 现在可以对视图写入
INSERT INTO v_order_detail (user_id, amount) VALUES (1, 500.00);
text
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1

INSTEAD OF 触发器必须定义 FOR EACH ROW,且只能建在视图上。它与 INSTEAD 规则的完整对比见 规则与触发器对比 与 INSTEAD 规则。

可更新视图的 RETURNING 行为 ​

自动可更新的视图支持 RETURNING,返回的是视图的列:

sql
INSERT INTO v_products_active (name, price, stock)
VALUES ('Monitor', 899.00, 5)
RETURNING id, name, price;
text
 id |  name   | price
----+---------+--------
  3 | Monitor | 899.00
(1 row)

但如果视图包含表达式列,RETURNING 该列会取到基表回算后的值(因为表达式在插入后重新求值):

sql
INSERT INTO v_orders_calc (user_id, amount) VALUES (1, 200)
RETURNING id, amount, amount_with_tax;
text
 id | amount | amount_with_tax
----+--------+-----------------
  2 | 200.00 |          212.00
(1 row)

注意:这一点在 INSTEAD OF 触发器实现的可写视图上不成立——触发器实现的可写视图常返回空或返回触发器显式返回的行,这是常见坑(详见 INSTEAD 规则)。

实战示例 ​

场景:用可更新视图实现电商订单的「软删除过滤 + 状态约束」,让应用层只看到一个干净的活跃订单视图,并防止越权写入。

第 1 步:建表与数据

sql
DROP TABLE IF EXISTS orders CASCADE;

CREATE TABLE orders (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    bigint  NOT NULL,
  amount     numeric(12,2) NOT NULL CHECK (amount > 0),
  status     text    NOT NULL DEFAULT 'pending'
             CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  is_deleted boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO orders (user_id, amount, status, is_deleted)
SELECT
  1 + (g % 100),
  (10 + (g % 990))::numeric(12,2),
  (ARRAY['pending','paid','shipped','cancelled'])[1 + (g % 4)],
  (g % 17 = 0)
FROM generate_series(1, 10000) AS g;

ANALYZE orders;
text
DROP TABLE
CREATE TABLE
INSERT 0 10000
ANALYZE

第 2 步:创建带 WITH CHECK OPTION 的可更新视图

sql
CREATE VIEW v_orders_active AS
SELECT id, user_id, amount, status, created_at
FROM orders
WHERE is_deleted = false
  AND status <> 'cancelled'
WITH CHECK OPTION;

第 3 步:验证可更新性

sql
SELECT is_updatable, is_insertable_into
FROM information_schema.views
WHERE table_name = 'v_orders_active';
text
 is_updatable | is_insertable_into
--------------+--------------------
 YES          | YES
(1 row)

第 4 步:合法写入

sql
INSERT INTO v_orders_active (user_id, amount, status)
VALUES (5, 299.00, 'pending')
RETURNING id, status;
text
   id   | status
--------+---------
 10001  | pending
(1 row)

第 5 步:越权写入被拦截(本质是 WITH CHECK OPTION 生效)

sql
-- 试图插入「已取消」订单
INSERT INTO v_orders_active (user_id, amount, status) VALUES (5, 100, 'cancelled');
text
ERROR:  new row violates check option for view "v_orders_active"
DETAIL:  Failing row contains (10002, 5, 100.00, cancelled, 2026-09-28 10:40:00+08).
sql
-- 试图软删除(把可见行改成不可见)
UPDATE v_orders_active SET status = 'cancelled' WHERE id = 10001;
text
ERROR:  new row violates check option for view "v_orders_active"
DETAIL:  Failing row contains (10001, 5, 299.00, cancelled, 2026-09-28 10:40:00+08).

第 6 步:确认 UPDATE 只影响可见行

sql
-- 这条语句只更新视图里可见的行,已软删除/已取消的行原样不动
UPDATE v_orders_active SET amount = amount + 1 WHERE user_id = 5;
text
UPDATE 98
sql
-- 对照:直接改全表会影响更多行
SELECT
  (SELECT count(*) FROM orders        WHERE user_id = 5) AS total_rows,
  (SELECT count(*) FROM v_orders_active WHERE user_id = 5) AS visible_rows;
text
 total_rows | visible_rows
------------+--------------
        101 |           98
(3 rows)

第 7 步:EXPLAIN 看写操作的执行计划

sql
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
UPDATE v_orders_active SET amount = amount + 1 WHERE id = 10001;
text
                             QUERY PLAN
---------------------------------------------------------------------
 Update on orders
   ->  Index Scan using orders_pkey on orders
         Index Cond: (id = 10001)
         Filter: ((NOT is_deleted) AND (status <> 'cancelled'::text))
(3 rows)

计划里目标表是 orders,且在基表上追加了视图的 Filter 条件——这直观展示了「视图写操作被重写为对基表的写操作」。

第 8 步:清理

sql
DROP VIEW v_orders_active;
DROP TABLE orders;

注意事项 ​

  • INSERT 不受视图 WHERE 约束。 不加 WITH CHECK OPTION,可以插入「插进去就看不见」的行,这是最隐蔽的数据污染来源。

    sql
    -- 错误!会插入不可见行
    INSERT INTO v_products_active (name, price, stock, is_deleted) VALUES ('Ghost', 9.99, 1, true);
  • 复合视图(JOIN、聚合、DISTINCT、LIMIT、UNION)不可自动更新。 不要以为「视图能 SELECT 就能写」。

  • 含表达式的列不可写。 写入会报 View columns that are not columns of their base relation are not updatable。

  • 生成列与 GENERATED ALWAYS AS IDENTITY 列不能显式写入。

    sql
    -- 错误!cannot insert a non-DEFAULT value into column "total"
    INSERT INTO v_items (name, qty, unit_price, total) VALUES ('X', 1, 1.00, 1.00);
  • WITH LOCAL CHECK OPTION 只管本层。 嵌套视图场景优先用 CASCADED(默认)。

  • WITH CHECK OPTION 管不到 DELETE。 删除只需行可见即可。

  • 默认 security_invoker = false 下,RLS 可能不作用于通过视图写入的调用者。 需要 RLS 兜底时用 security_invoker = true(PG 15+)或 FORCE ROW LEVEL SECURITY。

  • INSTEAD OF 触发器必须 FOR EACH ROW,且只能建在视图上,不能建在表上。

  • INSTEAD OF 触发器的 RETURNING 行为与自动可更新视图不同,容易返回空行集,迁移时要特别验证。

  • 视图写操作的实际目标表在 EXPLAIN 里可见,排查「写到了哪张表」时先看计划头行。

最佳实践 ​

  • 凡是要对外写入的视图,一律加 WITH CASCADED CHECK OPTION。 这是防止数据越界的最低成本手段:

    sql
    CREATE VIEW v_orders_active AS
    SELECT id, user_id, amount, status FROM orders
    WHERE is_deleted = false AND status <> 'cancelled'
    WITH CASCADED CHECK OPTION;
  • 用可更新视图统一实现「软删除过滤」,让应用层永远看不到已删除数据,也不用重复写 WHERE is_deleted = false。

  • 视图只暴露「直接引用」的列,需要派生列时另建只读视图,把可写视图与展示视图分开。

  • 给可写视图单独命名(如 v_xxx_w),让「能写」与「只读」一眼可辨。

  • 授权时用 GRANT INSERT/UPDATE/DELETE ON view,不要授基表权限,保持接口封闭:

    sql
    GRANT SELECT, INSERT, UPDATE, DELETE ON v_orders_active TO app_writer;
  • 需要 RLS 兜底时显式 security_invoker = true(PG 15+),并测试调用者是否拥有基表权限。

  • 复杂可写视图(JOIN / 多表)统一使用 INSTEAD OF 触发器,不要在应用层拆解写操作。

  • 上线后用 information_schema.views 做一次可更新性审计,避免误以为某视图可写:

    sql
    SELECT table_name, is_updatable, is_insertable_into, is_trigger_updatable
    FROM information_schema.views WHERE table_schema = 'public'
    ORDER BY is_updatable, table_name;
  • 写操作后用 RETURNING 校验落库结果,尤其能发现 CHECK OPTION 与触发器带来的差异。

下一节

继续学习:视图与性能(视图展开)

参考链接 ​