17.2. 视图和规则系统

17.2.1. Postgres里视图的实现

Postgres 里的视图是通过规则系统来实现的. 实际上下面的命令

    CREATE VIEW myview AS SELECT * FROM mytab;
和下面两条命令
    CREATE TABLE myview (same attribute list as for mytab);
    CREATE RULE "_RETmyview" AS ON SELECT TO myview DO INSTEAD
        SELECT * FROM mytab;
之间绝对没有区别,因为这就是 CREATE VIEW 命令在内部实际执行的内容. 这样做有一些负作用.其中 之一就是在 Postgres 系统表里的视图的信息与一般表的信息完全一样. 所以对于查询分析器来说,表和视图之间完全没有区别. 它们是同样的事物--关系.这就是目前很重要的一点.

17.2.2. SELECT 规则如何运转

ON SELECT 的规则在最后一步应用于所有查询, 哪怕给出的命令是一条 INSERT,UPDATE 或 DELETE. 而且与其他(规则)有不同的语意, 那就是它们在现场修改分析树而不是创建一个新的(分析树). 所以我们先介绍 SELECT 的规则.

目前,一个 ON SELECT 规则里只能有一个动作(action), 而且它必须是一个无条件的 INSTEAD (取代)的 SELECT 动作. 有这个限制是为了令规则安全到普通用户也可以打开它们, 并且它对真正的视图规则做 ON SELECT 规则限制.

本文档的例子是两个连接视图, 它们做一些运算并且因此会涉及到更多视图的使用. 这两个视图之一稍后将利用对 INSERT, UPDATE 和 DELETE 操作附加规则的方法客户化, 这样做最终的结果就会是这个视图表现得象一个具有一些特殊功能的真正的表. 这个例子可不是适合于开始的简单易懂的例子, 从这个例子开始讲可能会让我们的讲解变得有些难以理解. 但是我们认为用一个覆盖所有关键点的例子来一步一步讨论要比 举很多例子搞乱思维好多了.

在本例子中用到的数据库名是 al_bundy.你很快就会明白为什么叫这个名字. 而且这个例子需要安装过程语言 PL/pgSQL , 因为我们需要一个小巧的 min() 函数用于返回两个整数值中的小的那个. 我们用下面方法创建它

    CREATE FUNCTION min(integer, integer) RETURNS integer AS
        'BEGIN
            IF $1 < $2 THEN
                RETURN $1;
            END IF;
            RETURN $2;
        END;'
    LANGUAGE 'plpgsql';

我们头两个规则系统要用到的真实的表的描述如下:

    CREATE TABLE shoe_data (
        shoename   char(10),      -- 主键
        sh_avail   integer,       -- (鞋的)可用对数
        slcolor    char(10),      -- 较好的鞋带颜色
        slminlen   float,         -- 鞋带最短长度
        slmaxlen   float,         -- 鞋带最长长度
        slunit     char(8)        -- 长度单位
    );

    CREATE TABLE shoelace_data (
        sl_name    char(10),      -- 主键
        sl_avail   integer,       -- (鞋带的)可用双数
        sl_color   char(10),      -- 鞋带颜色
        sl_len     float,         -- 鞋带长度
        sl_unit    char(8)        -- 长度单位
    );

    CREATE TABLE unit (
        un_name    char(8),       -- 主键
        un_fact    float          -- 转换成厘米的系数
    );
我想我们大多数人要穿鞋子,所以应该会觉得上面的数据是非常有用的. 当然,有那些不需要鞋带的鞋子,但是不会让 AL (译注:鞋店老板.) 的生活变得更轻松,所以我们忽略之.

视图创建为

    CREATE VIEW shoe AS
        SELECT sh.shoename,
               sh.sh_avail,
               sh.slcolor,
               sh.slminlen,
               sh.slminlen * un.un_fact AS slminlen_cm,
               sh.slmaxlen,
               sh.slmaxlen * un.un_fact AS slmaxlen_cm,
               sh.slunit
          FROM shoe_data sh, unit un
         WHERE sh.slunit = un.un_name;

    CREATE VIEW shoelace AS
        SELECT s.sl_name,
               s.sl_avail,
               s.sl_color,
               s.sl_len,
               s.sl_unit,
               s.sl_len * u.un_fact AS sl_len_cm
          FROM shoelace_data s, unit u
         WHERE s.sl_unit = u.un_name;

    CREATE VIEW shoe_ready AS
        SELECT rsh.shoename,
               rsh.sh_avail,
               rsl.sl_name,
               rsl.sl_avail,
               min(rsh.sh_avail, rsl.sl_avail) AS total_avail
          FROM shoe rsh, shoelace rsl
         WHERE rsl.sl_color = rsh.slcolor
           AND rsl.sl_len_cm >= rsh.slminlen_cm
           AND rsl.sl_len_cm <= rsh.slmaxlen_cm;
