受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

39.4. INSERTUPDATEDELETE规则 #

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

  • 它们可以没有动作。

  • 它们可以有多个动作。

  • 它们可以是INSTEADALSO(缺省)。

  • 伪关系NEWOLD可以派上用场。

  • 它们可以有规则条件。

第二,它们不是就地修改查询树,而是创建零棵或多棵新查询树,并且可能丢弃原始查询树。

Caution

在很多情况下,使用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),或者被替换为一个空值(对于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';

此时存在一条ON UPDATE规则log_shoelace,它带有如下规则条件表达式:

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子句会被简单忽略。

请注意,在规则的RETURNING子句中,OLDNEW指的是作为额外范围表项加入重写后查询中的伪关系,而不是结果关系里的旧行或新行。因此,举例来说,在一条支持该视图上UPDATE查询的规则中,如果RETURNING子句包含old.sl_name,那么返回的总会是旧名称,而不管视图上的查询在其RETURNING子句中指定的是OLD还是NEW;这可能会让人困惑。为了避免这种混淆,并支持在视图上的查询中返回旧值和新值,规则定义里的RETURNING子句应当引用结果关系中的项,例如shoelace_data.sl_name,而不要显式指定OLDNEW

现在假设商店偶尔会收到一大包鞋带,并附带一份很长的零件清单。但你并不想每次都手工更新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,其子查询条件总共使用了四个嵌套/连接的视图,其中一个视图自身又带有一个包含视图的子查询条件,并且还用到了计算得到的视图列,最终仍会被重写成单棵查询树,从真正的表中删除所请求的数据。

在现实世界中,大概只有很少的场景会需要这样的构造。但知道它确实能工作,总归让人安心。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。