Postgresql 中文操作指南

41.2. Views and the Rule System #

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 命令,尽管它缺乏底层存储。这将在下面的 Section 41.2.4 中进一步讨论。

41.2.1. How SELECT Rules Work #

ON SELECT 规则会在最后一个步骤应用于所有查询,即使所给的命令是 INSERTUPDATEDELETE。这些规则与其他命令类型上的规则具有不同的语义,因为它们会修改查询树本身,而不是创建一个新的查询树。所以 SELECT 规则首先进行描述。

当前,ON SELECT 规则中只能有一个操作,它必须是 INSTEAD 的无条件 SELECT 操作。要使规则足够安全供普通用户使用,则需要此限制,它限制 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;

CREATE VIEW 命令(这是我们拥有的最简单的命令)用于 shoelace 视图,它将创建关系 shoelacepg_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 的重写规则,所以重写完成并且上文是提供给规划程序的最终结果。

现在,我们想要编写一个查询,找出商店中目前有哪些鞋子配有匹配的鞋带(颜色和长度),其中完全匹配的鞋带的总数大于或等于 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;

这种做法看上去似乎效率低下,但是,规划器只能通过“向上提取”子查询将此压缩成单层查询树,然后,它将计划连接,就像我们手动编写它们一样。因此,压缩查询树是对重写系统不必关心的优化。

41.2.2. View Rules in Non-SELECT Statements #

上述视图规则的描述中未提及查询树的两个细节。它们命令类型和结果关联。实际上,视图规则不需要命令类型,但结果关联可能会影响查询重写器的工作方式,因为结果关联是视图时需要特别小心。

@[1]和任何其他命令的查询树之间只有几个差异。显然,它们的命令类型不同,对于 @[2]之外的命令,结果关联指向结果应包含的范围表条目。其他所有内容完全相同。因此,有两个表 @[3]和 @[4],具有列 @[5]和 @[6],则两个语句的查询树:

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

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

几乎相同。特别是:

结果是,两个查询树都产生相似的执行计划:它们都是关于两个表的连接。对于 @[7],规划器将 @[8]中缺少的列添加到目标列表中,并且最终查询树将读取为:

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;

但是,@[9]中存在一个小问题:执行连接计划的部分不关心连接结果的用途。它只是产生行结果集。执行器高层处理一个为 @[10]命令,另一个为 @[11]的事实,在那里知道这是一个 @[12],并知道此结果应包含在表 @[13]中。但是,那里存在哪一行必须替换为新行呢?

为了解决此问题,在 @[14](和 @[15])语句中,将另一个条目添加到目标列表:当前元组 ID (CTID)。这是一个系统列,其中包含行所在的文件块号和块中的位置。在了解该表后,可以使用 CTID 来检索要更新的 @[16]的原始行。将 CTID 添加到目标列表后,查询实际上看起来像:

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

现在,PostgreSQL 的另一个详细内容进入阶段。旧表行不会被覆盖,这就是 @[17]快速的原因。在 @[18]中,新的结果行插入表中(在删除 CTID 之后),并且在旧行(CTID 指向该行)的行头中,@[19]和 @[20]条目设置为当前命令计数器和当前事务 ID。因此,旧行被隐藏,并且在事务提交后,真空吸尘器最终可以删除死行。

了解所有这些信息后,我们可以简单地以完全相同的方式将视图规则应用于任何命令。没有区别。

41.2.3. The Power of Views in PostgreSQL #

上述内容演示了规则系统如何将视图定义合并到原始查询树中。在第二个示例中,一个视图中的简单 @[21]创建了一个最终查询树,该查询树是 4 个表的连接(@[22]两次使用不同的名称)。

使用规则系统实现视图的好处在于,规划器具有所有有关哪些表必须被扫描、这些表之间的关系以及视图的限制条件加上原始查询中条件在一个查询树中的信息。并且当原始查询已经是关于视图的连接时仍然是这种情况。规划器必须决定执行查询的最佳路径,并且规划器信息越多,此决策就越好。而且,PostgreSQL 中实现的规则系统可确保这是该查询到目前为止的所有可用信息。

41.2.4. Updating a View #

如果视图被命名为 @[23]、@[24]或 @[25]的目标关联,该怎么办?执行上述替换将产生一个查询树,其中结果关联指向子查询范围表条目,而这将不起作用。不过,PostgreSQL 有很多种支持更新视图的方法。根据用户体验的复杂程度,这些方法包括:自动将视图中的基础表替代为,执行用户定义的触发器,或根据用户定义的规则重写查询。这些选项将在下面进行讨论。

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

或者,可以通过视图上的用户提供 INSTEAD OF 触发器处理此操作(请参见 CREATE TRIGGER )。在此情况下,重写方式略有不同。对于 INSERT ,重写器完全不处理视图,将其作为查询的结果关系。对于 UPDATEDELETE ,仍然需要扩展视图查询以生成该命令尝试更新或删除的“旧”行。因此会正常扩展视图,但会将另一个未扩展的范围表条目添加到查询中以表示该视图作为结果关系时的容量。

现在出现的问题是如何标识要视图中更新的行。回想一下,当结果关联是表时,会将一个特殊 CTID 条目添加到目标列表中以标识要更新的行的物理位置。如果结果关联是视图,则此方法不起作用,因为视图没有 CTID,因为它的行没有实际物理位置。相反,对于 @[34]或 @[35]操作,将一个特殊的 @[36]条目添加到目标列表中,该条目将扩展为包括视图中的所有列。执行器使用此值将“旧”行提供给 @[37]触发器。触发器必须根据旧行和新行值得出要更新的内容。

另外一种可能性是用户定义 @[38]规则,为视图上的 @[39]、@[40]和 @[41]命令指定替代动作。这些规则将重写命令,通常将其重写为一个更新一个或多个表而不是视图的命令。这是 @[42]的主题。

请注意,规则先经过评估,在计划和执行原始查询之前对其进行重写。因此,如果一个视图在 @[44]、@[45]或 @[46]上具有 @[43]触发器以及规则,则将首先评估规则,并且根据结果,可能根本不会使用触发器。

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

如果视图没有 INSTEAD 规则或 INSTEAD OF 触发器,而重写程序无法自动将查询重写为对基础基础关系的更新,那么将会引发错误,因为执行程序无法以这种方式更新视图。