创建shoelace 的 CREATE VIEW 命令 (也是我们用到的最简单的一个) 将创建一个关系 shoelace 并且在pg_rewrite 表里增加一个记录,告诉系统有一个重写规则应用于所有可排列元素里引用了 shoelace 关系的查询.该规则没有规则条件(将在非 SELECT 规则讨论, 因为目前的 SELECT 规则不可能有这些东西)并且它是 INSTEAD (取代)型的. 要注意规则条件与查询条件不一样!这个规则动作(action)有一个查询条件.

规则动作(action)是一个查询树, 实际上是在创建视图的命令里的 SELECT 语句的一个拷贝.

注意: 你在表 pg_rewrite 里看到的两个额外的用于 NEW 和 OLD 的范围表记录(因历史原 因,在打印出来的查询树里叫 *NEW* 和 *CURRENT* )对 SELECT 规则不感兴趣.

现在我们填充 unitshoe_datashoelace_data,Al 生平第一次键入了 SELECT 命令:
    al_bundy=> INSERT INTO unit VALUES ('cm', 1.0);
    al_bundy=> INSERT INTO unit VALUES ('m', 100.0);
    al_bundy=> INSERT INTO unit VALUES ('inch', 2.54);
    al_bundy=> 
    al_bundy=> INSERT INTO shoe_data VALUES 
    al_bundy->     ('sh1', 2, 'black', 70.0, 90.0, 'cm');
    al_bundy=> INSERT INTO shoe_data VALUES 
    al_bundy->     ('sh2', 0, 'black', 30.0, 40.0, 'inch');
    al_bundy=> INSERT INTO shoe_data VALUES 
    al_bundy->     ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
    al_bundy=> INSERT INTO shoe_data VALUES 
    al_bundy->     ('sh4', 3, 'brown', 40.0, 50.0, 'inch');
    al_bundy=> 
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl1', 5, 'black', 80.0, 'cm');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl2', 6, 'black', 100.0, 'cm');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl3', 0, 'black', 35.0 , 'inch');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl4', 8, 'black', 40.0 , 'inch');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl5', 4, 'brown', 1.0 , 'm');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl6', 0, 'brown', 0.9 , 'm');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl7', 7, 'brown', 60 , 'cm');
    al_bundy=> INSERT INTO shoelace_data VALUES 
    al_bundy->     ('sl8', 1, 'brown', 40 , 'inch');
    al_bundy=> 
    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       |       7|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)
这是 Al 可以在我们的视图上做的最简单的 SELECT , 所以我们我们把它作为我们解释基本视图规则的命令. 'SELECT * FROM shoelace' 被分析器解释成下面的分析树
    SELECT shoelace.sl_name, shoelace.sl_avail,
           shoelace.sl_color, shoelace.sl_len,
           shoelace.sl_unit, shoelace.sl_len_cm
      FROM shoelace shoelace;
然后把这些交给规则系统.规则系统把可排列元素(rangetable)过滤一遍, 检查一下在 pg_rewrite 表里面有没有适用该关系的任何规则.当为 shoelace 记录处理可排列元素时 (到目前为止唯一的一个),它会发现分析树里有规则 '_RETshoelace'
    SELECT s.sl_name, s.sl_avail,
           s.sl_color, s.sl_len, s.sl_unit,
           float8mul(s.sl_len, u.un_fact) AS sl_len_cm
      FROM shoelace *OLD*, shoelace *NEW*,
           shoelace_data s, unit u
     WHERE bpchareq(s.sl_unit, u.un_name);
注意分析器已经把(SQL里的)计算和条件换成了相应的函数调用. 但实际上这没有改变什么.

为扩展该视图,重写器简单地创建一个子查询可排列元素记录, 它包含规则动作的分析树,然后用这个可排列元素记录取代原先引用视图的 那个.生成的重写分析树几乎与 Al 键入的一样

    SELECT shoelace.sl_name, shoelace.sl_avail,
           shoelace.sl_color, shoelace.sl_len,
           shoelace.sl_unit, shoelace.sl_len_cm
      FROM (SELECT s.sl_name,
                   s.sl_avail,
                   s.sl_color,
                   s.sl_len,
                   s.sl_unit,
                   s.sl_len * u.un_fact AS sl_len_cm
              FROM shoelace_data s, unit u
             WHERE s.sl_unit = u.un_name) shoelace;
