33.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 系统表里的视图的信息与一般表的信息完全一样。 所以对于查询分析器来说,表和视图之间完全没有区别。 它们是同样的事物:关系。

33.2.1. SELECT规则如何运转

ON SELECT 的规则在最后一步应用于所有查询, 哪怕给出的命令是一条INSERTUPDATEDELETE。 而且与其他(规则)有不同的语意, 那就是它们在现场修改查询树而不是创建一个新的(查询树)。 所以我们先介绍SELECT的规则。

目前,一个 ON SELECT 规则里只能有一个动作(action), 而且它必须是一个无条件的 INSTEAD (取代)的 SELECT 动作。 有这个限制是为了令规则安全到普通用户也可以打开它们, 并且它限制 ON SELECT 规则使之行为类似试图。

本文档的例子是两个连接视图, 它们做一些运算并且因此会涉及到更多视图的使用。 这两个视图之一稍后将利用对INSERTUPDATEDELETE 操作附加规则的方法客户化, 这样做最终的结果就会是这个视图表现得象一个具有一些特殊功能的真正的表。 这个例子可不是适合于开始的简单易懂的例子, 从这个例子开始讲可能会让我们的讲解变得有些难以理解。 但是我们认为用一个覆盖所有关键点的例子来一步一步讨论要比举很多例子搞乱思维好多了。

比如,我们需要一个小巧的 min 函数用于返回两个整数值中的小的那个。 我们用下面方法创建它

CREATE FUNCTION min(integer, integer) RETURNS integer AS $$
    SELECT CASE WHEN $1 < $2 THEN $1 ELSE $2 END
$$ LANGUAGE SQL STRICT;

我们头两个规则系统要用到的真实的表的描述如下:

CREATE TABLE shoe_data (
    shoename   text,      -- 主键
    sh_avail   integer,   -- (鞋的)可用对数
    slcolor    text,      -- 首选的鞋带颜色
    slminlen   real,	  -- 鞋带最短长度
    slmaxlen   real,	  -- 鞋带最长长度
    slunit     text	  -- 长度单位
);

CREATE TABLE shoelace_data (
    sl_name    text,     -- 主键
    sl_avail   integer,  -- (鞋带的)可用双数
    sl_color   text,     -- 鞋带颜色
    sl_len     real,	 -- 鞋带长度
    sl_unit    text	 -- 长度单位
);

CREATE TABLE unit (
    un_name    text,      -- 主键
    un_fact    real	  -- 转换成厘米的系数
);

你可以看到,这些表代表鞋店的数据。

视图创建为

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,
	   min(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;

创建shoelaceCREATE VIEW命令(也是我们用到的最简单的一个) 将创建一个关系 shoelace 并且在 pg_rewrite 表里增加一个记录,告诉系统有一个重写规则应用于所有范围表里引用了 shoelace 关系的查询。该规则没有规则条件(将在非 SELECT 规则讨论,因为目前的 SELECT 规则不可能有这些东西)并且它是 INSTEAD(取代)型的。 要注意规则条件与查询条件不一样。我们的规则动作(action)有一个查询条件。 规则的动作是一个查询树,这个查询是树视图创建命令中的 SELECT 语句的一个拷贝。

注意: 你在表 pg_rewrite 里看到的两个额外的用于 NEWOLD 的范围表记录(因历史原因, 在打印出来的查询树里叫 *NEW**CURRENT* ) 对 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;

然后把这些交给规则系统。规则系统把范围表(rangetable)过滤一遍, 检查一下在有没有适用任何关系的任意规则。当为 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_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,
	       min(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,
	       min(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 仍然和顶层的分开,因为它包含聚集函数。如果我们把它们也拉进来, 那它就会修改最顶层的 SELECT 的行为,那可不是我们想要的。 不过,压缩查询树是重写系统自己不需要关心的优化操作。

注意: 目前规则系统中没有用于视图规则递归终止机制(只有用于其他规则的)。 这一点不会造成太大的损害, 因为把这个(规则)无限循环(不断膨胀后端,直到耗尽内存)的唯一方法是创建表然后后手工用CREATE RULE命令创建视图规则, 这个规则是这样的:一个从其他地方来的选择(select)选择(select)了这个视图。 如果使用了CREATE VIEW,这一点是永远不会发生的, 因为第二个关系不存在,所以第一个CREATE VIEW 不能从第二个视图里面选择(select)。

33.2.2. 非SELECT语句的视图规则

有两个查询树的细节我们在上面的视图规则中没有涉及到。 就是命令类型和结果关系。实际上,视图规则不需要这些信息。

一个SELECT的查询树和用于其他命令的查询树只有少数几个区别。 显然,它们不同的命令类型并且对于SELECT之外的命令, 结果关系指向结果将前往的范围表入口。任何其它东西都完全是一样的。 所以如果有两个表 t1t2 分别有字段 ab, 下面两个语句的查询树

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

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

几乎是一样的。特别是:

结果是,两个查询树生成相似的执行规划:它们都是两个表的连接。 对于UPDATE语句来说, 规划器把 t1 缺失的字段追加到目标列因而最终查询树看起来象

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

因此执行器在连接上运行的结果和下面语句

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

是完全一样的。但是在UPDATE里有点问题: 执行器不关心它正在处理的从连接出来的结果的含义是什么。 它只是产生一个行的结果集。 一个是SELECT命令而另一个是UPDATE 命令的区别是由执行器的调用者控制的。 该调用者这时还知道(查看查询树)这是一个UPDATE, 而且它还知道结果要记录到表 t1 里去。 但是现有的记录中的哪一行要被新行取代呢?

要解决这个问题, 在UPDATEDELETE语句的目标列表里面增加了另外一个入口。 当前的元组 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。这样旧的行就被隐藏起来并且在事务提交之后, vacuum 清理器就可以真正把它们删除掉。

知道了这些,我们就可以简单的把视图的规则应用到任意命令中。 它们(视图和命令)没有区别。

33.2.3. PostgreSQL里视图的强大能力

上面演示了规则系统如何融合到视图定义的初始查询树中去。 在第二个例子里,一个简单的对视图的SELECT 创建了一个 4 个表联合的查询树(unit 以不同的名称用了两次)。

在规则系统里实现视图的好处是,规划器在一个查询树里拥有所有信息: 应该扫描哪个表 + 表之间的关系+ 视图的资格限制 + 初始查询的资格(条件)。 并且仍然是在最初的查询已经是一个视图的联合的情况下。 现在规划器必须决定执行查询的最优路径。 规划器拥有越多信息,它的决策就越好。并且 PostgreSQL 里的规则系统的实现保证这些信息是此时能获得的有关该查询的所有信息。

33.2.4. 更新一个视图

如果视图是INSERTUPDATE,或者 DELETE 的目标关系会怎样? 在完成我们上面描述的替换之后, 我们就有一个这样的查询树:结果关系指向一个是子查询的范围表记录。 这样可不能运行,所以如果重写器看到自己生成这么一个东西, 它就抛出一个错误。

要修改这个特性,我们可以定义修改这些命令行为的规则。 这是下一节的主题。