2024 年 9 月 26 日: PostgreSQL 17 发布!
支持版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4

39.4. INSERTUPDATEDELETE 规则 #

INSERTUPDATEDELETE 上定义的规则与前面几节中描述的视图规则有很大不同。首先,它们的 CREATE RULE 命令允许更多

  • 它们可以没有操作。

  • 它们可以有多个操作。

  • 它们可以是 INSTEADALSO(默认值)。

  • 伪关系 NEWOLD 变得有用。

  • 它们可以有规则限定条件。

其次,它们不会就地修改查询树。相反,它们会创建零个或多个新的查询树,并且可以丢弃原始查询树。

注意

在许多情况下,可以用 INSERT/UPDATE/DELETE 规则执行的任务,最好用触发器完成。触发器在表示法上稍微复杂一些,但它们的语义更容易理解。当原始查询包含易变函数时,规则往往会产生令人惊讶的结果:在执行规则的过程中,易变函数的执行次数可能比预期多。

此外,某些情况根本不受这些类型规则的支持,特别包括原始查询中的 WITH 子句和 UPDATE 查询的 SET 列表中的多赋值子 SELECT。这是因为将这些构造复制到规则查询会导致子查询多次评估,这与查询作者的明确意图相矛盾。

39.4.1. 更新规则的工作原理 #

牢记语法

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

在以下内容中,更新规则 指的是在 INSERTUPDATEDELETE 上定义的规则。

当查询树的结果关系和命令类型等于 CREATE RULE 命令中给出的对象和事件时,更新规则会由规则系统应用。对于更新规则,规则系统会创建一个查询树列表。最初,查询树列表为空。可以有零个(NOTHING 关键字)、一个或多个操作。为了简化,我们将查看一个具有一个操作的规则。此规则可以有或没有限定条件,并且可以是 INSTEADALSO(默认值)。

什么是规则限定条件?它是一个限制,告诉何时应该执行规则的操作,何时不应该执行。此限定条件只能引用伪关系 NEW 和/或 OLD,它们基本上代表作为对象给出的关系(但具有特殊含义)。

因此,我们有三种情况,它们为一个操作的规则生成以下查询树。

没有限定条件,使用 ALSOINSTEAD

来自规则操作的查询树,添加了原始查询树的限定条件

给定限定条件并使用 ALSO

来自规则操作的查询树,添加了规则限定条件和原始查询树的限定条件

给定限定条件并使用 INSTEAD

来自规则操作的查询树,添加了规则限定条件和原始查询树的限定条件;以及添加了否定规则限定条件的原始查询树

最后,如果规则是 ALSO,则将未更改的原始查询树添加到列表中。由于只有限定的 INSTEAD 规则已经添加了原始查询树,因此对于具有一个操作的规则,我们最终会得到一个或两个输出查询树。

对于 ON INSERT 规则,在添加规则的操作之前,执行原始查询(如果未被 INSTEAD 抑制)。这使操作能够看到插入的行。但是,对于 ON UPDATEON DELETE 规则,在添加规则的操作之后,执行原始查询。这确保操作能够看到要更新或要删除的行;否则,操作可能无济于事,因为它们找不到与它们的限定条件匹配的行。

从规则操作生成的查询树将再次被抛入重写系统,并且可能应用更多规则,导致产生更多或更少的查询树。因此,规则的操作必须具有与规则本身不同的命令类型或不同的结果关系,否则此递归过程将最终陷入无限循环。(递归展开规则将被检测到并报告为错误。)

pg_rewrite 系统目录的操作中找到的查询树只是模板。由于它们可以引用 NEWOLD 的范围表条目,因此在使用它们之前必须进行一些替换。对于对 NEW 的任何引用,都会在原始查询的目标列表中搜索相应的条目。如果找到,则该条目的表达式将替换引用。否则,NEW 的含义与 OLD 相同(对于 UPDATE)或被替换为 null 值(对于 INSERT)。对 OLD 的任何引用都将被替换为对范围表条目的引用,该条目是结果关系。

在系统完成应用更新规则后,它会将视图规则应用于生成的查询树。视图不能插入新的更新操作,因此无需将更新规则应用于视图重写的输出。

39.4.1.1. 一步步完成第一个规则 #

假设我们想要跟踪 shoelace_data 关系中 sl_avail 列的变化。因此,我们设置了一个日志表和一个规则,该规则在对 shoelace_data 执行 UPDATE 时有条件地写入日志条目。

