INSERT、UPDATE和DELETE规则 #定义在INSERT、UPDATE和DELETE上的规则与前几节描述的视图规则有明显的不同。首先,它们的CREATE RULE命令允许更多:
它们可以没有动作。
它们可以有多个动作。
它们可以是INSTEAD或ALSO(缺省)。
伪关系NEW和OLD可以派上用场。
它们可以有规则条件。
第二,它们不是就地修改查询树,而是创建零棵或多棵新查询树,并且可能丢弃原始查询树。
在很多情况下,使用INSERT/UPDATE/DELETE规则完成的任务,用触发器会做得更好。触发器在写法上稍微复杂一些,但它们的语义要简单得多。当原始查询包含易变函数时,规则往往会产生出人意料的结果:在执行规则的过程中,易变函数的执行次数可能比预期更多。
此外,还有一些情况根本无法由这些类型的规则支持,尤其是原始查询中包含WITH子句,或者UPDATE查询的SET列表中存在多重赋值的子SELECT。这是因为把这些结构复制到规则查询中会导致子查询被多次求值,这与查询作者的明确意图相违背。
记住以下语法:
CREATE [ OR REPLACE ] RULEnameAS ONeventTOtable[ WHEREcondition] DO [ ALSO | INSTEAD ] { NOTHING |command| (command;command... ) }
在后文中,更新规则是指定义在INSERT、UPDATE或DELETE上的规则。
当查询树的结果关系和命令类型分别等于CREATE RULE命令中给出的对象和事件时,规则系统就会应用更新规则。对于更新规则,规则系统会创建一个查询树列表,初始时该列表为空。动作可以有零个(关键字NOTHING)、一个或多个。为简化说明,我们先看只有一个动作的规则。这个规则可以有条件,也可以没有条件;它还可以是INSTEAD或ALSO(默认值)。
什么是规则条件?它是一种限制,用来说明何时执行规则动作、何时不执行。这个条件只能引用伪关系NEW和/或OLD,它们基本上表示作为对象给出的那个关系,只是带有特殊含义。
因此,对于单动作规则,生成的查询树会有以下三种情况。
ALSO或INSTEAD规则动作的查询树,再附加原始查询树的条件
ALSO规则动作的查询树,再附加规则条件和原始查询树的条件
INSTEAD规则动作的查询树,再附加规则条件和原始查询树的条件;以及附加了规则条件取反后的原始查询树
最后,如果规则是ALSO,则未修改的原始查询树也会被加入列表。由于只有带条件的INSTEAD规则才会额外加入原始查询树,因此对于单动作规则,最终会得到一棵或两棵输出查询树。
对于ON INSERT规则,原始查询(如果未被INSTEAD抑制)会先于规则添加的任何动作执行。这样一来,这些动作就能看到被插入的行。但对ON UPDATE和ON DELETE规则,原始查询会在规则添加的动作之后执行。这就保证了这些动作能够看到将被更新或删除的行;否则,动作可能什么也做不了,因为它们找不到符合条件的行。
由规则动作生成的查询树会再次送入重写系统,随后可能还会有更多规则被应用,从而产生更多或更少的查询树。因此,一个规则的动作必须具有不同的命令类型,或者具有与该规则自身不同的结果关系。否则,这种递归过程就会陷入无限循环。(规则的递归展开会被检测出来,并作为错误报告。)
pg_rewrite系统目录中动作里的查询树只是模板。由于它们可以引用NEW和OLD的范围表项,因此在使用前必须进行一些替换。对任何NEW的引用,都会先在原始查询的目标列表中查找相应项。如果找到了,就用该项的表达式替换该引用。否则,NEW就与OLD含义相同(对于UPDATE),或者被替换为一个空值(对于INSERT)。任何对OLD的引用,都会被替换为对结果关系对应范围表项的引用。
在系统完成应用更新规则后,它再应用视图规则到生成的查询树上。视图无法插入新的更新动作,所以没有必要向视图重写的输出应用更新规则。
假设我们想跟踪shoelace_data关系中sl_avail列的变化。因此,我们建立一个日志表和一条规则,使其在shoelace_data上执行UPDATE时,有条件地写入一条日志记录。
CREATE TABLE shoelace_log (
sl_name text, -- 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
);
现在有人执行了:
UPDATE shoelace_data SET sl_avail = 6 WHERE sl_name = 'sl7';
然后我们查看日志表:
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 shoelace_data.sl_name = 'sl7';
此时存在一条ON UPDATE规则log_shoelace,它带有如下规则条件表达式:
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命令查询树中的变量能够引用它们。)
该规则是一条带条件的ALSO规则,因此规则系统必须返回两棵查询树:修改后的规则动作,以及原始查询树。第 1 步中,原始查询的范围表会并入规则动作的查询树,得到:
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 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 new.sl_avail <> old.sl_avail
AND 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 6 <> old.sl_avail
AND 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 6 <> shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
至此就完成了。由于规则是ALSO,我们还要输出原始查询树。简而言之,规则系统输出的是一个包含两棵查询树的列表,它们对应于以下语句:
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';
而该条件永远不可能为真。
如果原始查询修改多行,这种机制同样能够正常工作。例如,假设有人发出如下命令:
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;
如果现在某人尝试对视图关系shoe执行这些操作,规则系统就会应用这些规则。由于这些规则没有动作而且是INSTEAD,生成的查询树列表将为空;整个查询也就什么都不会做,因为规则系统处理完后已经没有任何内容可供优化或执行。
另一种更完善的做法,是创建一些规则,把查询树重写成在真实表上执行正确操作的查询树。要在视图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;
如果你想在视图上支持RETURNING查询,就需要让规则包含计算视图行的RETURNING子句。对于单表视图,这通常很简单;但对于像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
)
RETURNING
shoelace_data.*,
(SELECT shoelace_data.sl_len * u.un_fact
FROM unit u WHERE shoelace_data.sl_unit = u.un_name);
请注意,这一条规则同时支持该视图上的INSERT和INSERT RETURNING查询;对于普通INSERT,RETURNING子句会被简单忽略。
请注意,在规则的RETURNING子句中,OLD和NEW指的是作为额外范围表项加入重写后查询中的伪关系,而不是结果关系里的旧行或新行。因此,举例来说,在一条支持该视图上UPDATE查询的规则中,如果RETURNING子句包含old.sl_name,那么返回的总会是旧名称,而不管视图上的查询在其RETURNING子句中指定的是OLD还是NEW;这可能会让人困惑。为了避免这种混淆,并支持在视图上的查询中返回旧值和新值,规则定义里的RETURNING子句应当引用结果关系中的项,例如shoelace_data.sl_name,而不要显式指定OLD或NEW。
现在假设商店偶尔会收到一大包鞋带,并附带一份很长的零件清单。但你并不想每次都手工更新shoelace视图。于是我们设置两个小表:一个用来插入零件清单中的项目,另一个则带着一个特殊花招。它们的创建命令如下:
CREATE TABLE shoelace_arrive (
arr_name text,
arr_quant integer
);
CREATE TABLE shoelace_ok (
ok_name text,
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;
现在你可以把零件清单中的数据填入表shoelace_arrive:
SELECT * FROM shoelace_arrive; arr_name | arr_quant ----------+----------- sl3 | 10 sl6 | 20 sl8 | 20 (3 rows)
快速查看当前数据:
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)
现在将到达的鞋带放入:
INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;
并检查结果:
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) 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 = 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 shoelace.sl_name = shoelace_arrive.arr_name;
同时会丢弃针对shoelace_ok的原始INSERT。这个重写后的查询会再次交给规则系统,而接下来应用的规则shoelace_upd会生成:
UPDATE shoelace_data
SET sl_name = shoelace.sl_name,
sl_avail = 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 shoelace_data
WHERE shoelace.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = shoelace.sl_name;
这同样是一条INSTEAD规则,因此前一个查询树会被丢弃。注意,这个查询仍然使用视图shoelace。但规则系统尚未完成,因此会继续在其上应用_RETURN规则,于是得到:
UPDATE shoelace_data
SET sl_name = s.sl_name,
sl_avail = 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 shoelace_data,
shoelace old, shoelace new,
shoelace_data s, unit u
WHERE s.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = s.sl_name;
最后,规则log_shoelace被应用,生成额外的查询树:
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_ok shoelace_ok,
shoelace_ok old, shoelace_ok new,
shoelace shoelace, shoelace old,
shoelace new, shoelace_data shoelace_data,
shoelace old, shoelace new,
shoelace_data s, unit u,
shoelace_data old, shoelace_data new
shoelace_log shoelace_log
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;
到这里,规则系统已经没有更多规则可用,并返回生成的查询树。
因此,我们最终得到两棵查询树,它们等效于以下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规则系统及其威力的最后演示。假设你向数据库中加入了一些颜色特别的鞋带:
INSERT INTO shoelace VALUES ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
INSERT INTO shoelace VALUES ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);
我们想建立一个视图,用来检查哪些shoelace项在颜色上与任何鞋子都不匹配。这个视图是:
CREATE VIEW shoelace_mismatch AS
SELECT * FROM shoelace WHERE NOT EXISTS
(SELECT shoename FROM shoe WHERE slcolor = sl_color);
它的输出是:
SELECT * FROM shoelace_mismatch; 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
现在我们想把它设置成这样:凡是不匹配且没有库存的鞋带,都要从数据库中删除。为了让PostgreSQL多费一点工夫,我们不直接删除它们,而是再创建一个视图:
CREATE VIEW shoelace_can_delete AS
SELECT * FROM shoelace_mismatch WHERE sl_avail = 0;
然后这样做:
DELETE FROM shoelace WHERE EXISTS
(SELECT * FROM shoelace_can_delete
WHERE sl_name = shoelace.sl_name);
结果如下:
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,其子查询条件总共使用了四个嵌套/连接的视图,其中一个视图自身又带有一个包含视图的子查询条件,并且还用到了计算得到的视图列,最终仍会被重写成单棵查询树,从真正的表中删除所请求的数据。
在现实世界中,大概只有很少的场景会需要这样的构造。但知道它确实能工作,总归让人安心。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。