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
规则,允许对视图执行 INSERT
、UPDATE
或 DELETE
命令,尽管它没有底层存储。这将在下文 第 41.2.4 节 中进一步讨论。
SELECT
规则的工作原理 #ON SELECT
规则应用于所有查询,作为最后一步,即使给定的命令是 INSERT
、UPDATE
或 DELETE
。并且它们与其他命令类型的规则具有不同的语义,因为它们修改查询树本身,而不是创建一个新的查询树。因此,首先描述 SELECT
规则。
目前,ON SELECT
规则中只能有一个动作,并且它必须是一个无条件的 SELECT
动作,即 INSTEAD
。此限制是让规则足够安全以便向普通用户开放所必需的,并且它限制 ON SELECT
规则的行为类似于视图。
本章的示例是两个连接视图,它们进行一些计算,以及一些使用它们的视图。前两个视图之一稍后通过添加 INSERT
、UPDATE
和 DELETE
操作的规则进行自定义,以便最终结果将是一个视图,它表现得像一个具有某些神奇功能的真实表。这不是一个简单的示例,并且这使得事情变得更难理解。但最好有一个示例逐步涵盖所有讨论的要点,而不是有许多不同的示例,这些示例可能会混淆。
我们在前两个规则系统描述中需要的真实表如下
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
条目中看到的 NEW
和 OLD
的两个额外的范围表条目对于 SELECT
规则并不重要。
现在,我们填充 unit
、shoe_data
和 shoelace_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 old
和 shoelace new
。这些条目不会直接参与查询,因为子查询的连接树或目标列表没有引用它们。重写程序使用它们来存储最初存在于引用视图的范围表条目中的访问权限检查信息。通过这种方式,执行程序仍然会检查用户是否具有访问视图的适当权限,即使在重写查询中没有直接使用视图。
这是应用的第一条规则。规则系统将继续检查顶部查询中剩余的范围表条目(在此示例中,没有更多条目),并且它将递归检查添加的子查询中的范围表条目,以查看它们是否引用视图。(但它不会扩展 old
或 new
— 否则我们将有无限递归!)在此示例中,shoelace_data
或 unit
没有重写规则,因此重写完成,上述内容是提供给规划器的最终结果。
现在,我们希望编写一个查询,找出商店中当前有哪些鞋子的鞋带(颜色和长度)与之匹配,并且完全匹配的鞋带对的总数大于或等于 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;
类似地,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, 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;
这看起来可能效率低下,但规划器会通过““上拉””子查询将其折叠成单级查询树,然后它将计划连接,就像我们手动编写它们一样。因此,折叠查询树是一种优化,重写系统不必关心它。
SELECT
语句中的视图规则 #在上述视图规则的描述中,查询树的两个细节没有被触及。它们是命令类型和结果关系。事实上,视图规则不需要命令类型,但结果关系可能会影响查询重写程序的工作方式,因为如果结果关系是视图,则需要特别小心。
对于 SELECT
和任何其他命令的查询树,只有少数区别。显然,它们具有不同的命令类型,并且对于 SELECT
以外的命令,结果关系指向结果应该进入的范围表条目。其他所有内容完全相同。因此,对于具有列 a
和 b
的两个表 t1
和 t2
,两个语句的查询树
SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a; UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a;
几乎相同。具体来说
范围表包含表 t1
和 t2
的条目。
目标列表包含一个变量,指向表 t2
的范围表条目的列 b
。
限定表达式比较两个范围表条目的列 a
以判断相等性。
连接树显示 t1
和 t2
之间的简单连接。
结果是,两个查询树都产生类似的执行计划:它们都是对两个表的连接。对于 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 指向的旧行的行头中,cmax
和 xmax
条目被设置为当前命令计数器和当前事务 ID。因此,旧行被隐藏,并且在事务提交后,vacuum cleaner 最终可以删除死行。
知道了所有这些,我们就可以以完全相同的方式将视图规则应用于任何命令。没有区别。
上述内容演示了规则系统如何将视图定义合并到原始查询树中。在第二个示例中,从一个视图进行简单的 SELECT
创建了一个最终查询树,该树是 4 个表的连接(unit
使用了不同的名称两次)。
使用规则系统实现视图的好处在于,规划器具有有关必须扫描哪些表的信息,以及这些表之间的关系,以及视图中的限制性限定条件,以及原始查询中的限定条件,所有这些信息都在一个查询树中。即使原始查询已经是对视图的连接,情况仍然如此。规划器必须决定执行查询的最佳路径,规划器拥有的信息越多,此决策就越好。而 PostgreSQL 中实现的规则系统确保了这是查询到目前为止可用的所有信息。
如果视图被命名为 INSERT
、UPDATE
或 DELETE
的目标关系,会发生什么?执行上述替换将生成一个查询树,其中结果关系指向子查询范围表条目,这将不起作用。但是,PostgreSQL 有多种方式可以支持更新视图的外观。根据用户体验的复杂性,这些方式包括:自动将视图中的基础表替换为基础表、执行用户定义的触发器或根据用户定义的规则重写查询。以下讨论这些选项。
如果子查询从单个基本关系中选择并且足够简单,则重写器可以自动将子查询替换为基础基本关系,以便将 INSERT
、UPDATE
或 DELETE
以适当的方式应用于基本关系。对于此类“足够简单”的视图,称为自动可更新。有关可以自动更新的视图类型的详细信息,请参阅 CREATE VIEW。
或者,该操作可以通过视图上的用户提供的 INSTEAD OF
触发器来处理(请参阅 CREATE TRIGGER)。在这种情况下,重写的工作方式略有不同。对于 INSERT
,重写器不会对视图执行任何操作,将其保留为查询的结果关系。对于 UPDATE
和 DELETE
,仍然需要展开视图查询以生成命令将尝试更新或删除的“旧”行。因此,视图会正常展开,但另一个未展开的范围表条目会添加到查询中,以表示视图作为结果关系的能力。
现在出现的问题是如何识别视图中要更新的行。回想一下,当结果关系为表时,会将一个特殊的 CTID 条目添加到目标列表中,以识别要更新的行物理位置。如果结果关系为视图,则此方法无效,因为视图没有任何 CTID,因为其行没有实际物理位置。相反,对于 UPDATE
或 DELETE
操作,会将一个特殊的 wholerow
条目添加到目标列表中,该条目会展开以包含视图中的所有列。执行器使用此值向 INSTEAD OF
触发器提供 “旧” 行。触发器负责根据旧行和新行值确定要更新的内容。
另一种可能性是用户定义 INSTEAD
规则,该规则为视图上的 INSERT
、UPDATE
和 DELETE
命令指定替代操作。这些规则会重写命令,通常将其重写为更新一个或多个表而不是视图的命令。这是 第 41.4 节 的主题。
请注意,规则会首先进行评估,在计划和执行原始查询之前将其重写。因此,如果视图在 INSERT
、UPDATE
或 DELETE
上具有 INSTEAD OF
触发器以及规则,则会首先评估规则,并且根据结果,可能根本不会使用触发器。
最后总是尝试自动重写简单视图上的 INSERT
、UPDATE
或 DELETE
查询。因此,如果视图具有规则或触发器,它们将覆盖自动可更新视图的默认行为。
如果视图没有 INSTEAD
规则或 INSTEAD OF
触发器,并且重写器无法自动将查询重写为对基础基本关系的更新,则会引发错误,因为执行器无法按原样更新视图。