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

39.2. 视图和规则系统 #

PostgreSQL中的视图是通过规则系统实现的。事实上,下面这个定义:

CREATE VIEW myview AS SELECT * FROM mytab;

与下面这两条命令相比本质上没有区别:

CREATE TABLE myview (same column list as mytab);
CREATE RULE "_RETURN" AS ON SELECT TO myview DO INSTEAD
    SELECT * FROM mytab;

因为这正是 CREATE VIEW 命令在内部所做的事情。这会带来一些副作用。其中之一是,PostgreSQL 系统目录中关于视图的信息与表的信息完全相同。所以对于解析器而言,表和视图之间完全没有区别。它们是同一事物:关系。

39.2.1. SELECT 规则如何工作 #

ON SELECT规则会在所有查询上作为最后一步应用,即使给出的命令是INSERTUPDATEDELETE也一样。它们与其他命令类型上的规则在语义上不同,因为它们是就地修改查询树,而不是创建新的查询树。因此我们先讨论SELECT规则。

目前,一个ON SELECT规则中只能有一个动作,而且它必须是一个无条件、带有INSTEADSELECT动作。之所以有这个限制,是为了让规则足够安全,从而能够向普通用户开放;它也把ON SELECT规则限制为像视图那样工作。

本章的示例是两个进行一些计算的连接视图,以及另外一些依次使用它们的视图。最初两个视图中的一个会在后面通过为INSERTUPDATEDELETE操作添加规则来定制,从而最终得到一个在行为上像真正的表、但又带有某些特殊功能的视图。作为入门示例,这并不算简单,因此会让理解变得更难一些。但与其使用许多可能令人混淆的不同示例,不如用一个示例逐步覆盖这里讨论的全部要点。

在前两节对规则系统的描述中,我们需要用到如下真实表:

CREATE TABLE shoe_data (
    shoename   text,          -- primary key
    sh_avail   integer,       -- available number of pairs
    slcolor    text,          -- preferred shoelace color
    slminlen   real,          -- minimum shoelace length
    slmaxlen   real,          -- maximum shoelace length
    slunit     text           -- length unit
);

CREATE TABLE shoelace_data (
    sl_name    text,          -- primary key
    sl_avail   integer,       -- available number of pairs
    sl_color   text,          -- shoelace color
    sl_len     real,          -- shoelace length
    sl_unit    text           -- length unit
);

CREATE TABLE unit (
    un_name    text,          -- primary key
    un_fact    real           -- factor to transform to cm
);

如你所见,它们表示的是鞋店数据。

这些视图是这样创建的:

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,
           least(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语句的一个副本。

Note

你在pg_rewrite项中看到的、用于NEWOLD的两个额外范围表项,与SELECT规则无关。

现在我们填充unitshoe_datashoelace_data,并在一个视图上运行简单查询:

INSERT INTO unit VALUES ('cm', 1.0);
INSERT INTO unit VALUES ('m', 100.0);
INSERT INTO unit VALUES ('inch', 2.54);

INSERT INTO shoe_data VALUES ('sh1', 2, 'black', 70.0, 90.0, 'cm');
INSERT INTO shoe_data VALUES ('sh2', 0, 'black', 30.0, 40.0, 'inch');
INSERT INTO shoe_data VALUES ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
INSERT INTO shoe_data VALUES ('sh4', 3, 'brown', 40.0, 50.0, 'inch');

INSERT INTO shoelace_data VALUES ('sl1', 5, 'black', 80.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl2', 6, 'black', 100.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl3', 0, 'black', 35.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl4', 8, 'black', 40.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl5', 4, 'brown', 1.0 , 'm');
INSERT INTO shoelace_data VALUES ('sl6', 0, 'brown', 0.9 , 'm');
INSERT INTO shoelace_data VALUES ('sl7', 7, 'brown', 60 , 'cm');
INSERT INTO shoelace_data VALUES ('sl8', 1, 'brown', 40 , 'inch');

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)

这是你能在这些视图上执行的最简单的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;

然后它会被交给规则系统。规则系统遍历范围表,检查其中的关系是否有相应规则。在处理shoelace的范围表项时(到目前为止只有这一个),它会找到带有如下查询树的_RETURN规则:

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 old, shoelace new,
       shoelace_data s, unit u
 WHERE s.sl_unit = u.un_name;

为了展开该视图,重写器只需创建一个包含规则动作查询树的子查询范围表项,再用这个范围表项替换原先引用该视图的项。重写后的查询树几乎就像你直接写下了下面这条语句:

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 oldshoelace new。这些项并不直接参与查询,因为子查询的连接树或目标列表都没有引用它们。重写器利用它们保存原先存在于引用视图的范围表项中的访问权限检查信息。这样,即使在重写后的查询中不再直接使用该视图,执行器仍会检查用户是否具有访问该视图的适当权限。

这就是应用的第一条规则。规则系统接着会检查顶层查询中剩余的范围表项(本例中已经没有了),并递归检查新增子查询中的范围表项,看它们是否引用了视图。(但它不会展开oldnew,否则就会出现无限递归!)在这个例子中,shoelace_dataunit都没有重写规则,因此重写到此结束,上面的结果就是交给规划器的最终结果。

