Skip to content

39.1 触发器行为概述#

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

对于表和外部表,可以定义触发器在任何INSERTUPDATEDELETE操作之前或之后执行,每次修改一行执行一次,或每次SQL语句执行一次。此外,还可以将UPDATE触发器设置为仅在UPDATE语句的SET子句中提到某些列时才触发。触发器还可以针对TRUNCATE语句触发。如果触发器事件发生,则在适当的时间调用触发器的函数来处理该事件。

对于视图,可以定义触发器来代替INSERTUPDATEDELETE操作执行。此类INSTEAD OF触发器针对视图中需要修改的每一行触发一次。触发器的函数负责对视图的基础基本表执行必要的修改,并在适当的情况下返回修改后的行,该行将显示在视图中。还可以定义视图上的触发器,使其在INSERTUPDATEDELETE操作之前或之后每次SQL语句执行一次。但是,此类触发器仅在视图上还有INSTEAD OF触发器时才触发。否则,针对视图的任何语句都必须重写为影响其基础基本表的语句,然后触发的触发器将附加到基本表上。

必须先定义触发器函数,然后才能创建触发器本身。触发器函数必须声明为不带参数且返回类型为trigger的函数。(触发器函数通过特殊传递的TriggerData结构接收其输入,而不是以普通函数参数的形式接收。)

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

PostgreSQL同时提供逐行触发器和逐语句触发器。对于逐行触发器,触发器函数将针对触发触发器的语句影响的每一行调用一次。相反,无论该语句影响的行数如何,逐语句触发器仅在执行适当语句时调用一次。特别是,影响零行的语句仍将导致执行任何适用的逐语句触发器。这两种类型的触发器有时分别称为行级触发器和语句级触发器。在TRUNCATE上的触发器只能在语句级别定义,不能逐行定义。

触发器还根据它们在操作之前之后代替操作触发进行分类。这些分别称为BEFORE触发器、AFTER触发器和INSTEAD OF触发器。语句级BEFORE触发器在语句开始执行任何操作之前触发,而语句级AFTER触发器在语句的最后触发。这些类型的触发器可以在表、视图或外部表上定义。行级BEFORE触发器在对特定行进行操作之前立即触发,而行级AFTER触发器在语句结束时触发(但在任何语句级AFTER触发器之前)。这些类型的触发器只能在表和外部表上定义,不能在视图上定义。INSTEAD OF触发器只能在视图上定义,并且只能在行级别定义;当视图中的每一行被识别为需要进行操作时,它们会立即触发。

如果AFTER触发器被定义为约束触发器,则可以将其执行推迟到事务结束,而不是语句结束。在所有情况下,触发器作为触发它的语句的同一事务的一部分执行,因此如果语句或触发器导致错误,则两者的影响都将回滚。

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

如果INSERT包含ON CONFLICT DO UPDATE子句,则有可能以从更新行的最终状态中显而易见的方式应用行级BEFORE``INSERT触发器和行级BEFORE``UPDATE触发器的效果,如果引用了EXCLUDED列。但是,对于两组行级BEFORE触发器执行,不需要有EXCLUDED列引用。当存在BEFORE``INSERTBEFORE``UPDATE行级触发器更改正在插入/更新的行时,应考虑出现意外结果的可能性(即使修改或多或少是等效的,如果它们不是幂等的,这也会造成问题)。请注意,无论UPDATE是否影响任何行(以及是否曾经采用替代UPDATE路径),当指定ON CONFLICT DO 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触发器;只有在UPDATE语句中使用的目标表上定义的UPDATE触发器才会触发。

没有为MERGE定义单独的触发器。相反,语句级或行级UPDATEDELETEINSERT触发器会根据(对于语句级触发器)MERGE查询中指定的操作以及(对于行级触发器)执行的操作来触发。

在运行MERGE命令时,会针对MERGE命令操作中指定事件触发语句级BEFOREAFTER触发器,无论最终是否执行操作。这与不会更新任何行的UPDATE语句相同,但会触发语句级触发器。仅当实际更新、插入或删除行时,才会触发行级触发器。因此,虽然会针对某些类型的操作触发语句级触发器,但不会针对同类操作触发任何行级触发器,这是完全合法的。

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

  • 它可以返回 NULL 以跳过当前行的操作。这指示执行器不执行触发器调用的行级操作(特定表行的插入、修改或删除)。

  • 仅对于行级 INSERTUPDATE 触发器,返回的行将成为将要插入或将替换正在更新的行。这允许触发器函数修改正在插入或更新的行。

不打算导致上述任一行为的行级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触发器可以请求创建转换表,以便向触发器提供受影响的行集。AFTER ROW触发器也可以请求转换表,以便它们可以看到表中的总变化以及当前正在触发的各个行中的变化。检查转换表的方法再次取决于所使用的编程语言,但典型的方法是使转换表充当可通过触发器函数中发出的 SQL 命令访问的只读临时表。