一、触发器行为概述
一个触发器声明了当执行一种特定类型的操作时数据库应该自动执行一个特殊的函数。触发器可以被附加到表(分区的或者不分区的)、视图和外部表。
在表和外部表上,触发器可以被定义为在 INSERT
、UPDATE
或 DELETE
操作之前或之后被执行, 可以为每个SQL语句被执行一次或者为每个修改的行 被执行一次。UPDATE
触发器可以进一步地设置为只针对UPDATE
语句的SET
子句的特定列出发。触发器也可以被 TRUNCATE
语句触发。如果一个触发器事件发生, 触发器函数会在适当的事件被调用来处理该事件。
在视图上,触发器可以被定义来取代INSERT
、UPDATE
或 DELETE
操作的执行。这种INSTEAD OF
触发器对视图中需要被修改的每一行触发一次。触发器函数的职责是对视图的底层基本表执行必要的修改,并且在合适的时候返回被修改的行以便显示在视图中。视图上的触发器也可以被定义为对每个SQL语句执行一次,在INSERT
\UPDATE
或DELETE
操作之前或之后。不过,只有在该视图上还有一个INSTEAD OF
触发器时,上述那些触发器才会被触发。否则,以该视图为目标的任何语句都必须被重写成一个影响其底层基表的语句,然后附着在那些基表上的触发器将会被引发。
触发器函数必须在触发器本身被创建之前被定义好。触发器函数必须被定义成一个没有参数的函数,并且返回类型为trigger
(触发器函数通过一个特殊传递的TriggerData
结构作为其输入,而不是以普通函数参数的形式)。
一旦一个合适的触发器函数被创建,就可以使用CREATE TRIGGER建立触发器。同一个触发器函数可以被用于多个触发器。
PostgreSQL同时提供每行的触发器和每语句的触发器。对于一个每行的触发器,对于触发触发器的语句所修改的每一行都会调用一次触发器函数。相反,一个每语句的触发器对于其触发语句只被调用一次,而不管该语句影响了多少行。特别地,一个不影响任何行的语句仍然会导致任何可用每语句的触发器的执行。这两类触发器有时也分别被称作行级触发器和语句级触发器。TRUNCATE
上的触发器只能被定义在语句级。
触发器也可以根据它们是否在操作之前、之后触发,或者被触发来取代操作来分类。它们分别指BEFORE
触发器、AFTER
触发器以及INSTEAD OF
触发器。语句级BEFORE
触发器在语句开始做任何事情之前被触发,而语句级AFTER
触发器则在语句做完所有事情之后被触发。这些触发器类型可以被定义在表、视图或外部表上。行级BEFORE
触发器在每一个行被操作之前被触发,而行级AFTER
触发器在语句结束之后被触发(但在任何语句级AFTER
触发器之前)。这些触发器类型只能被定义在非分区表和外部表上,但不能定义在视图上。INSTEAD OF
触发器只能被定义在视图上,并且只能定义在行级,当视图中的每一行被标识为需要被操作时,它们会立即触发。
一个以继承或者分区层次中父表为目标的语句不会导致受影响的子表的语句级触发器被引发,只有父表的语句级触发器会被引发。不过,受影响的子表的行级触发器将被引发。
如果一个INSERT
包含ON CONFLICT DO UPDATE
子句并且引用了EXCLUDED
列,有可能所有行级 BEFORE
INSERT
触发器和所有行级 BEFORE
UPDATE
触发器的效果可能会以一种对于 被更新行最终状态透明的方式被应用。不过,对于要执行的两种集合的行级 BEFORE
触发器都不需要有EXCLUDED
列引用。当同时有行级 BEFORE
INSERT
和 BEFORE
UPDATE
触发器影响被插入/ 更新的行时(如果在两者不幂等时修改或多或少地等价,这仍可能是有问题的), 应该考虑可能出现的意料之外的结果。注意在指定了 ON CONFLICT DO UPDATE
时,不管有没有行被 UPDATE
影响(并且不管是否采用了其他 UPDATE
路径),语句级 UPDATE
都将被执行。一个带有 ON CONFLICT DO UPDATE
子句的INSERT
将首先执行语句级BEFORE
INSERT
, 然后执行语句级BEFORE
UPDATE
触发器, 接着是语句级AFTER
UPDATE
触发器, 最后是语句级AFTER
INSERT
触发器。
如果一个分区表上的UPDATE
导致一行移动到另一个分区,它将被从原始分区DELETE
掉然后再INSERT
到新分区中。在这种情况下,原始分区上所有的行级BEFORE
UPDATE
触发器和所有行级BEFORE
DELETE
触发器会被引发。然后目标分区上所有的行级BEFORE
INSERT
触发器会被引发。当所有这些触发器都影响被移动的行时,应该对令人惊讶的结果有心理准备。至于AFTER ROW
触发器,AFTER
DELETE
和AFTER
INSERT
触发器会被应用,但AFTER
UPDATE
触发器不会被应用,因为UPDATE
已经被转换成了一个DELETE
和一个INSERT
。对于语句级触发器,即便发生行移动,DELETE
和INSERT
触发器也都不会被引发,只有UPDATE
语句中用到的目标表上的UPDATE
触发器将被引发。
被语句级触发器调用的触发器函数应该总是返回NULL
。根据行级触发器的选择,被其调用的触发器函数可以返回一个表行(类型HeapTuple
的一个值)给执行器。在一个操作前触发的行级触发器有下列选择:
-
它可以返回
NULL
来跳过对当前行的操作。这指示执行器不要执行调用触发器的行级操作(对一个特定表行的插入、修改或删除)。 -
仅对行级
INSERT
和UPDATE
触发器来说,被返回的行称为将要被插入的行或者替代将被更新的行。这允许触发器函数修改将要被插入或更新的行。
一个无意导致任何这些行为的行级BEFORE
触发器必须小心地它的结果,使之和被传入的行一样(即,INSERT
和UPDATE
触发器的NEW
行,DELETE
触发器的OLD
行)。
一个行级INSTEAD OF
触发器可以返回NULL
来指示它没有修改任何来自于视图底层基表的数据,也可以返回被传入的视图行(INSERT
和UPDATE
操作的NEW
行,或者DELETE
操作的OLD
行)。一个非空返回值被用于标志触发器在视图中执行了必须的数据修改。这将会导致被命令修改的行计数被增加。对于INSERT
和UPDATE
操作,触发器可能会在返回NEW
行之前对其进行修改。这将会改变INSERT RETURNING
或UPDATE RETURNING
返回的数据,并在视图无法正确地显示提供给它的相同数据时有用。
对于在一个操作之后触发的行级触发器,返回值会被忽略,因此它们可以返回NULL
。
如果为同一个关系上的同一事件定义了超过一个触发器,它们将按照其名称的字母表顺序被触发。在BEFORE
和INSTEAD OF
触发器的情况下,每一个触发器返回的可能被修改的行将成为下一个触发器的输入。如果任何一个BEFORE
或INSTEAD OF
触发器返回NULL
,该操作将在该行上被禁用并且对于该行不会触发后续的触发器。
一个触发器定义也能指定一个布尔的WHEN
条件,它将被测试来看该触发器是否应该被触发。在行级触发器中,WHEN
条件可以检查该行的旧列值和/或新列值(语句级触发器也能有WHEN
条件,但是该特性对它们不太有用)。在一个BEFORE
触发器中,WHEN
条件只是在该函数被或者将被执行前计算,因此使用WHEN
条件与在该触发器函数的开始测试相同的条件没有本质区别。不过,在一个AFTER
触发器中,WHEN
条件只是在行更新发生之后被计算,并且它决定在语句的末尾一个事件是否被排队来触发该触发器。因此当一个AFTER
触发器的WHEN
不返回真时,在语句的末尾没有必要将一个事件进行排队,也没有必要重新取出该行。如果触发器只对少数行触发,这可以使得修改很多行的语句明显加快。INSTEAD OF
触发器不支持WHEN
条件。
通常,行级BEFORE
被用来检查或修改即将被插入或更新的数据。例如,一个BEFORE
触发器可以被用来把当前时间插入到一个timestamp
列中,或者检查该行的两个元素之间是否一致。行级AFTER
触发器大多数被用来将更新传播到其他表,或者针对其他表进行一致性检查。进行这种工作分工的原因是,一个AFTER
触发器可以肯定它看到的是该行的最终值,而一个BEFORE
触发器则不能,因为还可能有其他BEFORE
触发器在它之后触发。如果你不知道让一个触发器是BEFORE
或AFTER
,则BEFORE
形式更加有效,因为关于该操作的信息直到语句的末尾都不需要被保存。
如果一个触发器函数执行 SQL 命令,则这些命令可能会再次引发触发器。这就是所谓的级联触发器。对于级联的层数没有直接的限制。级联有可能会导致对同一个触发器的递归调用。例如,一个INSERT
触发器可能执行一个向同一个表插入一个额外行的命令,这就导致该INSERT
触发器被再次引发。所以在这种情形下,触发器程序员应该负责避免无限递归。
在定义一个触发器时,可以为它指定参数。在触发器定义中包括参数的目的是允许具有相似需求的不同触发器调用同一个函数。例如,可能有一个一般性的触发器函数,它需要两个列名作为参数,一个放当前用户而另一个放当前时间戳。在正确编写的情况下,这个触发器函数应该独立于它所触发的表。因此同一个函数可以被用于具有适当列的任意表上的INSERT
事件,这样做的用途之一是可以自动追踪一个交易表中记录的创建。如果被定义成一个UPDATE
触发器,它也可以被用来追踪最新的更新事件。
每一种支持触发器的编程语言都有自己的方法来让触发器输入数据对触发器函数可用。这种输入数据包括触发器事件的类型(如INSERT
或UPDATE
)以及被列在CREATE TRIGGER
中的任何参数。对于一个行级触发器,输入数据还包括用于INSERT
和UPDATE
触发器的NEW
行,和/或用于UPDATE
和DELETE
触发器的OLD
行。语句级触发器当前没有任何方法检查被语句修改的单个行。
默认情况下,语句级触发器没有办法检查该语句修改的行。但是AFTER STATEMENT
触发器可以请求创建传递表,这样可以让受影响的行集合对该触发器可用。AFTER ROW
触发器也可以请求传递表,这样它们可以看到表中的整个变化,同时也能看到当前引发它们的个体行中的变化。检查传递表的方法仍是取决于使用的编程语言,但是通常的方法让传递表变得像触发器函数内部发出的SQL命令能够访问的只读临时表一样。
二、数据改变的可见性
如果你在你的触发器函数中执行 SQL 命令,并且这些命令会访问触发器所在的表,那么你需要注意数据可见性规则。因为这些规则决定了这些 SQL 命令是否将能看见引发触发器的数据改变。简单地:
-
语句级触发器遵循简单的可见性规则:一个语句所作的改变对于语句级
BEFORE
触发器都不可见,而所有修改对于语句级AFTER
触发器都是可见的。 -
导致触发器被引发的数据更改(插入、更新或删除)自然对于在一个行级
BEFORE
触发器中执行的 SQL 命令不可见,因为它还没有发生。 -
但是,在一个行级
BEFORE
触发器中执行的 SQL 命令将会看见之前在同一个外层命令中所作的数据更改的效果。这里需要小心,因为这些更改时间的顺序通常是不可预测的,一个影响多行的 SQL 命令可能以任何顺序访问这些行。 -
类似地,一个行级
INSTEAD OF
触发器将会看见之前在同一个外层命令中INSTEAD OF
触发器引发所作的数据更改。 -
当一个行级
AFTER
触发器被引发时,所有由外层命令所作的数据更改已经完成,并且对于该被调用的触发器函数是可见的。
如果你的触发器函数使用任何一种标准过程语言编写的,那么只有在该函数被声明为VOLATILE
时上述陈述才适用。被声明为STABLE
或IMMUTABLE
的函数在任何情况下将不能看到由调用命令所作出的更改。
三、事件触发器
为了对上述触发器中讨论的触发器机制加以补充, PostgreSQL也提供了事件触发器。和常规触发器(附着在 一个表上并且只捕捉 DML 事件)不同,事件触发器对一个特定数据库来说是全局 的,并且可以捕捉 DDL 事件。
和常规触发器相似,可以用任何包括了事件触发器支持的过程语言或者 C 编写 事件触发器,但是不能用纯 SQL 编写。
四、事件触发器行为总览
只要与一个事件触发器相关的事件在事件触发器所在的数据库中发生, 该事件触发器就会被引发。当前支持的事件是 ddl_command_start
、ddl_command_end
、 table_rewrite
和sql_drop
。未来的发行版 中可能会增加对更多事件的支持。
ddl_command_start
事件就在CREATE
、 ALTER
、DROP
、SECURITY LABEL
、 COMMENT
、GRANT
或者REVOKE
命令的执行之前发生。在事件触发器引发前不会做受影响对象是否存在的检查。 不过,一个例外是,这个事件不会为目标是共享对象 — 数据库、角色 以及表空间 — 的 DDL 命令发生,也不会为目标是事件触发器的 DDL 命令发生。事件触发器机制不支持这些对象类型。 ddl_command_start
也会在SELECT INTO
命令的执行之前发生,因为这等价于 CREATE TABLE AS
。
ddl_command_end
事件就在同一组命令的执行之后发生。为了 得到发生的DDL操作的更多细节,可以从 ddl_command_end
事件触发器代码中使用集合返回函数 pg_event_trigger_ddl_commands()
。注意该触发器是在那些动作 已经发生之后(但是在事务提交前)引发,并且因此系统目录会被读作已更改。
sql_drop
事件为任何删除数据库对象的操作在 ddl_command_end
事件触发器之前发生。要列出已经被删除的 对象,可以从sql_drop
事件触发器代码中使用集合返回函数 pg_event_trigger_dropped_objects()
。注意该触发器是在对象已经 从系统目录删除以后执行,因此不能再查看它们。
table_rewrite
事件在表被命令ALTER TABLE
和 ALTER TYPE
的某些动作重写之前发生。虽然其他控制语句(例如 CLUSTER
和VACUUM
)也可以用来重 写表,但是它们不会触发table_rewrite
事件。
不能在一个中止的事务中执行事件触发器(其他函数也一样)。因此,如果一个 DDL 命令出现错误失败,将不会执行任何相关的 ddl_command_end
触发器。反过来,如果一个 ddl_command_start
触发器出现错误失败,将不会引发进一步的 事件触发器,并且不会尝试执行该命令本身。类似地,如果一个 ddl_command_end
触发器出现错误失败,DDL 命令的效果将被 回滚,就像其他包含事务中止的情况中那样。
事件触发器通过命令CREATE EVENT TRIGGER创建。为了 创建一个事件触发器,你必须首先创建一个有特殊返回类型 event_trigger
的函数。这个函数不一定需要返回一个值, 该返回类型仅仅是作为一种信号表示该函数要被作为一个事件触发器调用。
如果对于一个特定的事件定义了多于一个事件触发器,它们将按照触发器名称 的字母表顺序被引发。
一个触发器定义也可以指定一个WHEN
条件,这样事件触 发器(例如ddl_command_start
触发器)就可以只对用户 希望介入的特定命令触发。这类触发器的通常用法是用于限制用户可能执行的 DDL 操作的范围。
五、事件触发器触发矩阵
表5.1列出了所有命令的 事件触发器支持情况。
表 5.1. 支持事件触发器的命令标签
命令标签 | ddl_command_start | ddl_command_end | sql_drop | table_rewrite | 注解 |
---|---|---|---|---|---|
ALTER AGGREGATE | X | X | - | - | |
ALTER COLLATION | X | X | - | - | |
ALTER CONVERSION | X | X | - | - | |
ALTER DOMAIN | X | X | - | - | |
ALTER EXTENSION | X | X | - | - | |
ALTER FOREIGN DATA WRAPPER | X | X | - | - | |
ALTER FOREIGN TABLE | X | X | X | - | |
ALTER FUNCTION | X | X | - | - | |
ALTER LANGUAGE | X | X | - | - | |
ALTER OPERATOR | X | X | - | - | |
ALTER OPERATOR CLASS | X | X | - | - | |
ALTER OPERATOR FAMILY | X | X | - | - | |
ALTER POLICY | X | X | - | - | |
ALTER SCHEMA | X | X | - | - | |
ALTER SEQUENCE | X | X | - | - | |
ALTER SERVER | X | X | - | - | |
ALTER TABLE | X | X | X | X | |
ALTER TEXT SEARCH CONFIGURATION | X | X | - | - | |
ALTER TEXT SEARCH DICTIONARY | X | X | - | - | |
ALTER TEXT SEARCH PARSER | X | X | - | - | |
ALTER TEXT SEARCH TEMPLATE | X | X | - | - | |
ALTER TRIGGER | X | X | - | - | |
ALTER TYPE | X | X | - | X | |
ALTER USER MAPPING | X | X | - | - | |
ALTER VIEW | X | X | - | - | |
CREATE AGGREGATE | X | X | - | - | |
COMMENT | X | X | - | - | 只对本地对象 |
CREATE CAST | X | X | - | - | |
CREATE COLLATION | X | X | - | - | |
CREATE CONVERSION | X | X | - | - | |
CREATE DOMAIN | X | X | - | - | |
CREATE EXTENSION | X | X | - | - | |
CREATE FOREIGN DATA WRAPPER | X | X | - | - | |
CREATE FOREIGN TABLE | X | X | - | - | |
CREATE FUNCTION | X | X | - | - | |
CREATE INDEX | X | X | - | - | |
CREATE LANGUAGE | X | X | - | - | |
CREATE OPERATOR | X | X | - | - | |
CREATE OPERATOR CLASS | X | X | - | - | |
CREATE OPERATOR FAMILY | X | X | - | - | |
CREATE POLICY | X | X | - | - | |
CREATE RULE | X | X | - | - | |
CREATE SCHEMA | X | X | - | - | |
CREATE SEQUENCE | X | X | - | - | |
CREATE SERVER | X | X | - | - | |
CREATE STATISTICS | X | X | - | - | |
CREATE TABLE | X | X | - | - | |
CREATE TABLE AS | X | X | - | - | |
CREATE TEXT SEARCH CONFIGURATION | X | X | - | - | |
CREATE TEXT SEARCH DICTIONARY | X | X | - | - | |
CREATE TEXT SEARCH PARSER | X | X | - | - | |
CREATE TEXT SEARCH TEMPLATE | X | X | - | - | |
CREATE TRIGGER | X | X | - | - | |
CREATE TYPE | X | X | - | - | |
CREATE USER MAPPING | X | X | - | - | |
CREATE VIEW | X | X | - | - | |
DROP AGGREGATE | X | X | X | - | |
DROP CAST | X | X | X | - | |
DROP COLLATION | X | X | X | - | |
DROP CONVERSION | X | X | X | - | |
DROP DOMAIN | X | X | X | - | |
DROP EXTENSION | X | X | X | - | |
DROP FOREIGN DATA WRAPPER | X | X | X | - | |
DROP FOREIGN TABLE | X | X | X | - | |
DROP FUNCTION | X | X | X | - | |
DROP INDEX | X | X | X | - | |
DROP LANGUAGE | X | X | X | - | |
DROP OPERATOR | X | X | X | - | |
DROP OPERATOR CLASS | X | X | X | - | |
DROP OPERATOR FAMILY | X | X | X | - | |
DROP OWNED | X | X | X | - | |
DROP POLICY | X | X | X | - | |
DROP RULE | X | X | X | - | |
DROP SCHEMA | X | X | X | - | |
DROP SEQUENCE | X | X | X | - | |
DROP SERVER | X | X | X | - | |
DROP STATISTICS | X | X | X | - | |
DROP TABLE | X | X | X | - | |
DROP TEXT SEARCH CONFIGURATION | X | X | X | - | |
DROP TEXT SEARCH DICTIONARY | X | X | X | - | |
DROP TEXT SEARCH PARSER | X | X | X | - | |
DROP TEXT SEARCH TEMPLATE | X | X | X | - | |
DROP TRIGGER | X | X | X | - | |
DROP TYPE | X | X | X | - | |
DROP USER MAPPING | X | X | X | - | |
DROP VIEW | X | X | X | - | |
GRANT | X | X | - | - | 只对本地对象 |
IMPORT FOREIGN SCHEMA | X | X | - | - | |
REVOKE | X | X | - | - | 只对本地对象 |
SECURITY LABEL | X | X | - | - | 只对本地对象 |
SELECT INTO | X | X | - | - |