CREATE TABLE shoelace_log (
    sl_name    text,          -- shoelace changed
    sl_avail   integer,       -- new available value
    log_who    text,          -- who did it
    log_when   timestamp      -- when
);

CREATE RULE log_shoelace AS ON UPDATE TO shoelace_data
    WHERE NEW.sl_avail <> OLD.sl_avail
    DO 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';

我们查看日志表

SELECT * FROM shoelace_log;

 sl_name | sl_avail | log_who | log_when
---------+----------+---------+----------------------------------
 sl7     |        6 | Al      | Tue Oct 20 16:14:45 1998 MET DST
(1 row)

这就是我们所预期的。在后台发生的事情如下。解析器创建了查询树

UPDATE shoelace_data SET sl_avail = 6
  FROM shoelace_data shoelace_data
 WHERE shoelace_data.sl_name = 'sl7';

有一个规则 log_shoelace,它是 ON UPDATE,具有规则限定条件表达式

NEW.sl_avail <> OLD.sl_avail

以及操作

INSERT INTO shoelace_log VALUES (
       new.sl_name, new.sl_avail,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old;

(这看起来有点奇怪,因为您通常无法写入 INSERT ... VALUES ... FROM。这里的 FROM 子句只是为了表明查询树中存在 newold 的范围表条目。这些是必需的,以便它们可以被 INSERT 命令的查询树中的变量引用。)

该规则是一个限定的 ALSO 规则,因此规则系统必须返回两个查询树:修改后的规则操作和原始查询树。在步骤 1 中,原始查询的范围表被合并到规则的操作查询树中。这将导致

INSERT INTO shoelace_log VALUES (
       new.sl_name, new.sl_avail,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old,
       shoelace_data shoelace_data;

在步骤 2 中,将规则限定条件添加到其中,因此结果集被限制为 sl_avail 发生变化的行

INSERT INTO shoelace_log VALUES (
       new.sl_name, new.sl_avail,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old,
       shoelace_data shoelace_data
 WHERE new.sl_avail <> old.sl_avail;

(这看起来更奇怪,因为 INSERT ... VALUES 也没有 WHERE 子句,但规划器和执行器不会遇到任何困难。无论如何,它们都需要支持此相同的功能,用于 INSERT ... SELECT。)

在步骤 3 中,添加了原始查询树的限定条件,进一步限制结果集,只保留原始查询本来要触及的行

INSERT INTO shoelace_log VALUES (
       new.sl_name, new.sl_avail,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old,
       shoelace_data shoelace_data
 WHERE new.sl_avail <> old.sl_avail
   AND shoelace_data.sl_name = 'sl7';

步骤 4 将对 NEW 的引用替换为原始查询树中的目标列表条目,或替换为结果关系中的匹配变量引用

INSERT INTO shoelace_log VALUES (
       shoelace_data.sl_name, 6,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old,
       shoelace_data shoelace_data
 WHERE 6 <> old.sl_avail
   AND shoelace_data.sl_name = 'sl7';

步骤 5 将 OLD 引用更改为结果关系引用

INSERT INTO shoelace_log VALUES (
       shoelace_data.sl_name, 6,
       current_user, current_timestamp )
  FROM shoelace_data new, shoelace_data old,
       shoelace_data shoelace_data
 WHERE 6 <> shoelace_data.sl_avail
   AND shoelace_data.sl_name = 'sl7';

就是这样。由于该规则是 ALSO,我们还输出原始查询树。简而言之,规则系统的输出是一个包含两个查询树的列表,它们对应于以下语句

INSERT INTO shoelace_log VALUES (
       shoelace_data.sl_name, 6,
       current_user, current_timestamp )
  FROM shoelace_data
 WHERE 6 <> shoelace_data.sl_avail
   AND shoelace_data.sl_name = 'sl7';

UPDATE shoelace_data SET sl_avail = 6
 WHERE sl_name = 'sl7';

这些语句按此顺序执行,这正是规则的本意。

替换和添加的限定条件确保如果原始查询是,例如

UPDATE shoelace_data SET sl_color = 'green'
 WHERE sl_name = 'sl7';

不会写入任何日志条目。在这种情况下,原始查询树不包含 sl_avail 的目标列表条目,因此 NEW.sl_avail 将被替换为 shoelace_data.sl_avail。因此,规则生成的额外命令是

INSERT INTO shoelace_log VALUES (
       shoelace_data.sl_name, shoelace_data.sl_avail,
       current_user, current_timestamp )
  FROM shoelace_data
 WHERE shoelace_data.sl_avail <> shoelace_data.sl_avail
   AND shoelace_data.sl_name = 'sl7';

并且该限定条件永远不会为真。

如果原始查询修改多行,它也将会起作用。因此,如果有人发出了命令

UPDATE shoelace_data SET sl_avail = 0
 WHERE sl_color = 'black';

实际上更新了四行(sl1sl2sl3sl4)。但是,sl3 已经具有 sl_avail = 0。在这种情况下,原始查询树的限定条件不同,这将导致规则生成额外的查询树

INSERT INTO shoelace_log
SELECT shoelace_data.sl_name, 0,
       current_user, current_timestamp
  FROM shoelace_data
 WHERE 0 <> shoelace_data.sl_avail
   AND shoelace_data.sl_color = 'black';

被规则生成。此查询树肯定会插入三个新的日志条目。这绝对正确。

这里我们可以看到为什么原始查询树必须最后执行。如果UPDATE首先执行,所有行都将被设置为零,因此日志记录INSERT将找不到任何0 <> shoelace_data.sl_avail的行。

39.4.2. 与视图的合作 #

保护视图关系免受有人尝试在视图关系上运行INSERTUPDATEDELETE的可能性的一个简单方法是让这些查询树被丢弃。因此,我们可以创建以下规则

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执行任何这些操作,规则系统将应用这些规则。由于这些规则没有操作且为INSTEAD,因此生成的查询树列表将为空,并且整个查询将变为空,因为在规则系统处理完它之后,没有剩下任何东西需要优化或执行。

使用规则系统的更复杂方法是创建将查询树重写为对真实表执行正确操作的查询树的规则。为了在shoelace视图上执行此操作,我们创建以下规则

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

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;

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

如果要支持视图上的RETURNING查询,则需要使规则包含RETURNING子句,这些子句计算视图行。对于单个表的视图,这通常非常简单,但对于像shoelace这样的联接视图来说,这有点繁琐。插入情况的示例是

CREATE RULE shoelace_ins AS ON INSERT TO shoelace
    DO INSTEAD
    INSERT INTO shoelace_data VALUES (
           NEW.sl_name,
           NEW.sl_avail,
           NEW.sl_color,
           NEW.sl_len,
           NEW.sl_unit
    )
    RETURNING
           shoelace_data.*,
           (SELECT shoelace_data.sl_len * u.un_fact
            FROM unit u WHERE shoelace_data.sl_unit = u.un_name);

请注意,此规则支持视图上的INSERTINSERT RETURNING查询 - 对于INSERTRETURNING子句将被简单地忽略。

现在假设偶尔会有一包鞋带运到商店,并附带一份详细的零件清单。但您不想每次都手动更新shoelace视图。相反,我们设置了两个小表:一个用于插入零件清单中的项目,另一个带有特殊技巧。它们的创建命令如下

CREATE TABLE shoelace_arrive (
    arr_name    text,
    arr_quant   integer
);

CREATE TABLE shoelace_ok (
    ok_name     text,
    ok_quant    integer
);

CREATE RULE shoelace_ok_ins AS ON INSERT TO shoelace_ok
    DO INSTEAD
    UPDATE shoelace
       SET sl_avail = sl_avail + NEW.ok_quant
     WHERE sl_name = NEW.ok_name;

现在您可以用零件清单中的数据填充shoelace_arrive

SELECT * FROM shoelace_arrive;

 arr_name | arr_quant
----------+-----------
 sl3      |        10
 sl6      |        20
 sl8      |        20
(3 rows)

快速查看当前数据

SELECT * FROM shoelace;

 sl_name  | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
----------+----------+----------+--------+---------+-----------
 sl1      |        5 | black    |     80 | cm      |        80
 sl2      |        6 | black    |    100 | cm      |       100
 sl7      |        6 | brown    |     60 | cm      |        60
 sl3      |        0 | black    |     35 | inch    |      88.9
 sl4      |        8 | black    |     40 | inch    |     101.6
 sl8      |        1 | brown    |     40 | inch    |     101.6
 sl5      |        4 | brown    |      1 | m       |       100
 sl6      |        0 | brown    |    0.9 | m       |        90
(8 rows)

现在将到达的鞋带移入

INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;

并检查结果

SELECT * FROM shoelace ORDER BY sl_name;

 sl_name  | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
----------+----------+----------+--------+---------+-----------
 sl1      |        5 | black    |     80 | cm      |        80
 sl2      |        6 | black    |    100 | cm      |       100
 sl7      |        6 | brown    |     60 | cm      |        60
 sl4      |        8 | black    |     40 | inch    |     101.6
 sl3      |       10 | black    |     35 | inch    |      88.9
 sl8      |       21 | brown    |     40 | inch    |     101.6
 sl5      |        4 | brown    |      1 | m       |       100
 sl6      |       20 | brown    |    0.9 | m       |        90
(8 rows)

SELECT * FROM shoelace_log;

 sl_name | sl_avail | log_who| log_when
---------+----------+--------+----------------------------------
 sl7     |        6 | Al     | Tue Oct 20 19:14:45 1998 MET DST
 sl3     |       10 | Al     | Tue Oct 20 19:25:16 1998 MET DST
 sl6     |       20 | Al     | Tue Oct 20 19:25:16 1998 MET DST
 sl8     |       21 | Al     | Tue Oct 20 19:25:16 1998 MET DST
(4 rows)

从一个INSERT ... SELECT到这些结果,这是一段很长的路。查询树转换的描述将是本章的最后一个。首先,有解析器的输出

INSERT INTO shoelace_ok
SELECT shoelace_arrive.arr_name, shoelace_arrive.arr_quant
  FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok;

现在应用第一个规则shoelace_ok_ins,将其转换为

UPDATE shoelace
   SET sl_avail = shoelace.sl_avail + shoelace_arrive.arr_quant
  FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
       shoelace_ok old, shoelace_ok new,
       shoelace shoelace
 WHERE shoelace.sl_name = shoelace_arrive.arr_name;

并丢弃对shoelace_ok的原始INSERT。此重写的查询再次传递给规则系统,应用的第二个规则shoelace_upd产生

UPDATE shoelace_data
   SET sl_name = shoelace.sl_name,
       sl_avail = shoelace.sl_avail + shoelace_arrive.arr_quant,
       sl_color = shoelace.sl_color,
       sl_len = shoelace.sl_len,
       sl_unit = shoelace.sl_unit
  FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
       shoelace_ok old, shoelace_ok new,
       shoelace shoelace, shoelace old,
       shoelace new, shoelace_data shoelace_data
 WHERE shoelace.sl_name = shoelace_arrive.arr_name
   AND shoelace_data.sl_name = shoelace.sl_name;

同样,它是一个INSTEAD规则,并且先前的查询树被丢弃。请注意,此查询仍然使用视图shoelace。但规则系统尚未完成此步骤,因此它继续并在其上应用_RETURN规则,我们得到

UPDATE shoelace_data
   SET sl_name = s.sl_name,
       sl_avail = s.sl_avail + shoelace_arrive.arr_quant,
       sl_color = s.sl_color,
       sl_len = s.sl_len,
       sl_unit = s.sl_unit
  FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
       shoelace_ok old, shoelace_ok new,
       shoelace shoelace, shoelace old,
       shoelace new, shoelace_data shoelace_data,
       shoelace old, shoelace new,
       shoelace_data s, unit u
 WHERE s.sl_name = shoelace_arrive.arr_name
   AND shoelace_data.sl_name = s.sl_name;

最后,应用规则log_shoelace,生成额外的查询树

INSERT INTO shoelace_log
SELECT s.sl_name,
       s.sl_avail + shoelace_arrive.arr_quant,
       current_user,
       current_timestamp
  FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
       shoelace_ok old, shoelace_ok new,
       shoelace shoelace, shoelace old,
       shoelace new, shoelace_data shoelace_data,
       shoelace old, shoelace new,
       shoelace_data s, unit u,
       shoelace_data old, shoelace_data new
       shoelace_log shoelace_log
 WHERE s.sl_name = shoelace_arrive.arr_name
   AND shoelace_data.sl_name = s.sl_name
   AND (s.sl_avail + shoelace_arrive.arr_quant) <> s.sl_avail;

之后,规则系统用完规则并返回生成的查询树。

因此,我们最终得到了两个最终的查询树,它们等效于SQL语句

INSERT INTO shoelace_log
SELECT s.sl_name,
       s.sl_avail + shoelace_arrive.arr_quant,
       current_user,
       current_timestamp
  FROM shoelace_arrive shoelace_arrive, shoelace_data shoelace_data,
       shoelace_data s
 WHERE s.sl_name = shoelace_arrive.arr_name
   AND shoelace_data.sl_name = s.sl_name
   AND s.sl_avail + shoelace_arrive.arr_quant <> s.sl_avail;

UPDATE shoelace_data
   SET sl_avail = shoelace_data.sl_avail + shoelace_arrive.arr_quant
  FROM shoelace_arrive shoelace_arrive,
       shoelace_data shoelace_data,
       shoelace_data s
 WHERE s.sl_name = shoelace_arrive.sl_name
   AND shoelace_data.sl_name = s.sl_name;

结果是,来自一个关系的数据插入到另一个关系,更改为对第三个关系的更新,更改为更新第四个关系,加上在第五个关系中记录最终更新,都被简化为两个查询。

有一个细节有点难看。查看这两个查询,事实证明shoelace_data关系在范围表中出现了两次,而它绝对可以减少到一次。规划器不处理它,因此规则系统输出的INSERT的执行计划将是

Nested Loop
  ->  Merge Join
        ->  Seq Scan
              ->  Sort
                    ->  Seq Scan on s
        ->  Seq Scan
              ->  Sort
                    ->  Seq Scan on shoelace_arrive
  ->  Seq Scan on shoelace_data

而省略额外的范围表条目将导致

Merge Join
  ->  Seq Scan
        ->  Sort
              ->  Seq Scan on s
  ->  Seq Scan
        ->  Sort
              ->  Seq Scan on shoelace_arrive

它在日志表中生成完全相同的条目。因此,规则系统导致对shoelace_data表进行了一次额外的扫描,这绝对没有必要。并且UPDATE中再次进行相同的冗余扫描。但要使这一切成为可能确实是一项非常艰巨的任务。

现在,我们对PostgreSQL规则系统及其功能进行最终演示。假设您在数据库中添加了一些颜色非凡的鞋带

INSERT INTO shoelace VALUES ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
INSERT INTO shoelace VALUES ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);

我们想创建一个视图来检查哪些shoelace条目在颜色上不适合任何鞋子。为此,视图是

CREATE VIEW shoelace_mismatch AS
    SELECT * FROM shoelace WHERE NOT EXISTS
        (SELECT shoename FROM shoe WHERE slcolor = sl_color);

它的输出是

SELECT * FROM shoelace_mismatch;

 sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
---------+----------+----------+--------+---------+-----------
 sl9     |        0 | pink     |     35 | inch    |      88.9
 sl10    |     1000 | magenta  |     40 | inch    |     101.6

现在,我们要将其设置好,以便将不在库存中的不匹配鞋带从数据库中删除。为了让PostgreSQL更加困难,我们不会直接删除它。相反,我们创建一个视图

CREATE VIEW shoelace_can_delete AS
    SELECT * FROM shoelace_mismatch WHERE sl_avail = 0;

并以这种方式执行

DELETE FROM shoelace WHERE EXISTS
    (SELECT * FROM shoelace_can_delete
             WHERE sl_name = shoelace.sl_name);

结果是

SELECT * FROM shoelace;

 sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
---------+----------+----------+--------+---------+-----------
 sl1     |        5 | black    |     80 | cm      |        80
 sl2     |        6 | black    |    100 | cm      |       100
 sl7     |        6 | brown    |     60 | cm      |        60
 sl4     |        8 | black    |     40 | inch    |     101.6
 sl3     |       10 | black    |     35 | inch    |      88.9
 sl8     |       21 | brown    |     40 | inch    |     101.6
 sl10    |     1000 | magenta  |     40 | inch    |     101.6
 sl5     |        4 | brown    |      1 | m       |       100
 sl6     |       20 | brown    |    0.9 | m       |        90
(9 rows)

对视图执行DELETE操作,带有子查询限定,总共使用了 4 个嵌套/联接视图,其中一个本身具有包含视图的子查询限定,并且使用了计算出的视图列,被重写为一个单独的查询树,该查询树从真实表中删除请求的数据。

在现实世界中,可能只有很少的情况下需要这种结构。但它让你感觉很舒服,因为它确实有效。

提交更正

如果您在文档中发现任何不正确的内容,与您对特定功能的体验不符或需要进一步澄清,请使用此表格报告文档问题。