不过还是有一个区别:子查询可排列元素有两个额外的记录 shoelace *OLD*,shoelace *NEW*.这些记录并不直接参与查询, 因为它们没有被子查询的连接树或者目标列表引用. 重写器用它们存储最初出现在引用视图的可排列元素里面的 访问权限检查.这样,执行器仍然会检查该用户是否有访问视图的合适权限, 即使在重写查询里面没有对视图的直接使用也如此.

这是应用的第一个规则.规则系统继续检查顶层查询里剩下的可排列元素记录 (本例中没有了),并且它在加进来的子查询中递归地检查可排列元素记录, 看看其中有没有引用视图的.(不过这样不会扩展 *OLD* 或 *NEW* --- 否则我们会无穷递归下去!) 在这个例子中,没有用于 shoelace_data 或 unit 的重写规则, 所以重写结束并且上面的就是给规划器的最终结果.

现在我们让 Al 面对这样一个问题:Blues 兄弟到了他的鞋店想买一双新鞋, 而且 Blues 兄弟想买一样的鞋子.并且要立即就穿上,所以他们还需要鞋带.

Al 需要知道鞋店里目前那种鞋有合适的鞋带(颜色和尺寸), 而且匹配的双数大于或等于两双.我们告诉他如何做,于是他问他的数据库:

    al_bundy=> SELECT * FROM shoe_ready WHERE total_avail >= 2;
    shoename  |sh_avail|sl_name   |sl_avail|total_avail
    ----------+--------+----------+--------+-----------
    sh1       |       2|sl1       |       5|          2
    sh3       |       4|sl7       |       7|          4
    (2 rows)
Al 是鞋的专家,知道只有 sh1 的类型会适用 (sl7鞋带是棕色的,而与棕色的鞋带匹配的鞋子是 Blues 兄弟从来不穿的).

这回分析器的输出是分析树

    SELECT shoe_ready.shoename, shoe_ready.sh_avail,
           shoe_ready.sl_name, shoe_ready.sl_avail,
           shoe_ready.total_avail
      FROM shoe_ready shoe_ready
     WHERE int4ge(shoe_ready.total_avail, 2);
