17.3. INSERT,UPDATE 和 DELETE 的规则

17.3.1. 与 View Rules 的区别

定义在 ON INSERT,UPDATE 和 DELETE 的规则与前一章描述的视图规则完全不同. 首先,他们的 CREATE RULE 命令允许更多:

第二,它们不是就地修改分析树. 它们是创建零个或多个新分析树并且可能把原始的那个仍掉.

17.3.2. 这些规则是如何运转的

把下面语法

    CREATE RULE rule_name AS ON event
        TO object [WHERE rule_qualification]
        DO [INSTEAD] [action | (actions) | NOTHING];
牢牢记住.在随后的内容里, "update rules"(更新规则) 意思是定义在 ON INSERT,UPDATE 或 DELETE 上的规则.

如果分析树的结果关系和命令类型与 CREATE RULE 命令里给出的对象和事件一样的话, 规则系统就把更新规则 (update rules)应用上去. 对于更新规则(update rules),规则系统创建一个分析树列表. 一开始分析树是空的.这里可以有零个(NOTHING 关键字),一个或多个动作. 一个或多个动作.为简单起见, 我们先看一眼一个只有一个动作(action)的规则. 这个规则可以有一个条件或没有并且它可以是 INSTEAD 或反之.

何为规则条件?它是一个限制条件,告诉规则动作(action)什么时候要做, 什么时候不用做.这个资格(条件)可以只引用 NEW 和/或 OLD 伪关系 --它们是作为对象给出的基本关系(但是有着特殊含义).

所以,对这个一个动作(action)的规则生成分析树,有下面四种情况.

最后,如果规则不是 INSTEAD,最初的未修改的分析树被加入到列表. 因为只有合格的INSTEAD规则已经在初始的分析树里面, 所以对于单动作(action)规则我们最终得到最多有两个分析树。

从规则动作生成的分析树被再次送到重写系统并且可能附加更多的规则, 结果是更多的或更少的分析树.所以规则动作里 的分析树必须是另一个命令类型或另一个结果关系. 否则这样的递归过程就会没完没了. 现在有一个10层的编译级的递归限制. 如果10次递归之后还有需要应用的更新规则(update rules), 规则系统就认为是一个多规则的循环而退出事务.

pg_rewrite 系统表里的分析树的动作(action)只是模板. 因为他们可以引用 NEW 和 OLD 的可排列元素, 在使用 它们之前必须做一些调整.对于任何对 NEW 的引用, 都要先在初始查询的目标列中搜索对应的条目.如果找到, 把该条目表达式放到引用里.否则 NEW 和 OLD 的含义一样 (UPDATE 时)或者被 NULL 替代(INSERT 时). 任何用于 OLD 的引用都用结果关系的可排列元素的引用替换.

在我们完成更新规则的附加之后,我们附加视图规则到生成的分析树上. 视图无法插入新的更新动作,所以我们没有必要向视图重写的输出 附加更新规则.

17.3.2.1. 循序渐进的第一个规则

我们希望跟踪 shoelace_data 关系中的 sl_avail 字段. 所以我们设置一个日志表和一条规则,这条规则每次在用UPDATE 更新 shoelace_data 表时都要往数据库里写一条记录.

    CREATE TABLE shoelace_log (
        sl_name    char(10),      -- 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
                                    );

现在 Al 键入

    al_bundy=> UPDATE shoelace_data SET sl_avail = 6                       
    al_bundy->     WHERE sl_name = 'sl7';
然后我们看看日志表.
    al_bundy=> 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 bpchareq(shoelace_data.sl_name, 'sl7');