现在我们想写一个查询,找出商店里目前哪些鞋子有颜色和长度都匹配的鞋带,并且完全匹配的总双数大于等于二。

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)

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

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 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,
               least(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 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,
               least(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 shoe_ready.total_avail > 2;

这看起来可能效率不高,但规划器会通过上拉子查询把它折叠成单层查询树,然后像我们手工写出这些连接一样来规划它们。因此,折叠查询树属于一种优化,重写系统本身无须关心。

39.2.2. 非 SELECT 语句中的视图规则 #

上文对视图规则的说明没有涉及查询树中的两个细节:命令类型和结果关系。实际上,视图规则并不需要命令类型,但结果关系可能会影响查询重写器的工作方式,因为当结果关系是视图时,必须进行特殊处理。

SELECT的查询树与其他任何命令的查询树之间只有少数差别。显然,它们的命令类型不同;对于SELECT之外的命令,结果关系会指向结果应写入的那个范围表项。除此之外,其余部分完全相同。因此,假设有两个表t1t2,都具有列ab,那么下面两条语句的查询树:

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

UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a;

几乎是一样的。特别是:

  • 范围表中包含表t1t2的项。

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

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

  • 连接树都表示t1t2之间的一次简单连接。

结果是,这两个查询树都会产生相似的执行计划:它们都是对这两个表的连接。对于UPDATE,规划器会把t1中缺失的列补入目标列表,最终查询树会变成:

UPDATE t1 SET a = t1.a, b = t2.b FROM t2 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;

现在还要引入PostgreSQL的另一个细节。旧表行不会被覆盖,这也是ROLLBACK之所以很快的原因。在UPDATE中,新结果行会被插入表中(去掉CTID之后),而CTID所指向的旧行,其行头中的cmaxxmax项会被设置为当前命令计数器和当前事务 ID。于是旧行被隐藏起来,事务提交后,清理器最终就可以移除这条死行。

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

39.2.3. PostgreSQL 中视图的威力 #

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

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

39.2.4. 更新视图 #

如果某个视图被指定为INSERTUPDATEDELETEMERGE的目标关系,会发生什么?如果按上文描述的方式进行替换,就会得到一棵结果关系指向子查询范围表项的查询树,而这是行不通的。不过,PostgreSQL仍有几种方法能够支持“更新视图”这种表象。按用户感受到的复杂度从低到高排列,这些方法是:自动将视图替换为底层表、执行用户定义的触发器,或依据用户定义的规则重写查询。下面将分别讨论这些选项。

如果子查询从单个基础关系取数并且足够简单,重写器就可以自动用底层基础关系替换该子查询,使INSERTUPDATEDELETEMERGE以适当方式作用于基础关系。对这种足够简单的视图,称为自动可更新视图。关于哪些视图可以自动更新的详细信息,请参见CREATE VIEW

另一种办法是由视图上的用户自定义INSTEAD OF触发器来处理该操作(见CREATE TRIGGER)。在这种情况下,重写的工作方式会略有不同。对于INSERT,重写器完全不处理该视图,而是让它继续作为查询的结果关系。对于UPDATEDELETEMERGE,仍然需要展开视图查询,以产生命令打算更新、删除或合并的行。因此,视图会照常展开,但查询中还会再添加一个未展开的范围表项,用来表示该视图作为结果关系时的角色。

此时出现的问题是,如何标识视图中需要更新的行。回想一下,当结果关系是表时,目标列表中会加入一个特殊的CTID项,用来标识待更新行的物理位置。如果结果关系是视图,这就行不通了,因为视图没有CTID,它的行并没有实际物理位置。取而代之的是,对于UPDATEDELETEMERGE操作,目标列表中会加入一个特殊的wholerow项,它会展开为该视图的全部列。执行器利用这个值把行传给INSTEAD OF触发器。之后由触发器根据旧行值和新行值决定应当更新什么。

还有一种可能,是由用户定义INSTEAD规则,为视图上的INSERTUPDATEDELETE命令指定替代动作。这些规则会重写命令,通常是把它改写成更新一个或多个表而不是视图的命令。这正是Section 39.4的主题。请注意,这种方法不适用于MERGE,因为目前除SELECT规则外,MERGE不支持目标关系上的其他规则。

注意,规则总是先被处理,也就是在原始查询被规划和执行之前先完成重写。因此,如果一个视图上同时有INSTEAD OF触发器以及INSERTUPDATEDELETE规则,那么规则会先执行;根据其结果,触发器甚至可能完全不会被用到。

对简单视图上的INSERTUPDATEDELETEMERGE查询,总是最后才尝试自动重写。因此,如果某个视图定义了规则或触发器,它们会覆盖自动可更新视图的默认行为。

如果该视图既没有INSTEAD规则,也没有INSTEAD OF触发器,并且重写器又无法把查询自动改写成对底层基础关系的更新,那么就会抛出错误,因为执行器本身并不能直接更新视图。

提交更正

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