Postgresql 中文操作指南

39.1. Overview of Trigger Behavior #

触发器是一项规范,即无论何时执行特定类型的操作,数据库都应该自动执行特定函数。触发器可以附加到表格(分区或非分区)、视图和外部表格上。

在表格和外部表格上,可以定义触发器在任何_INSERT_、_UPDATE_或_DELETE_操作之前或之后执行,无论是对每行的修改还是对 SQL 语句的修改。此外,可以将_UPDATE_触发器设置为仅在_UPDATE_语句的_SET_子句中提到特定列时才触发。触发器也可以触发_TRUNCATE_语句。如果触发事件发生,将在适当的时间调用触发器的函数来处理该事件。

在视图上,可以定义触发器在_INSERT_、UPDATE_或_DELETE_操作的替代操作中执行。对此类_INSTEAD OF_触发器,每个需要在视图中修改的行都会触发一次。有责任的触发器函数对视图的底层基本表格执行必要的修改,并在适当的情况下以它将在视图中显示的形式返回修改后的行。还可以定义视图上的触发器,使它们在 _INSERT、_UPDATE_或_DELETE_操作之前或之后针对每个 SQL 语句执行一次。然而,此类触发器仅在视图上还有_INSTEAD OF_触发器时才会触发。否则,任何针对视图的语句都必须重写成影响其底层基本表格的语句,然后将触发的触发器附加到基本表格上。

在创建触发器本身之前必须先定义触发器函数。触发器函数必须声明为不接收任何参数且返回类型_trigger_的函数。(触发器函数通过特殊传递的_TriggerData_结构接收其输入,而不是以常规函数参数的形式接收。)

一旦创建了合适的触发器函数,就使用 CREATE TRIGGER 建立该触发器。同一个触发器函数可用于多个触发器。

PostgreSQL 既提供 per-row 触发器,又提供 per-statement 触发器。对于每行触发器,触发器函数对于因引发触发器的语句而受到影响的每一行调用一次。相比之下,每语句触发器仅在执行适用的语句时调用一次,无论此语句影响多少行。具体来说,影响零行的语句仍将导致执行任何适用的每语句触发器。这两种类型的触发器有时分别称为 row-level 触发器和 statement-level 触发器。TRUNCATE 的触发器只能在语句级别定义,而不是每行。

触发器还会根据它们是在 beforeafter 还是 instead of 操作期间触发,对其进行分类。它们分别称为 BEFORE 触发器、AFTER 触发器和 INSTEAD OF 触发器。语句级别的 BEFORE 触发器自然会在语句开始执行任何操作之前触发,而语句级别的 AFTER 触发器则在语句的最末尾触发。这些类型的触发器可以在表、视图或外部表上定义。行级别的 BEFORE 触发器在对特定行进行操作之前立即触发,而行级别的 AFTER 触发器在语句末尾触发(但在任何语句级别的 AFTER 触发器之前)。这些类型的触发器仅可定义在表和外部表上,但不可定义在视图上。INSTEAD OF 触发器仅可定义在视图上,且仅可在行级别;当将视图中的每一行识别为需要进行操作时,它们会立即触发。

AFTER 触发器的执行可以推迟到事务末尾(而非语句末尾),如果它被定义为 constraint trigger。在所有情况下,触发器都是作为触发它的语句的同一事务的一部分执行的,因此,如果语句或触发器导致错误,两个错误的影响都会被回滚。

针对继承或分区层次结构中父表的一个语句不会导致受影响的子表的语句级别的触发器被触发;只有父表的语句级别触发器才会被触发。但是,任何受影响的子表的行级别的触发器都会被触发。

如果 INSERT 包含一个 ON CONFLICT DO UPDATE 子句,那么行级别的 BEFORE INSERT 触发器和行级别的 BEFORE UPDATE 触发器的影响都可以通过已更新行的最终状态来应用(如果一个 EXCLUDED 列被引用的话)。但是,这两组行级别的 BEFORE 触发器执行并不需要存在 EXCLUDED 列引用。如果两个 BEFORE INSERTBEFORE UPDATE 行级别的触发器更改插入/更新的行(即使修改或多或少等价,但也存在它们不幂等的问题),便应考虑出现意外结果的可能性。请注意,无条件 UPDATE 触发器在指定 ON CONFLICT DO 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 DELETEAFTER INSERT 触发器适用;但 AFTER UPDATE 触发器不适用,因为 UPDATE 已转换为 DELETEINSERT。就语句级别的触发器而言,即使发生行移动,也不触发任何 DELETEINSERT 触发器;仅 UPDATEUPDATE 语句中使用,在目标表上定义的触发器会被触发。

没有为 MERGE 定义单独的触发器。而是根据 (对于语句级别的触发器) MERGE 查询中指定的操作是什么,(对于行级别的触发器) 执行了什么操作,来触发语句级别的或行级别的 UPDATEDELETEINSERT 触发器。

在运行 MERGE 命令时,针对 MERGE 命令操作中指定事件的语句级别的 BEFOREAFTER 触发器会被触发,而不管是否最终执行操作。这与无条件 UPDATE 语句相同,尽管语句级别触发器被触发,但不会更新任何行。行级别的触发器仅在实际上更新、插入或删除行时触发。因此,对于某类动作,触发语句级别的触发器而没有行级别的触发器被触发时,这完全合法。

