2024年9月26日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 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

39.2. 视图和规则系统 #

PostgreSQL 中,视图是使用规则系统实现的。视图本质上是一个空表(没有实际存储空间),包含一个 ON SELECT DO INSTEAD 规则。按照惯例,该规则命名为 _RETURN。因此,像这样的视图

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;

尽管您不能实际编写该代码,因为不允许表具有 ON SELECT 规则。

视图也可以包含其他类型的 DO INSTEAD 规则,允许在视图上执行 INSERTUPDATEDELETE 命令,尽管它没有底层存储。这将在下面的 第 39.2.4 节 中进一步讨论。

39.2.1. SELECT 规则的工作原理 #

ON SELECT 规则应用于所有查询作为最后一步,即使给定的命令是 INSERTUPDATEDELETE。它们与其他命令类型上的规则具有不同的语义,因为它们会就地修改查询树,而不是创建一个新的查询树。因此,首先描述 ON SELECT 规则。

目前,ON SELECT 规则中只能有一个操作,并且它必须是无条件的 SELECT 操作,并且是 INSTEAD。此限制是为了使规则足够安全,以便为普通用户开放,并且它将 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 语句的副本。

注意

您在 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 没有重写规则,因此重写完成,上述内容是提供给计划程序的最终结果。

现在,我们想编写一个查询,找出当前在商店中有哪些鞋子,我们有与之匹配的鞋带(颜色和长度),以及完全匹配的对数是否大于或等于 2。

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 操作创建了一个最终的查询树,该树是 4 个表的联接(unit 使用了不同的名称两次)。

使用规则系统实现视图的好处是,计划程序拥有有关哪些表需要扫描、这些表之间的关系、视图中的限制性限定条件以及原始查询中的限定条件的所有信息,这些信息都包含在一个查询树中。即使原始查询已经是跨视图的联接,情况仍然如此。计划程序必须决定执行查询的最佳路径,计划程序拥有的信息越多,该决定就越好。在 PostgreSQL 中实现的规则系统确保了所有这些信息在该点之前都可用。

39.2.4. 更新视图 #

如果视图被命名为 INSERTUPDATEDELETEMERGE 的目标关系,会发生什么?执行上面描述的替换将生成一个查询树,其中结果关系指向一个子查询范围表条目,这将无法正常工作。但是,PostgreSQL 可以用多种方法支持更新视图的外观。按用户体验的复杂程度排序,分别是:自动将基础表替换为视图、执行用户定义的触发器或根据用户定义的规则重写查询。这些选项将在下面讨论。

如果子查询从单个基本关系中进行选择,并且足够简单,则重写器可以自动用基础基本关系替换子查询,以便 INSERTUPDATEDELETEMERGE 以适当的方式应用于基本关系。对于此目的,足够简单 的视图称为 自动可更新。有关可以自动更新的视图类型的详细信息,请参阅 CREATE VIEW.

或者,操作可以通过视图上的用户提供的 INSTEAD OF 触发器来处理(请参阅 CREATE TRIGGER)。在这种情况下,重写的工作方式略有不同。对于 INSERT,重写器对视图不做任何处理,将其保留为查询的结果关系。对于 UPDATEDELETEMERGE,仍然需要扩展视图查询以生成命令将尝试更新、删除或合并的 行。因此,视图以正常方式扩展,但另一个未扩展的范围表条目被添加到查询中,以表示其作为结果关系的容量的视图。

现在出现的问题是如何识别要更新的视图中的行。回想一下,当结果关系是一个表时,一个特殊的CTID条目被添加到目标列表中,以识别要更新的行的物理位置。如果结果关系是一个视图,这将不起作用,因为视图没有CTID,因为它的行没有实际的物理位置。相反,对于 UPDATEDELETEMERGE 操作,一个特殊的 wholerow 条目被添加到目标列表中,该条目扩展为包含视图中的所有列。执行器使用此值将 行提供给 INSTEAD OF 触发器。由触发器根据旧值和新值确定要更新的内容。

另一种可能性是用户定义 INSTEAD 规则,这些规则指定在视图上的 INSERTUPDATEDELETE 命令的替代操作。这些规则将重写命令,通常重写为更新一个或多个表的命令,而不是视图。这是 第 39.4 节 的主题。请注意,这将不适用于 MERGE,目前它不支持目标关系上的规则,除了 SELECT 规则。

请注意,规则首先被评估,在计划和执行原始查询之前对其进行重写。因此,如果视图具有 INSTEAD OF 触发器以及 INSERTUPDATEDELETE 上的规则,则规则将首先被评估,并且根据结果,触发器可能根本不被使用。

简单视图上 INSERTUPDATEDELETEMERGE 查询的自动重写总是最后尝试。因此,如果视图具有规则或触发器,它们将覆盖自动可更新视图的默认行为。

如果没有视图的 INSTEAD 规则或 INSTEAD OF 触发器,并且重写器无法自动将查询重写为对基础基本关系的更新,则会抛出错误,因为执行器无法更新视图本身。

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符或需要进一步澄清,请使用 此表格 报告文档问题。