↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 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 / 7.3 / 7.2 / 7.1
历史版本PostgreSQL 7.1 已于 2006 年 4 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本。

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 规则中只能有一个动作,而且它必须是一个无条件、带有 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),      -- primary key
    sh_avail   integer,       -- available # of pairs
    slcolor    char(10),      -- preferred shoelace color
    slminlen   float,         -- miminum shoelace length
    slmaxlen   float,         -- maximum shoelace length
    slunit     char(8)        -- length unit
);

CREATE TABLE shoelace_data (
    sl_name    char(10),      -- primary key
    sl_avail   integer,       -- available # of pairs
    sl_color   char(10),      -- shoelace color
    sl_len     float,         -- shoelace length
    sl_unit    char(8)        -- length unit
);

CREATE TABLE unit (
    un_name    char(8),       -- the primary key
    un_fact    float          -- factor to transform to cm
);

我想我们大多数人都穿鞋,都能看出这确实是很有用的数据。当然,世上有一些不需要鞋带的鞋,但这并不能让 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。注意,规则条件与查询条件不是一回事!这里我们的规则动作带有一个查询条件。

规则动作本身是一棵查询树,它是视图创建命令中 SELECT 语句的一个副本。

注意

你在 pg_rewrite 项中看到的、用于 NEW 和 OLD 的两个额外范围表项(在打印出来的查询树中,它们出于历史原因被命名为 *NEW* 和 *CURRENT*),与 SELECT 规则无关。

现在我们填充 unit、shoe_data 和 shoelace_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;

然后它会被交给规则系统。规则系统遍历范围表,检查 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);

注意,解析器把计算和限定条件都改写成了对相应函数的调用。但这实际上并没有改变任何东西。

为了展开视图,重写器只需创建一个子查询范围表条目,其中包含规则的动作解析树,然后用这个范围表条目替换原先引用视图的条目。所得的重写后解析树几乎等同于 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 Brothers)来到他的店里,想买一些新鞋;而且正如布鲁斯兄弟的本色那样,他们要穿一模一样的鞋。他们还想立刻就穿上,所以还需要鞋带。

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 是棕色的,而需要棕色鞋带的鞋是布鲁斯兄弟绝不会穿的鞋)。

这一次解析器的输出是如下解析树:

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);

类似地,shoe 和 shoelace 的规则会被替换进子查询的范围表中, 最终得到一棵三层查询树:

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 把视图规则设置成一个从另一个选择、 而另一个又从这个选择的形式。如果使用 CREATE VIEW, 这种情况绝不会发生,因为在执行第一个 CREATE VIEW 时, 第二个关系尚不存在,因此第一个视图无法从第二个视图选择。

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;

几乎是一样的。

  • 范围表中包含表 t1 和 t2 的项。

  • 目标列表中都包含一个变量,该变量指向表 t2 的范围表项中的属性 b。

  • 条件表达式会比较两个范围中的属性 a 是否相等。

  • 连接树都表示 t1 与 t2 之间的一次简单连接。

结果是,这两个解析树都会产生相似的执行计划。它们都是对这两个表的连接。对于 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。 于是旧行被隐藏起来,事务提交后,清理器(vacuum)最终就可以 真正移除它。

知道了这些以后,我们就可以用完全相同的方式把视图规则应用到任何命令上。没有区别。

17.2.4. Postgres 中视图的能力

上文演示了规则系统如何把视图定义整合进原始解析树。在第二个示例中,从一个视图发出的简单 SELECT 最终生成了一棵四表连接的解析树(unit 以不同名称使用了两次)。

17.2.4.1. 好处

用规则系统实现视图的好处在于:规划器能够在单棵解析树中同时看到哪些表必须被扫描、这些表之间的关系、来自视图的限制条件,以及原始查询自身的条件。即使原始查询本身已经是对若干视图的连接,情况也仍然如此。现在,规划器必须决定执行查询的最佳路径。它掌握的信息越多,这个决定就能做得越好。Postgres实现的规则系统能够保证,到此为止,关于该查询的全部可用信息都已经集中在这里。

17.2.5. 那么更新视图会怎么样?

如果把一个视图指定为 INSERT、 UPDATE 或 DELETE 的目标 关系,会发生什么?在完成上述替换之后,我们得到的查询树中, 结果关系会指向一个子查询范围表项。这是行不通的,因此重写器 一旦发现自己产生了这样的结果,就会抛出一个错误。

要改变这种情况,我们可以定义规则来修改非 SELECT 查询的行为。这是下一节的主题。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。