由每条语句触发器调用的触发器函数应始终返回 NULL。由每行触发器调用的触发器函数可以选择向调用执行器返回一个表行(类型为 HeapTuple 的一个值)。在操作之前触发的行级触发器有以下选择:

一个不打算导致这两种行为的行级 BEFORE 触发器在返回结果时必须小心,其返回的行和传递给触发器的行保持一致(即对于 INSERTUPDATE 触发器为 NEW 行,对于 DELETE 触发器为 OLD 行)。

一个行级 INSTEAD OF 触发器应返回 NULL 以指示它没有修改视图底层基本表的任何数据,或者它应该返回已传递的视图行(对于 INSERTUPDATE 操作的 NEW 行,或者对于 DELETE 操作的 OLD 行)。使用非空返回值来指示触发器在视图中执行了必要的数据修改。这将导致受命令影响的行数计数增加。仅对于 INSERTUPDATE 操作,触发器可以在返回 NEW 行之前对其进行修改。这将更改 INSERT RETURNINGUPDATE RETURNING 返回的数据,并在视图未能显示与提供数据完全相同的数据时很有用。

在操作之后触发的行级触发器的返回值将被忽略,因此它们可以返回 NULL

针对生成列有一些注意事项。存储的生成列是在 BEFORE 触发器之后和 AFTER 触发器之前计算的。因此,可以在 AFTER 触发器中检查生成的值。在 BEFORE 触发器中,如预期的那样,OLD 行包含旧的生成值,但 NEW 行还不包含新的生成值且不应访问。在 C 语言接口中,此时列的内容未定义;更高级的编程语言应防止在 BEFORE 触发器中的 NEW 行中访问存储的生成列。BEFORE 触发器中生成列值的更改将被忽略并会被覆盖。

如果在同一关系的同一起始事件上定义了多个触发器,那么将按照触发器名称的字母顺序触发触发器。对于 BEFOREINSTEAD OF 触发器,每个触发器返回的可能经过修改的行成为下一个触发器的输入。如果任何 BEFOREINSTEAD OF 触发器返回 NULL,则放弃对该行的操作,并且不会触发后续触发器(对于该行)。

触发器定义还可以指定一个布尔 WHEN 条件,将对其进行测试以查看是否应触发触发器。在行级触发器中,WHEN 条件可以检查该行的列的旧值和/或新值。(语句级触发器也可以有 WHEN 条件,尽管该特性对它们不是那么有用。)在 BEFORE 触发器中,WHEN 条件在函数被执行或将被执行之前进行评估,因此使用 WHEN 与在触发器函数开头测试相同条件并无实质性不同。但是,在 AFTER 触发器中,WHEN 条件将在行更新发生后立即进行评估,并且它确定是否将事件排队以便在语句末尾触发触发器。因此,当 AFTER 触发器的 WHEN 条件未返回 true 时,无需排队事件,也无需在语句末尾重新获取行。如果只针对一些行触发触发器,那么这将显着加快修改多行的语句的运行速度。INSTEAD OF 触发器不支持 WHEN 条件。

通常,行级 BEFORE 触发器用于检查或修改将要插入或更新的数据。例如,BEFORE 触发器可用于将当前时间插入 timestamp 列,或用于检查行的两个元素是否一致。 AFTER 行级触发器最明智的用途是将更新传播到其他表或对其他表执行一致性检查。进行这种劳动分工的原因是,AFTER 触发器可以确定它正在查看行的最终值,而 BEFORE 触发器则不能;它后面还有其他 BEFORE 触发器正在触发。如果您没有特定的理由使触发器变为 BEFOREAFTER,那么 BEFORE 的情况更高效,因为有关操作的信息不必保存到语句结束时。

如果触发器函数执行 SQL 命令,那么这些命令可能会再次触发触发器。这称为级联触发器。级联级别的数量没有直接限制。级联可能会导致对同一触发器进行递归调用;例如,INSERT 触发器可能会执行将附加行插入同一表的命令,导致再次触发 INSERT 触发器。在这样的情况下,触发器程序员有责任避免无限递归。

在定义触发器时,可以为其指定参数。在触发器定义中包含参数的目的是允许具有类似要求的不同触发器调用同一个函数。作为一个示例,可以有一个通用的触发器函数,以两个列名作为其参数,并将当前用户放入第一个参数,将当前时间戳放入另一个参数。如果编写正确,该触发器函数将独立于触发其的特定表。因此,同一个函数可用于任何表上的 INSERT 事件,以自动跟踪事务表中的记录创建,例如。如果定义为 UPDATE 触发器,该函数还可用于跟踪上次更新事件。

支持触发器的每种编程语言都有自己的方法可以将触发器输入数据提供给触发器函数。此输入数据包括触发器事件的类型(例如,INSERTUPDATE)以及 CREATE TRIGGER 中列出的任何参数。对于行级触发器,输入数据还包括 INSERTUPDATE 触发器的 NEW 行,和/或 UPDATEDELETE 触发器的 OLD 行。

在默认情况下,语句级触发器没有任何方法能够检查语句修改的各个行。但是 AFTER STATEMENT 触发器可以请求创建 transition tables,使受影响的行集合可用于触发器。AFTER ROW 触发器还可以请求转换表,以便既能看到表中的全部更改,又能看到当前正在为其触发的各行中的更改。再次检查转换表的方法取决于所使用的编程语言,但典型的方法是使转换表充当可通过触发器函数中发出的 SQL 命令访问的只读临时表。