pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
在 Postgres 中,视图通过规则系统实现。实际上,以下命令:
CREATE VIEW myview AS SELECT * FROM mytab;
与下面这两条命令基本没有区别:
CREATE TABLE myview (same attribute list as for mytab);
CREATE RULE "_RETmyview" AS ON SELECT TO myview DO INSTEAD
SELECT * FROM mytab;
因为这正是 CREATE VIEW 命令在内部所做的事情。这会带来一些副作用。其中之一是,在 Postgres 系统目录中,视图的信息与表的信息完全相同。因此,对查询解析器而言,表和视图完全没有区别。它们是同一种东西——关系。这是目前最重要的一点。
ON SELECT 规则会在所有查询上作为最后一步应用,即使给出的命令是 INSERT、UPDATE 或 DELETE 也一样。它们与其他规则在语义上不同,因为它们是就地修改解析树,而不是创建新的解析树。因此我们先讨论 SELECT 规则。
目前,只能有一个动作,而且它必须是一个带有 INSTEAD 的 SELECT 动作。之所以有这个限制,是为了让规则足够安全,从而能够向普通用户开放;它也把 ON SELECT 规则限制为真正的视图规则。
本文的示例是两个进行一些计算的连接视图,以及另外一些依次使用它们的视图。最初两个视图中的一个会在后面通过为 INSERT、UPDATE 和 DELETE 操作添加规则来定制,从而最终得到一个在行为上像真正的表、但又带有某些特殊功能的视图。作为入门示例,这并不算简单,因此会让理解变得更难一些。但与其使用许多可能令人混淆的不同示例,不如用一个示例逐步覆盖这里讨论的全部要点。
用于在这些示例上操作的数据库名为 al_bundy。你很快就会明白为什么取这个名字。它还需要安装过程语言 PL/pgSQL,因为我们需要一个返回两个整数值中较小者的 min() 函数。我们这样创建它:
CREATE FUNCTION min(integer, integer) RETURNS integer AS 'BEGIN
IF $1 < $2 THEN
RETURN $1;
END IF;
RETURN $2;
END;' LANGUAGE 'plpgsql';
在前两节对规则系统的描述(descripitons)中,我们需要用到如下真实表:
CREATE TABLE shoe_data (
shoename char(10), -- primary key
sh_avail integer, -- available # of pairs
slcolor char(10), -- preferred shoelace color
slminlen float, -- miminum shoelace length
slmaxlen float, -- maximum shoelace length
slunit char(8) -- length unit
);
CREATE TABLE shoelace_data (
sl_name char(10), -- primary key
sl_avail integer, -- available # of pairs
sl_color char(10), -- shoelace color
sl_len float, -- shoelace length
sl_unit char(8) -- length unit
);
CREATE TABLE unit (
un_name char(8), -- the primary key
un_fact float -- factor to transform to cm
);
我想我们大多数人都穿鞋,都能看出这确实是很有用的数据。当然,世上有一些不需要鞋带的鞋,但这并不能让 Al 的日子好过一点,所以我们忽略这一点。
这些视图的创建方式如下:
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;
创建 shoelace 视图(这是我们这里最简单的一个例子)的 CREATE VIEW 命令会创建一个关系 shoelace,并在 pg_rewrite 中创建一项,说明只要查询的 rangetable 中引用了关系 shoelace,就必须应用一条重写规则。该规则没有规则条件(这在非 SELECT 规则中讨论,因为 SELECT 规则目前不能有规则条件),并且它是 INSTEAD。注意,规则条件与查询条件不是一回事!这里我们的规则动作带有一个查询条件。
规则动作本身是一棵查询树,它是视图创建命令中 SELECT 语句的一个精确副本。
你在 pg_rewrite 项中看到的、用于 NEW 和 OLD 的两个额外范围表项(在打印出来的查询树中,它们出于历史原因被命名为 *NEW* 和 *CURRENT*),与 SELECT 规则无关。
现在我们填充 unit、shoe_data 和 shoelace_data,然后 Al 在他的一生中第一次输入 SELECT:
al_bundy=> INSERT INTO unit VALUES ('cm', 1.0);
al_bundy=> INSERT INTO unit VALUES ('m', 100.0);
al_bundy=> INSERT INTO unit VALUES ('inch', 2.54);
al_bundy=>
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy-> ('sh1', 2, 'black', 70.0, 90.0, 'cm');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy-> ('sh2', 0, 'black', 30.0, 40.0, 'inch');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy-> ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy-> ('sh4', 3, 'brown', 40.0, 50.0, 'inch');
al_bundy=>
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl1', 5, 'black', 80.0, 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl2', 6, 'black', 100.0, 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl3', 0, 'black', 35.0 , 'inch');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl4', 8, 'black', 40.0 , 'inch');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl5', 4, 'brown', 1.0 , 'm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl6', 0, 'brown', 0.9 , 'm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl7', 7, 'brown', 60 , 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy-> ('sl8', 1, 'brown', 40 , 'inch');
al_bundy=>
al_bundy=> 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)
这是 Al 在这些视图上能执行的最简单的 SELECT,因此我们借此机会说明视图规则的基础。'SELECT * FROM shoelace' 由解析器解释后,会生成如下 parsetree:
SELECT shoelace.sl_name, shoelace.sl_avail,
shoelace.sl_color, shoelace.sl_len,
shoelace.sl_unit, shoelace.sl_len_cm
FROM shoelace shoelace;
然后它会被交给规则系统。规则系统遍历 rangetable,检查 pg_rewrite 中是否有关系对应的规则。在处理 shoelace 的 rangetable 项时(到目前为止只有这一个),它会找到带有如下 parsetree 的规则 '_RETshoelace':
SELECT s.sl_name, s.sl_avail,
s.sl_color, s.sl_len, s.sl_unit,
float8mul(s.sl_len, u.un_fact) AS sl_len_cm
FROM shoelace *OLD*, shoelace *NEW*,
shoelace_data s, unit u
WHERE bpchareq(s.sl_unit, u.un_name);
注意,解析器把计算和限定条件都改写成了对相应函数的调用。但这实际上并没有改变任何东西。重写的第一步是合并两个 rangetable。得到的 parsetree 如下:
SELECT shoelace.sl_name, shoelace.sl_avail,
shoelace.sl_color, shoelace.sl_len,
shoelace.sl_unit, shoelace.sl_len_cm
FROM shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data s,
unit u;
第 2 步把规则动作中的限定条件加到该 parsetree 上,得到:
SELECT shoelace.sl_name, shoelace.sl_avail,
shoelace.sl_color, shoelace.sl_len,
shoelace.sl_unit, shoelace.sl_len_cm
FROM shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data s,
unit u
WHERE bpchareq(s.sl_unit, u.un_name);
第 3 步把 parsetree 中所有引用当前正在处理的 rangetable 项(即 shoelace 的那一项)的变量,替换为规则动作 targetlist 中的相应表达式。最终得到的查询是:
SELECT s.sl_name, s.sl_avail,
s.sl_color, s.sl_len,
s.sl_unit, float8mul(s.sl_len, u.un_fact) AS sl_len_cm
FROM shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data s,
unit u
WHERE bpchareq(s.sl_unit, u.un_name);
把它转换回人类用户会输入的真实 SQL 语句,读作:
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;
这就是应用的第一条规则。在此过程中,rangetable 已经变大了。于是规则系统继续检查各个 rangetable 项。下一个是第 2 号(shoelace *OLD*)。关系 shoelace 有一条规则,但这个 rangetable 项没有被 parsetree 中的任何变量引用,因此被忽略。由于其余的 rangetable 项要么在 pg_rewrite 中没有规则、要么未被引用,检查到达 rangetable 末尾。重写至此完成,上面的结果就是交给优化器的最终结果。优化器会忽略那些未被 parsetree 中的变量引用的额外 rangetable 项,优化器/优化器产生的计划,将与 Al 直接输入上面的 SELECT 查询(而不是对视图的选择)完全相同。
现在我们让 Al 面对这样一个问题:布鲁斯兄弟(Blues Brothers)来到他的店里,想买一些新鞋;而且正如布鲁斯兄弟的本色那样,他们要穿一模一样的鞋。他们还想立刻就穿上,所以还需要鞋带。
Al 需要知道,商店里目前哪些鞋子有颜色和尺码都匹配的鞋带,并且完全匹配的总双数大于等于二。我们教会(theach)他怎么做,于是他询问他的数据库:
al_bundy=> 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)
Al 是一位鞋类行家,所以他知道只有 sh1 型号的鞋才合适(鞋带 sl7 是棕色的,而需要棕色鞋带的鞋是布鲁斯兄弟绝不会穿的鞋)。
这一次解析器的输出是如下 parsetree:
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 int4ge(shoe_ready.total_avail, 2);
首先应用的是用于 shoe_ready 关系的规则,它得到如下 parsetree:
SELECT rsh.shoename, rsh.sh_avail,
rsl.sl_name, rsl.sl_avail,
min(rsh.sh_avail, rsl.sl_avail) AS total_avail
FROM shoe_ready shoe_ready, shoe_ready *OLD*,
shoe_ready *NEW*, shoe rsh,
shoelace rsl
WHERE int4ge(min(rsh.sh_avail, rsl.sl_avail), 2)
AND (bpchareq(rsl.sl_color, rsh.slcolor)
AND float8ge(rsl.sl_len_cm, rsh.slminlen_cm)
AND float8le(rsl.sl_len_cm, rsh.slmaxlen_cm)
);
实际上,限定条件中的 AND 子句会是 AND 类型的操作符节点,各带一个左表达式和一个右表达式。但那样只会让本已不易读的东西更难读,而且还有更多规则要应用。所以我只是把它们放进一些括号里,按添加的顺序把它们分组成逻辑单元,然后继续处理关系 shoe 的规则,因为它是下一个被引用且带有规则的 rangetable 项。应用该规则的结果是:
SELECT sh.shoename, sh.sh_avail,
rsl.sl_name, rsl.sl_avail,
min(sh.sh_avail, rsl.sl_avail) AS total_avail,
FROM shoe_ready shoe_ready, shoe_ready *OLD*,
shoe_ready *NEW*, shoe rsh,
shoelace rsl, shoe *OLD*,
shoe *NEW*, shoe_data sh,
unit un
WHERE (int4ge(min(sh.sh_avail, rsl.sl_avail), 2)
AND (bpchareq(rsl.sl_color, sh.slcolor)
AND float8ge(rsl.sl_len_cm,
float8mul(sh.slminlen, un.un_fact))
AND float8le(rsl.sl_len_cm,
float8mul(sh.slmaxlen, un.un_fact))
)
)
AND bpchareq(sh.slunit, un.un_name);
最后我们应用那条已经十分熟悉的 shoelace 规则(这一次是在一个稍微复杂一点的 parsetree 上),得到:
SELECT sh.shoename, sh.sh_avail,
s.sl_name, s.sl_avail,
min(sh.sh_avail, s.sl_avail) AS total_avail
FROM shoe_ready shoe_ready, shoe_ready *OLD*,
shoe_ready *NEW*, shoe rsh,
shoelace rsl, shoe *OLD*,
shoe *NEW*, shoe_data sh,
unit un, shoelace *OLD*,
shoelace *NEW*, shoelace_data s,
unit u
WHERE ( (int4ge(min(sh.sh_avail, s.sl_avail), 2)
AND (bpchareq(s.sl_color, sh.slcolor)
AND float8ge(float8mul(s.sl_len, u.un_fact),
float8mul(sh.slminlen, un.un_fact))
AND float8le(float8mul(s.sl_len, u.un_fact),
float8mul(sh.slmaxlen, un.un_fact))
)
)
AND bpchareq(sh.slunit, un.un_name)
)
AND bpchareq(s.sl_unit, u.un_name);
我们再把它化简为与规则系统最终输出等价的真实 SQL 语句:
SELECT sh.shoename, sh.sh_avail,
s.sl_name, s.sl_avail,
min(sh.sh_avail, s.sl_avail) AS total_avail
FROM shoe_data sh, shoelace_data s, unit u, unit un
WHERE min(sh.sh_avail, s.sl_avail) >= 2
AND s.sl_color = sh.slcolor
AND s.sl_len * u.un_fact >= sh.slminlen * un.un_fact
AND s.sl_len * u.un_fact <= sh.slmaxlen * un.un_fact
AND sh.sl_unit = un.un_name
AND s.sl_unit = u.un_name;
规则的递归处理把一个对视图的 SELECT 重写成了一个 parsetree,它恰好等价于在没有视图的情况下 Al 必须输入的内容。
规则系统目前对视图规则没有递归终止机制(对其他规则才有)。 这并不会造成太大问题,因为要把处理推入无限循环(使后端不断膨胀, 直至达到内存上限)的唯一方式,是先创建一些表,然后手工用 CREATE RULE 把视图规则设置成一个从另一个选择、 而另一个又从这个选择的形式。如果使用 CREATE VIEW, 这种情况绝不会发生,因为在执行第一个 CREATE VIEW 时, 第二个关系尚不存在,因此第一个视图无法从第二个视图选择。
上面关于视图规则的描述没有触及 parsetree 的两个细节:commandtype 和 resultrelation。事实上,视图规则并不需要这些信息(informations)。
SELECT 的 parsetree 与其他任何命令的 parsetree 之间只有少数差别。显然,它们的 commandtype 不同,而且这一次 resultrelation 会指向结果应写入的那个 rangetable 项。除此之外,其余部分完全相同。因此,假设有两个表 t1 和 t2,都具有属性 a 和 b,那么下面两条语句的解析树:
SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a; UPDATE t1 SET b = t2.b WHERE t1.a = t2.a;
几乎是一样的。
范围表中包含表 t1 和 t2 的项。
目标列表中都包含一个变量,该变量指向表 t2 的范围表项中的属性 b。
条件表达式会比较两个范围中的属性 a 是否相等。
结果是,这两个 parsetree 都会产生相似的执行计划。它们都是对这两个表的连接。对于 UPDATE,优化器会把 t1 中缺失的列补入 targetlist,最终的 parsetree 会读作:
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。但那 666 行里的哪一行应当被新行替换?所执行的计划是一个带限定条件的连接,可能以未知顺序产生 0 到 666 行之间的任意数量的行。
为了解决这个问题,UPDATE 和 DELETE 语句的 targetlist 中会额外加入一项:当前元组 ID(ctid)。这是一个带特殊功能的系统属性。它包含该行所在的块号以及在块中的位置。已知表之后,利用 ctid 只需取出一个数据块,就可以在一个包含数百万行、大小为 1.5GB 的表中找到特定的那一行。把 ctid 加入 targetlist 后,最终结果集可以定义为:
SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
现在还要引入 Postgres 的另一个细节。目前,表行不会被覆盖,这也是 ABORT TRANSACTION 之所以很快的原因。在 UPDATE 中,新结果行会被插入表中(去掉 ctid 之后),而 ctid 所指向的行,其元组头中的 cmax 和 xmax 项会被设置为当前命令计数器和当前事务 ID。 于是旧行被隐藏起来,事务提交后,清理器(vacuum)最终就可以 真正移除它。
知道了那一切之后,我们就可以用完全相同的方式把视图规则应用到任何命令上。没有区别。
上文演示了规则系统如何把视图定义整合进原始 parsetree。在第二个示例中,从一个视图发出的简单 SELECT 最终生成了一个四表连接的 parsetree(unit 以不同名称使用了两次)。
用规则系统实现视图的好处在于:优化器能够在单个 parsetree 中同时看到哪些表必须被扫描、这些表之间的关系、来自视图的限制条件,以及原始查询自身的条件。即使原始查询本身已经是对若干视图的连接,情况也仍然如此。现在,优化器必须决定执行查询的最佳路径。优化器掌握的信息越多,这个决定就能做得越好。Postgres实现的规则系统能够保证,到此为止,关于该查询的全部可用信息都已经集中在这里。
曾经有很长一段时间,Postgres 的规则系统被认为是坏的。不推荐使用规则,唯一能工作的部分是视图规则。而即使是这些视图规则也会带来问题,因为规则系统无法在 SELECT 之外的语句上正确应用它们(例如,一个使用了视图数据的 UPDATE 无法工作)。
在那段时间里,开发继续推进,解析器和优化器添加了许多特性。规则系统与它们的能力越来越脱节,着手修复也越来越难。于是,没有人去做。
到了 6.4,有人锁上门、深吸一口气,把这该死的东西彻底翻修了一遍。出来的就是本文所描述的这种能力的规则系统。但仍有一些构造没有得到处理,还有一些因为 Postgres 查询优化器目前不支持而失败。
带聚合列的视图有严重的问题。限定条件中的聚合表达式必须放在子查询中使用。目前无法对两个各带一个聚合列的视图做连接,并在限定条件中比较这两个聚合值。眼下可以把这些聚合表达式放进带有适当参数的函数中,再在视图定义里使用它们。
目前不支持 union 的视图。把一个简单的 SELECT 重写成 union 很容易。但如果该视图是一个做更新的连接的一部分,就有点困难了。
不支持视图定义中的 ORDER BY 子句。
不支持视图定义中的 DISTINCT。
没有好的理由说明,优化器为什么不应该处理那些解析器由于 SQL 语法限制而永远无法产生的 parsetree 构造。作者希望这些条目将来会消失。
用上面描述的规则系统来实现视图,有一个有趣的副作用。下面这个操作看起来不起作用:
al_bundy=> INSERT INTO shoe (shoename, sh_avail, slcolor)
al_bundy-> VALUES ('sh5', 0, 'black');
INSERT 20128 1
al_bundy=> SELECT shoename, sh_avail, slcolor FROM shoe_data;
shoename |sh_avail|slcolor
----------+--------+----------
sh1 | 2|black
sh3 | 4|brown
sh2 | 0|black
sh4 | 3|brown
(4 rows)
有趣的是,INSERT 的返回码给了我们一个对象 ID,并告诉我们已经插入了 1 行。但它并没有出现在 shoe_data 中。查看数据库目录可以发现,视图关系 shoe 的数据库文件现在似乎有了一个数据块。而这确实是真的。
我们还可以执行一条 DELETE,如果它不带限定条件,它会告诉我们已经删除了一些行,而下次 vacuum 运行将把该文件重置为零大小。
这种行为的原因在于,INSERT 的 parsetree 没有在任何变量中引用 shoe 关系。targetlist 只包含常量值。因此没有规则可应用,它原封不动地进入执行,行被插入。DELETE 也是如此。
要改变这种情况,我们可以定义规则来修改非 SELECT 查询的行为。这是下一节的主题。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。