pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
定义在 ON INSERT、UPDATE 和 DELETE 上的规则与前一节描述的视图规则完全不同。首先,它们的 CREATE RULE 命令允许更多:
它们可以没有动作。
它们可以有多个动作。
INSTEAD 关键字是可选的。
伪关系 NEW 和 OLD 可以派上用场。
它们可以有规则条件。
第二,它们不是就地修改解析树,而是创建零棵或多棵新解析树,并且可能丢弃原始查询树。
记住以下语法:
CREATE RULE rule_name AS ON event
TO object [WHERE rule_qualification]
DO [INSTEAD] [action | (actions) | NOTHING];
在后文中,更新规则是指定义在 ON INSERT、UPDATE 或 DELETE 上的规则。
当解析树的结果关系和命令类型分别等于 CREATE RULE 命令中给出的对象和事件时,规则系统就会应用更新规则。对于更新规则,规则系统会创建一个解析树列表,初始时该列表为空。动作可以有零个(NOTHING 关键字)、一个或多个。为简化说明,我们来看只有一个动作的规则。这个规则可以有条件,也可以没有条件;它可以是 INSTEAD,也可以不是。
什么是规则条件?它是一种限制,用来说明何时执行规则动作、何时不执行。这个条件只能引用 NEW 和/或 OLD 伪关系,它们基本上就是作为对象给出的那个关系,只是带有特殊含义。
因此,对于只有一个动作的规则,有以下四种情况会产生相应的解析树。
没有条件,也不是 INSTEAD:
规则动作的解析树,其中附加了原始解析树的条件。
没有条件,但带有 INSTEAD:
规则动作的解析树,其中附加了原始解析树的条件。
给出了条件,不是 INSTEAD:
规则动作的解析树,其中附加了规则条件和原始解析树的条件。
给出了条件,带有 INSTEAD:
规则动作的解析树,其中附加了规则条件和原始解析树的条件。
原始解析树,其中附加了规则条件取反后的条件。
最后,如果规则不是 INSTEAD,就把未经更改的原始解析树加入列表。由于只有带条件的 INSTEAD 规则已经加入了原始解析树,因此,对于只有一个动作的规则,最终会得到一棵或两棵输出解析树。
对于 ON INSERT 规则,原始查询(如果未被 INSTEAD 抑制)会先于规则添加的任何动作执行。这样一来,这些动作就能看到被插入的行。但对 ON UPDATE 和 ON DELETE 规则,原始查询会在规则添加的动作之后执行。这就保证了这些动作能够看到将被更新或删除的行;否则,动作可能什么也做不了,因为它们找不到符合条件的行。
由规则动作生成的解析树会再次送入重写系统,随后可能还会有更多规则被应用,从而产生更多或更少的解析树。因此,规则动作中的解析树必须具有不同的命令类型,或者具有不同的结果关系。否则,这种递归过程就会陷入循环。目前内置的递归上限是 10 次迭代。如果经过 10 次迭代之后仍有更新规则要应用,规则系统就会认为出现了多个规则定义之间的循环,并报告一个错误。
pg_rewrite 系统目录中动作里的解析树只是模板。由于它们可以引用 NEW 和 OLD 的范围表项,因此在使用前必须进行一些替换。对任何 NEW 的引用,都会先在原始查询的目标列表中查找相应项。如果找到了,就用该项的表达式替换该引用。否则,NEW 就与 OLD 含义相同(对于 UPDATE),或者被替换为 NULL(对于 INSERT)。任何对 OLD 的引用,都会被替换为对作为结果关系的那个范围表项的引用。
在我们完成应用更新规则之后,再对生成的解析树应用视图规则。视图无法插入新的更新动作,所以没有必要向视图重写的输出应用更新规则。
我们想跟踪 shoelace_data 关系中 sl_avail 列的变化。因此,我们建立一个日志表和一条规则,使其在 shoelace_data 上执行 UPDATE 时,有条件地写入一条日志记录。
CREATE TABLE shoelace_log (
sl_name char(10), -- shoelace changed
sl_avail integer, -- new available value
log_who text, -- who did it
log_when timestamp -- when
);
CREATE RULE log_shoelace AS ON UPDATE TO shoelace_data
WHERE NEW.sl_avail != OLD.sl_avail
DO INSERT INTO shoelace_log VALUES (
NEW.sl_name,
NEW.sl_avail,
current_user,
current_timestamp
);
现在 Al 执行了:
al_bundy=> UPDATE shoelace_data SET sl_avail = 6 al_bundy-> WHERE sl_name = 'sl7';
然后我们查看日志表:
al_bundy=> SELECT * FROM shoelace_log; sl_name |sl_avail|log_who|log_when ----------+--------+-------+-------------------------------- sl7 | 6|Al |Tue Oct 20 16:14:45 1998 MET DST (1 row)
这正是我们预期的结果。后台发生的事情如下。解析器创建了解析树(这一次突出显示的是原始解析树中的部分,因为对更新规则而言,操作的基础是规则动作):
UPDATE shoelace_data SET sl_avail = 6
FROM shoelace_data shoelace_data
WHERE bpchareq(shoelace_data.sl_name, 'sl7');
此时存在一条 ON UPDATE 规则 log_shoelace,它带有如下规则条件表达式:
int4ne(NEW.sl_avail, OLD.sl_avail)
以及一个动作:
INSERT INTO shoelace_log VALUES(
*NEW*.sl_name, *NEW*.sl_avail,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*;
这看起来有点奇怪,因为通常你不能写 INSERT ... VALUES ... FROM。这里的 FROM 子句只是为了表明解析树中存在用于 *NEW* 和 *OLD* 的范围表项。之所以需要这些项,是为了让 INSERT 命令的 querytree 中的变量能够引用它们。
该规则是一条带条件的非 INSTEAD 规则,因此规则系统必须返回两棵解析树:修改后的规则动作,以及原始解析树。第一步中,原始查询的范围表会并入规则动作的解析树,得到:
INSERT INTO shoelace_log VALUES(
*NEW*.sl_name, *NEW*.sl_avail,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*,
shoelace_data shoelace_data;
第 2 步将规则条件加进去,因此结果集被限制为 sl_avail 发生变化的行:
INSERT INTO shoelace_log VALUES(
*NEW*.sl_name, *NEW*.sl_avail,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*,
shoelace_data shoelace_data
WHERE int4ne(*NEW*.sl_avail, *OLD*.sl_avail);
这看起来更奇怪,因为 INSERT ... VALUES 同样没有 WHERE 子句,但规划器和执行器处理它并无困难。反正它们本来也需要为 INSERT ... SELECT 支持相同功能。 第 3 步把原始解析树的条件加进去,把结果集进一步限制为只有那些原始解析树会触及的行:
INSERT INTO shoelace_log VALUES(
*NEW*.sl_name, *NEW*.sl_avail,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*,
shoelace_data shoelace_data
WHERE int4ne(*NEW*.sl_avail, *OLD*.sl_avail)
AND bpchareq(shoelace_data.sl_name, 'sl7');
第 4 步把 NEW 引用替换为原始解析树中的目标列表项,或者替换为结果关系中相应的变量引用:
INSERT INTO shoelace_log VALUES(
shoelace_data.sl_name, 6,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*,
shoelace_data shoelace_data
WHERE int4ne(6, *OLD*.sl_avail)
AND bpchareq(shoelace_data.sl_name, 'sl7');
第 5 步把 OLD 引用替换为结果关系引用:
INSERT INTO shoelace_log VALUES(
shoelace_data.sl_name, 6,
current_user, current_timestamp
FROM shoelace_data *NEW*, shoelace_data *OLD*,
shoelace_data shoelace_data
WHERE int4ne(6, shoelace_data.sl_avail)
AND bpchareq(shoelace_data.sl_name, 'sl7');
至此就完成了。由于规则不是 INSTEAD,我们还要输出原始解析树。简而言之,规则系统输出的是一个包含两棵解析树的列表,它们等同于以下语句:
INSERT INTO shoelace_log VALUES(
shoelace_data.sl_name, 6,
current_user, current_timestamp
FROM shoelace_data
WHERE 6 != shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
UPDATE shoelace_data SET sl_avail = 6
WHERE sl_name = 'sl7';
它们会按这个顺序执行,而这正是该规则所定义的行为。上述替换以及附加的条件能够保证:如果原始查询是下面这样,就不会写入任何日志记录:
UPDATE shoelace_data SET sl_color = 'green' WHERE sl_name = 'sl7';
在这种情况下,原始解析树不包含 sl_avail 的目标列表项,因此 NEW.sl_avail 会被 shoelace_data.sl_avail 替换,于是规则产生的额外查询是:
INSERT INTO shoelace_log VALUES(
shoelace_data.sl_name, shoelace_data.sl_avail,
current_user, current_timestamp)
FROM shoelace_data
WHERE shoelace_data.sl_avail != shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
而该条件永远不可能为真。如果原始查询修改多行,这种机制同样能够正常工作。因此,如果 Al 发出如下命令:
UPDATE shoelace_data SET sl_avail = 0 WHERE sl_color = 'black';
实际上会更新四行(sl1、sl2、sl3 和 sl4)。但 sl3 本来就已经是 sl_avail = 0。这一次,原始解析树的条件不同,因此规则会产生额外的解析树:
INSERT INTO shoelace_log SELECT
shoelace_data.sl_name, 0,
current_user, current_timestamp
FROM shoelace_data
WHERE 0 != shoelace_data.sl_avail
AND shoelace_data.sl_color = 'black';
这棵解析树必然会插入三条新的日志记录。这完全正确。
到这里就能看出,为什么原始解析树最后执行至关重要。如果先执行 UPDATE,那么所有行都已经被设为零,记日志的 INSERT 就找不到任何满足 0 != shoelace_data.sl_avail 的行了。
为了防止有人像前面提到的那样对视图关系尝试 INSERT、UPDATE 和 DELETE,一种简单的办法是让那些解析树直接被丢弃。我们创建如下规则:
CREATE RULE shoe_ins_protect AS ON INSERT TO shoe
DO INSTEAD NOTHING;
CREATE RULE shoe_upd_protect AS ON UPDATE TO shoe
DO INSTEAD NOTHING;
CREATE RULE shoe_del_protect AS ON DELETE TO shoe
DO INSTEAD NOTHING;
如果现在 Al 尝试对视图关系 shoe 执行这些操作中的任何一种,规则系统就会应用这些规则。由于这些规则没有动作而且是 INSTEAD,生成的解析树列表将为空;整个查询也就什么都不会做,因为规则系统处理完后已经没有任何内容可供优化或执行。
这种方式可能会让前端应用程序感到困惑,因为数据库上绝对什么都没有发生,因而后端不会为该查询返回任何东西,甚至连 PGRES_EMPTY_QUERY 也不会在 libpq 中出现。在 psql 中,什么都不会发生。这一点将来可能会改变。
另一种更完善的做法,是创建一些规则,把解析树重写成在真实表上执行正确操作的解析树。要在 shoelace 视图上做到这一点,我们创建下列规则:
CREATE RULE shoelace_ins AS ON INSERT TO shoelace
DO INSTEAD
INSERT INTO shoelace_data VALUES (
NEW.sl_name,
NEW.sl_avail,
NEW.sl_color,
NEW.sl_len,
NEW.sl_unit);
CREATE RULE shoelace_upd AS ON UPDATE TO shoelace
DO INSTEAD
UPDATE shoelace_data SET
sl_name = NEW.sl_name,
sl_avail = NEW.sl_avail,
sl_color = NEW.sl_color,
sl_len = NEW.sl_len,
sl_unit = NEW.sl_unit
WHERE sl_name = OLD.sl_name;
CREATE RULE shoelace_del AS ON DELETE TO shoelace
DO INSTEAD
DELETE FROM shoelace_data
WHERE sl_name = OLD.sl_name;
现在有一批鞋带到货进入 Al 的商店,随附一份很长的部件清单。Al 不太擅长计算,因此我们不想让他手工更新 shoelace 视图。于是我们建立两个小表:一个让他可以插入部件清单中的项目,另一个则采用一个特殊技巧。创建命令如下:
CREATE TABLE shoelace_arrive (
arr_name char(10),
arr_quant integer
);
CREATE TABLE shoelace_ok (
ok_name char(10),
ok_quant integer
);
CREATE RULE shoelace_ok_ins AS ON INSERT TO shoelace_ok
DO INSTEAD
UPDATE shoelace SET
sl_avail = sl_avail + NEW.ok_quant
WHERE sl_name = NEW.ok_name;
现在 Al 可以坐下来随便干点什么,直到
al_bundy=> SELECT * FROM shoelace_arrive; arr_name |arr_quant ----------+--------- sl3 | 10 sl6 | 20 sl8 | 20 (3 rows)
与部件清单上的内容完全一致为止。我们快速查看一下当前数据,
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 | 6|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_bundy=> INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;
然后检查结果:
al_bundy=> SELECT * FROM shoelace ORDER BY sl_name; 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 | 6|brown | 60|cm | 60 sl4 | 8|black | 40|inch | 101.6 sl3 | 10|black | 35|inch | 88.9 sl8 | 21|brown | 40|inch | 101.6 sl5 | 4|brown | 1|m | 100 sl6 | 20|brown | 0.9|m | 90 (8 rows) al_bundy=> SELECT * FROM shoelace_log; sl_name |sl_avail|log_who|log_when ----------+--------+-------+-------------------------------- sl7 | 6|Al |Tue Oct 20 19:14:45 1998 MET DST sl3 | 10|Al |Tue Oct 20 19:25:16 1998 MET DST sl6 | 20|Al |Tue Oct 20 19:25:16 1998 MET DST sl8 | 21|Al |Tue Oct 20 19:25:16 1998 MET DST (4 rows)
从一条 INSERT ... SELECT 到这些结果,中间要经历相当长的一段过程。对它的描述将是本文中的最后一个(但不是最后一个例子 :-))。首先是解析器的输出:
INSERT INTO shoelace_ok SELECT
shoelace_arrive.arr_name, shoelace_arrive.arr_quant
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok;
现在应用第一条规则 shoelace_ok_ins,它会把这一输出转换成:
UPDATE shoelace SET
sl_avail = int4pl(shoelace.sl_avail, shoelace_arrive.arr_quant)
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok *OLD*, shoelace_ok *NEW*,
shoelace shoelace
WHERE bpchareq(shoelace.sl_name, showlace_arrive.arr_name);
同时会丢弃针对 shoelace_ok 的原始 INSERT。这个重写后的查询会再次交给规则系统,随后应用的规则 shoelace_upd 产生:
UPDATE shoelace_data SET
sl_name = shoelace.sl_name,
sl_avail = int4pl(shoelace.sl_avail, shoelace_arrive.arr_quant),
sl_color = shoelace.sl_color,
sl_len = shoelace.sl_len,
sl_unit = shoelace.sl_unit
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok *OLD*, shoelace_ok *NEW*,
shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data showlace_data
WHERE bpchareq(shoelace.sl_name, showlace_arrive.arr_name)
AND bpchareq(shoelace_data.sl_name, shoelace.sl_name);
这同样是一条 INSTEAD 规则,因此前一个解析树会被丢弃。注意,这个查询仍然使用视图 shoelace。但规则系统在这个循环中尚未完成,因此它会继续在其上应用规则 _RETshoelace,于是得到:
UPDATE shoelace_data SET
sl_name = s.sl_name,
sl_avail = int4pl(s.sl_avail, shoelace_arrive.arr_quant),
sl_color = s.sl_color,
sl_len = s.sl_len,
sl_unit = s.sl_unit
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok *OLD*, shoelace_ok *NEW*,
shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data showlace_data,
shoelace *OLD*, shoelace *NEW*,
shoelace_data s, unit u
WHERE bpchareq(s.sl_name, showlace_arrive.arr_name)
AND bpchareq(shoelace_data.sl_name, s.sl_name);
又有一条更新规则被应用,于是车轮继续转动,我们进入了第 3 轮重写。这一次应用的是规则 log_shoelace,这产生了额外的解析树:
INSERT INTO shoelace_log SELECT
s.sl_name,
int4pl(s.sl_avail, shoelace_arrive.arr_quant),
current_user,
current_timestamp
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok *OLD*, shoelace_ok *NEW*,
shoelace shoelace, shoelace *OLD*,
shoelace *NEW*, shoelace_data showlace_data,
shoelace *OLD*, shoelace *NEW*,
shoelace_data s, unit u,
shoelace_data *OLD*, shoelace_data *NEW*
shoelace_log shoelace_log
WHERE bpchareq(s.sl_name, showlace_arrive.arr_name)
AND bpchareq(shoelace_data.sl_name, s.sl_name);
AND int4ne(int4pl(s.sl_avail, shoelace_arrive.arr_quant), s.sl_avail);
到这里,规则系统已经没有更多规则可用,返回生成的解析树。于是我们最终得到两棵解析树,它们等同于以下 SQL 语句:
INSERT INTO shoelace_log SELECT
s.sl_name,
s.sl_avail + shoelace_arrive.arr_quant,
current_user,
current_timestamp
FROM shoelace_arrive shoelace_arrive, shoelace_data shoelace_data,
shoelace_data s
WHERE s.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = s.sl_name
AND s.sl_avail + shoelace_arrive.arr_quant != s.sl_avail;
UPDATE shoelace_data SET
sl_avail = shoelace_data.sl_avail + shoelace_arrive.arr_quant
FROM shoelace_arrive shoelace_arrive,
shoelace_data shoelace_data,
shoelace_data s
WHERE s.sl_name = shoelace_arrive.sl_name
AND shoelace_data.sl_name = s.sl_name;
结果是:来自一个关系的数据被插入到另一个关系中,这个插入被改写为对第三个关系的更新,再被改写为对第四个关系的更新外加在第五个关系中记录该更新,最后整个过程被化简为两个查询。
这里有个稍显难看的小细节。观察这两个查询就会发现,shoelace_data 关系在范围表中出现了两次,而实际上完全可以缩减成一次。规划器不会处理这一点,因此规则系统为 INSERT 输出的执行计划会是:
Nested Loop
-> Merge Join
-> Seq Scan
-> Sort
-> Seq Scan on s
-> Seq Scan
-> Sort
-> Seq Scan on shoelace_arrive
-> Seq Scan on shoelace_data
而省略那个额外的范围表项则会得到:
Merge Join
-> Seq Scan
-> Sort
-> Seq Scan on s
-> Seq Scan
-> Sort
-> Seq Scan on shoelace_arrive
后者完全会在日志关系中产生相同的项。因此,规则系统导致了对 shoelace_data 关系的一次完全不必要的额外扫描。而在 UPDATE 中,同样的冗余扫描还会再发生一次。不过,能让这一切总体上工作起来,已经是一项相当艰巨的工作。
最后来演示一下 PostgreSQL 规则系统及其威力。有一位漂亮的金发女郎卖鞋带。而 Al 永远不会意识到的是,她不仅漂亮,还很聪明——甚至有点太聪明了。于是,Al 不时会订购一些完全卖不出去的鞋带。这一次他订购了 1000 双品红色(magenta)鞋带,而且由于另一种鞋带暂时缺货、而他已承诺购买一些,他又为粉色(pink)的鞋带做好了数据库准备:
al_bundy=> INSERT INTO shoelace VALUES
al_bundy-> ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
al_bundy=> INSERT INTO shoelace VALUES
al_bundy-> ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);
由于这种事经常发生,我们必须不时地找出那些绝对不适合任何鞋子的鞋带项。我们可以每次都用一条复杂的语句来完成,也可以为此建立一个视图。这个视图是:
CREATE VIEW shoelace_obsolete AS
SELECT * FROM shoelace WHERE NOT EXISTS
(SELECT shoename FROM shoe WHERE slcolor = sl_color);
它的输出是:
al_bundy=> SELECT * FROM shoelace_obsolete; sl_name |sl_avail|sl_color |sl_len|sl_unit |sl_len_cm ----------+--------+----------+------+--------+--------- sl9 | 0|pink | 35|inch | 88.9 sl10 | 1000|magenta | 40|inch | 101.6
那 1000 双品红色鞋带,我们得先向 Al 讨了债才能把它们扔掉,不过那是另一个问题了。粉色的条目我们要删掉。为了给 PostgreSQL 增加一点难度,我们不直接删除,而是再创建一个视图:
CREATE VIEW shoelace_candelete AS
SELECT * FROM shoelace_obsolete WHERE sl_avail = 0;
然后这样执行:
DELETE FROM shoelace WHERE EXISTS
(SELECT * FROM shoelace_candelete
WHERE sl_name = shoelace.sl_name);
Voilà:
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 | 6|brown | 60|cm | 60 sl4 | 8|black | 40|inch | 101.6 sl3 | 10|black | 35|inch | 88.9 sl8 | 21|brown | 40|inch | 101.6 sl10 | 1000|magenta | 40|inch | 101.6 sl5 | 4|brown | 1|m | 100 sl6 | 20|brown | 0.9|m | 90 (9 rows)
作用于一个视图上的 DELETE,其子查询条件总共使用了四个嵌套/连接的视图,其中一个视图自身又带有一个包含视图的子查询条件,并且还用到了计算得到的视图列,最终仍会被重写成单棵解析树,从真正的表中删除所请求的数据。
我想在现实世界中,大概只有很少的场景会需要这样的构造。但知道它确实能工作,总归让人安心。
实情是:. 在写这一章的过程中,我又发现了一个 bug。不过修完那个 bug 之后,这一切居然能工作,倒是让我有点惊讶。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。