应用的第一个规则将是用于 shoe_ready 视图的,结果是生成分析树
    SELECT shoe_ready.shoename, shoe_ready.sh_avail,
           shoe_ready.sl_name, shoe_ready.sl_avail,
           shoe_ready.total_avail
      FROM (SELECT rsh.shoename,
                   rsh.sh_avail,
                   rsl.sl_name,
                   rsl.sl_avail,
                   min(rsh.sh_avail, rsl.sl_avail) AS total_avail
              FROM shoe rsh, shoelace rsl
             WHERE rsl.sl_color = rsh.slcolor
               AND rsl.sl_len_cm >= rsh.slminlen_cm
               AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
     WHERE int4ge(shoe_ready.total_avail, 2);
与上面类似,用于 shoeshoelace 的规则替换到子查询可排列元素里, 生成一个最终的三层查询树:
    SELECT shoe_ready.shoename, shoe_ready.sh_avail,
           shoe_ready.sl_name, shoe_ready.sl_avail,
           shoe_ready.total_avail
      FROM (SELECT rsh.shoename,
                   rsh.sh_avail,
                   rsl.sl_name,
                   rsl.sl_avail,
                   min(rsh.sh_avail, rsl.sl_avail) AS total_avail
              FROM (SELECT sh.shoename,
                           sh.sh_avail,
                           sh.slcolor,
                           sh.slminlen,
                           sh.slminlen * un.un_fact AS slminlen_cm,
                           sh.slmaxlen,
                           sh.slmaxlen * un.un_fact AS slmaxlen_cm,
                           sh.slunit
                      FROM shoe_data sh, unit un
                     WHERE sh.slunit = un.un_name) rsh,
                   (SELECT s.sl_name,
                           s.sl_avail,
                           s.sl_color,
                           s.sl_len,
                           s.sl_unit,
                           s.sl_len * u.un_fact AS sl_len_cm
                      FROM shoelace_data s, unit u
                     WHERE s.sl_unit = u.un_name) rsl
             WHERE rsl.sl_color = rsh.slcolor
               AND rsl.sl_len_cm >= rsh.slminlen_cm
               AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
     WHERE int4ge(shoe_ready.total_avail, 2);
最后规划器会把这个树压缩成一个两层查询树: 最下层的 select 将"拖到"中间的 select 中, 因为没有必要分别处理它们.但是中间的 select 仍然和 顶层的分开,因为它包含聚集函数.如果我们把它们也拉进来, 那它就会修改最顶层的 select 的行为,那可不是我们想要的. 不过,压缩查询树是重写系统自己不需要关心的优化操作.

注意: 目前规则系统中没有用于视图规则递归终止机制(只有用于其他规则的). 这一点不会造成太大的损害, 因为把这个(规则)无限循环(把后端摧毁,直到耗尽内存) 的唯一方法是创建表然后后手工用 CREATE RULE 命令创建视图规则, 这个规则是这样的:一个从其他地方来的选择(select) 选择(select)了这个视图. 如果使用了 CREATE VIEW ,这一点是永远不会发生的, 因为第二个关系不存在,所以第一个视图不能从第二个视图里面选择(select).

17.2.3. 非 SELECT 语句的视图规则

有两个分析树的细节我们在上面的视图规则中没有涉及到. 就是命令类型和结果关系.实际上,视图规则不需要这些信息.

一个 SELECT 的分析树和用于其他命令的分析树只有少数几个区别. 显然它们有另一个命令类型并且这回结果 关系指向生成的结果前往的可排列元素入口.任何其它东西都完全是一样的. 所以如果有两个表 t1 和 t2 分别有字段 a 和 b, 下面两个语句的分析树

    SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a;

    UPDATE t1 SET b = t2.b WHERE t1.a = t2.a;
几乎是一样的.

结果是,两个分析树生成相似的执行规划.它们都是两个表的连接. 对于 UPDATE 语句来说, 规划器把 t1 缺失的字段追加到目标列因而最终分析树看起来象
    UPDATE t1 SET a = t1.a, b = t2.b WHERE t1.a = t2.a;
因此执行器在连接上运行的结果和下面语句
    SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a;
是完全一样的.但是在 UPDATE 里有点问题. 执行器不关心它正在处理的从连接出来的结果的含义是什么. 它只是产生一个行的结果集. 一个是 SELECT 命令而另一个是 UPDATE 命令的区别是由执行器的调用者控制的. 该调用者这时还知道(查看分析树)这是一个 UPDATE, 而且它还知道结果要记录到表 t1 里去. 但是现有的记录中的哪一行要被新行取代呢?

要解决这个问题, 在 UPDATE 和 DELETE 语句的目标列表里面增加了另外一个入口. 当前的元组 ID(ctid). 这是一个有着特殊特性的系统字段. 它包含行在(存储)块中的(存储)块数和位置信息. 在已知表的情况下,可以通过 ctid 检索最初的需要更新的 t1 行. 在把 ctid 加到目标列表中去以后,查询看上去实际上象这样:

    SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
现在,另一个 Postgres 的细节进入到这个阶段里了. 这时,表中的行还没有被覆盖,这就是为什么 ABORT TRANSACTION 飞快的原因. 在一个 UPDATE里,新的结果行插入到表里(在通过 ctid 查找之后)并且 把 ctid 指向的 cmax 和 xmax 记录行的记录头设置为当前命令计数器和当前事务ID. 这样旧的行就被隐藏起来并且在事务提交之后 vacumm cleaner(清理器)就可以真正把它们删除掉.

知道了这些,我们就可以简单的把视图的规则应用到任意命令中. 它们(视图和命令)没有区别.

17.2.4. Postgres里视图的强大能力

上面演示了规则系统如何融合到视图定义的初始分析树中去. 在第二个例子里,一个简单的对视图的 SELECT 创建了一个 4 个表联合的分析树(unit 以不同的名称用了两次).

17.2.4.1. 益处

在规则系统里实现视图的好处是,优化器在一个分析树里拥有所有信息: 应该扫描哪个表 + 表之间的关系+ 视图的资格限制 + 初始查询的资格(条件). 并且仍然是在最初的查询已经是一个视图的联合的情况下. 现在优化器必须决定执行查询的最优路径. 优化器拥有越多信息,它的决策就越好.并且 Postgres 里的规则系统的实现保证这些信息是目前能获得的有关该查询的所有信息.

17.2.5. 更新视图会发生什么?

如果视图是 INSERT,UPDATE,或者 DELETE 的目标关系会怎样? 在完成我们上面描述的替换之后, 我们就有一个这样的查询树:结果关系指向一个是子查询的可排列元素记录. 这样可不能运行,所以如果重写器看到自己生成这么一个东西, 它就抛出一个错误.

要修改这个特性,我们可以定义修改非 SELECT 查询的规则. 这是下一节的主题.