这里是一个带有资格表达式的ON UPDATE 规则'log_shoelace'
    int4ne(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 子句只是表示这里有用于 *NEW* 和 *OLD* 的可排列元素记录. 这些东西的存在是因为这样一来它们就可以被 INSERT 命令的查询树里的变量引用.

该规则是一个有资格(条件)的非 INSTEAD 规则, 所以规则系统必须返回两个分析树: 更改过的规则动作(action)和原始分析树. 在第一步里,原始查询的可排列元素集成到规则动作(action)分析树里.生成

    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;
第二步把规则资格(条件)增加进去,所以结果集限制为 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 int4ne(*NEW*.sl_avail, *OLD*.sl_avail);
这个东西看起来更奇怪,因为 INSERT ... VALUES 也没有 WHERE 子句, 不过规划器和执行器对此并不在意.它们毕竟还要为 INSERT ... SELECT 支持这种功能. 第三步把原始分析树的资格(条件)加进去, 把结果集进一步限制成只有被初始分析树改变的行.
    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 int4ne(*NEW*.sl_avail, *OLD*.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
第四步把 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 int4ne(6, *OLD*.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
第五步用结果关系引用把 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 int4ne(6, shoelace_data.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
这就成了.因为规则不是 INSTEAD,我们还输出原始分析树. 简而言之,从规则系统输出的是一个两个分析树的列表, 与下面语句相同:
    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';
并且资格(条件)将永远不可能是真值. 如果最初的查询修改多个行,它也能运行. 所以如果 Al 写出下面命令
    UPDATE shoelace_data SET sl_avail = 0
     WHERE sl_color = 'black';
实际上有四行被更新(sl1,sl2,sl3 和 sl4). 但 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';
这个分析树将肯定插入三个新的日志记录.这也是完全正确的.

重要的是原始分析树最后执行. Postgres 的"交警" 在两个分析树的执行间做一次命令计数器增一的动作, 所以第二个(分析树)可以看到第一个(分析树)所做的改变. 如果 UPDATE 将先被执行,所有的行都已经设为零,所以记日志的 INSERT 将不能找到任何行是符合 0 != shoelace_data.sl_avail 条件的.

17.3.3. 与视图共存

一个保护视图关系, 使其避免我们曾提到的有人可以在其中 INSERT,UPDATE 和 DELETE 不可见的数据的 简单方法是让那些分析树被丢弃.我们创建下面规则

    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;
如果 Al 现在试图对视图关系 shoe做上面的任何操作, 规则系统将应用这些规则.因为这些规则没有动作而且是INSTEAD, 结果是生成的分析树将是空的并且整个查询将变得空空如也, 因为经过规则系统处理后没有什么东西剩下来用于优化或执行了.

注意: 这个方法可能会令前端困惑,因为在数据库里完全没有任何事情发生, 因而后端对查询将不返回任何信息. 在 libqp 里甚至是一个 PGRES_EMPTY_QUERY 或其他的信息也不返回.在 psql 里,什么也没发生. 这些将在以后修改.

一个更复杂的使用规则系统的方法是用规则系统创建一个重写分析树的规则, 使分析树对真实的表进行正确的操作. 要在视图 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;
现在有一包鞋带到达 Al 的商店,而且这是一大笔到货. Al 并不长于计算,所以我们不想让他手工更新鞋带视图. 取而代之的是我们创建了两个小表,一个是我们可以从到货清单中插入东西, 另一个是一个特殊的技巧.创建这些的命令如下:
    CREATE TABLE shoelace_arrive (
        arr_name    char(10),
        arr_quant   integer
    );

    CREATE TABLE shoelace_ok (
        ok_name     char(10),
        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;
现在 Al 可以坐下来做这些事情直到(下面查询的输出)
    al_bundy=> SELECT * FROM shoelace_arrive;
    arr_name  |arr_quant
    ----------+---------
    sl3       |       10
    sl6       |       20
    sl8       |       20
    (3 rows)
就是那些到货列表中的东西.让我们迅速地看一眼当前的数据,
    al_bundy=> 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
    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)
把到货鞋带移到(shoelace_ok)中
    al_bundy=> INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;
然后检查结果
    al_bundy=> 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)

    al_bundy=> 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 = int4pl(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 bpchareq(shoelace.sl_name, showlace_arrive.arr_name);
并且把原始的对 shoelace_ok 的 INSERT 丢弃掉. 这样重写后的查询再次传入规则系统并且第二次应用了规则 'shoelace_upd' 生成
    UPDATE shoelace_data SET
           sl_name = shoelace.sl_name,
           sl_avail = int4pl(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 showlace_data
     WHERE bpchareq(shoelace.sl_name, showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, shoelace.sl_name);
同样这是一个 INSTEAD 规则并且前一个分析树被丢弃掉. 注意这个查询仍然是使用视图 shoelace ,但是规则系统还没有完成(规则)循环, 所以它继续对(分析树)应用规则 '_RETshoelace',然后我们得到
    UPDATE shoelace_data SET
           sl_name = s.sl_name,
           sl_avail = int4pl(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 showlace_data,
           shoelace *OLD*, shoelace *NEW*,
           shoelace_data s, unit u
     WHERE bpchareq(s.sl_name, showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, s.sl_name);
同样又是应用了一个更新规则并且我们继续规则的附加, 到了重写的第三回合.这回应用规则 'log_shoelace' 生成下面分析树
    INSERT INTO shoelace_log SELECT
           s.sl_name,
           int4pl(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 showlace_data,
           shoelace *OLD*, shoelace *NEW*,
           shoelace_data s, unit u,
           shoelace_data *OLD*, shoelace_data *NEW*
           shoelace_log shoelace_log
     WHERE bpchareq(s.sl_name,  showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, s.sl_name);
       AND int4ne(int4pl(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 里也一样多做一次. 不过要想把这些不足去掉是一样太困难的活了.

Postgres 规则系统及其功能的最后一个演示. 有个金发美女出售鞋带.而且 Al 可能永远不知道的是, 她不仅漂亮,而且聪明 -有点太聪明了. 因此,时不时的会发生 Al 订购的鞋带完全不能销售的情况. 这回他定了 1000 对洋红色的鞋带, 并且因为其他类型的目前还没货所以他忘了买一些, 他还准备在他的数据库里增加一些粉红的鞋带.

    al_bundy=> INSERT INTO shoelace VALUES 
    al_bundy->     ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
    al_bundy=> INSERT INTO shoelace VALUES 
    al_bundy->     ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);
因为常发生这种事,我们必须看一眼鞋带记录表, 看看有没有那些某一时段没有相配的鞋子的(鞋带). 我们可以在每次都用一个复杂的语句实现这些, 或者我们可以创建一个用于这个方面的视图.如下
    CREATE VIEW shoelace_obsolete AS
        SELECT * FROM shoelace WHERE NOT EXISTS
            (SELECT shoename FROM shoe WHERE slcolor = sl_color);
它的输出是
    al_bundy=> SELECT * FROM shoelace_obsolete;
    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
那 1000 条洋红色鞋带,在把它们仍掉之前我们必须先欠着 Al, 不过那是另一回事.粉红(鞋带)的记录我们要删掉.为了让这事对 Postgres 有点难度,我们不直接删除它们.取而代之的是我们再创建一个视图
    CREATE VIEW shoelace_candelete AS
        SELECT * FROM shoelace_obsolete WHERE sl_avail = 0;
然后用下面方法做:
    DELETE FROM shoelace WHERE EXISTS
        (SELECT * FROM shoelace_candelete
                 WHERE sl_name = shoelace.sl_name);
所以:
    al_bundy=> 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, 这个视图带有一个总共使用了四个嵌套/连接的视图的子查询资格(条件), 这四个视图之一本身有一个拥有对一个视图的子查询资格(条件), 该条件计算使用的视图的列;最后重写成了一个分析树, 该分析树从一个真正的表里面把需要删除的数据删除.

我想在现实世界里只有很少的机会需要做上面的这类事情. 但这些东西能运转让我很开心.

真相是: 我在写本文档时做上面的试验又发现了一个臭虫. 但在去除该错误之后我有点惊奇的发现这些都正常工作了.