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
命令,尽管其缺少底层存储。这会在下文中 第 39.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;
CREATE VIEW
命令用于 shoelace
视图(这是我们拥有的最简单的视图),将创建一个关系 shoelace
并在 pg_rewrite
中创建一个条目,指示无论何时在查询的范围表中引用关系 shoelace
,都必须应用一条重写规则。该规则没有规则限定(以后在非 SELECT
规则中讨论,因为 SELECT
规则当前不能拥有它们),并且它是 INSTEAD
。请注意,规则限定与查询限定不同。我们的规则的动作具有查询限定。该规则的动作是一个查询树,它是视图创建命令中的 SELECT
语句的副本。
NEW
和 OLD
在 pg_rewrite
条目中可以看到的两个额外范围表条目对于 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
或 MERGE
的目标关系对一个视图进行命名,会发生什么?进行上述替换将得到一个查询树,其中结果关系指向一个子查询范围表项,这是不可行的。PostgreSQL 可以通过多种方式支持更新视图的行为。按照用户体验的复杂程度排列,这些方式包括:自动替换视图中的基本表、执行用户定义的触发器或根据用户定义的规则重写查询。以下将讨论这些选项。
如果子查询从单个基本关系中进行选择且足够简单,重写器可以自动用基本关系替换子查询,这样,INSERT
、UPDATE
、DELETE
或 MERGE
将以适当方式应用于基本关系。对于此操作“足够简单”的视图被称为自动可更新。有关可自动更新的视图类型的详细信息,请参见 CREATE VIEW。
或者,此操作可以通过视图上的用户提供的 INSTEAD OF
触发器来处理(请参见 CREATE TRIGGER)。在这种情况下,重写的操作略有不同。对于 INSERT
,重写器对视图没有任何操作,让它作为查询的结果关系。对于 UPDATE
、DELETE
和 MERGE
,仍然需要展开视图查询以生成命令将尝试更新、删除或合并的“旧”行。因此,正常展开视图,但向查询添加另一个未展开的范围表项以表示视图作为结果关系的能力。
现在出现的问题是如何识别视图中要更新的行。回想一下,当结果关系是一个表时,一个特殊CTID向目标列表中添加了一项,以标识待更新行实际位置。如果结果关系视图,此操作将无效,因为视图没有CTID它不带有实际物理位置,因为它的行没有实际的物理位置。对于 UPDATE
、 DELETE
或 MERGE
操作,会向目标列表中添加一个特殊的 wholrow
条目,它将会展开为包含视图中的所有列。执行程序使用此值向 INSTEAD OF
触发器提供 “old” 行。基于新旧行值搞清楚要更新什么内容是触发器的工作。
另一种可能性是用户定义 INSTEAD
规则,指定视图上的 INSERT
、 UPDATE
和 DELETE
命令的替代操作。这些规则将重新编写命令,通常将其转换为更新一个或多个表而不是视图的命令。这是 第 39.4 节 的主题。请注意,这与 MERGE
不起作用,它目前不支持目标关系规则(除 SELECT
规则之外)。
请注意,首先会评估规则,在计划和执行原始查询之前对其进行重新编写。因此,如果视图除了对 INSERT
、 UPDATE
或 DELETE
有规则之外,还具有 INSTEAD OF
触发器,那么会优先评估规则,根据结果,触发器根本不会被使用。
始终最后尝试对简单视图上的 INSERT
、 UPDATE
、 DELETE
或 MERGE
查询进行自动改写。因此,如果视图具有规则或触发器,它们将覆盖自动可更新视图的默认行为。
如果视图没有 INSTEAD
规则或 INSTEAD OF
触发器,并且改写器无法将查询自动改写为对基础基本关系的更新,则会抛出错误,因为执行程序无法更新此类视图。