前言
本文简单介绍 PostgreSQL 数据库的 Trigger 从创建、存储、触发、执行、修改,到删除的过程,贯穿 Trigger 的一生。
文中引用的函数、结构体来源于 PG 14 源码,分支为 REL_14_STABLE,对应的 commit id 如下。此外还引用了 PG 14 官方文档。
commit be0b0528cb64d49750fcb632faa2cfcd8d920be2
Author: Tom Lane <tgl@sss.pgh.pa.us>
Date: Fri Sep 9 15:34:04 2022 -0400
Fix possible omission of variable storage markers in ECPG.
触发器简介
Trigger 即触发器,它可以在特定事件发生时,对数据库中的对象执行特定操作:
-
这里的 事件可以是向某张表插入、更新、删除数据,也可以是执行某个 DDL 语句
-
触发后执行的 操作可以是插入、更新、删除、查询数据等。
根据触发事件的不同,PG 的触发器分为两类:
-
Trigger,普通的触发器,通过 DML 操作触发,比如表上的插入、更新、删除数据的操作
-
Event Trigger,事件触发器,通过 DDL 等事件触发,比如创建表、删除表等操作
另外,不同数据库中触发器的分类有所不同,比如 Oracle 分为 DML Trigger 和 System Trigger,SQL Server 分为 DML Trigger、DDL Trigger 和 Login Trigger,不论其如何划分,多数都可以与 PG 的触发器对应上。
创建触发器
语法
首先介绍创建触发器的 SQL 和 PLpgSQL 语法。
Trigger
根据 PG 官方文档,创建 Trigger的语法如下:
CREATE [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
ON table_name
[ FROM referenced_table_name ]
[ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
[ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )
event可以是下列之一:
INSERT
UPDATE [ OF column_name [, ... ] ]
DELETE
TRUNCATE
-
触发器可以定义在 INSERT、UPDATE、DELETE、TRUNCATE 事件上;可以在事件发生前、发生后触发,还可以取代事件(INSTEAD OF);可通过 WHEN condition 指定触发条件
-
行级触发器(ROW )对一次 DML 操作涉及的每一行都触发, 语句级触发器( STATEMENT)对一次 DML 只触发一次
-
若将触发器定义成 约束(Constraint)触发器,可设置触发时机为可延迟(DEFERRABLE)
-
触发器需要创建在表(或者视图)上, 依赖于表而存在。也正因为如此,触发器本身不属于任何模式(schema),因此不可在创建触发器指定 schema
-
触发以后需要执行的操作由 EXECUTE 语句的函数(或者存储过程,为方便描述,统一称为函数)决定,该函数通常由用户自定义, 返回值类型必须为 trigger
-
OLD、NEW 为上下文信息,分别对应事件发生前和发生后的表,以 UPDATE 操作为例,OLD.a 表示被更新的行中 a 列的旧值,NEW.a 则表示更新后的新值。需要注意的是:
-
OLD、NEW 表示某一行的内容,只对行级触发器有效,对语句级触发器无效
-
INSERT 操作只有新值 NEW,没有旧值 OLD
-
DELETE 操作只有旧值 OLD,没有新值 NEW
下面以表 t1、t2 为例创建一个简单的触发器示例。表的定义如下:
CREATE TABLE t1 (
a INTEGER,
b TEXT
);
CREATE TABLE t2 (
c INTEGER,
d TEXT
);
触发器定义如下,是表 t1 上的行级触发器,对 t1 进行 INSERT 之后会触发,并执行 insert_into_t2 函数,将插入到 t1 的数据也插入到 t2。
CREATE TRIGGER after_insert_into_t1
AFTER INSERT ON t1
FOR EACH ROW
EXECUTE FUNCTION insert_into_t2();
insert_into_t2 函数定义如下,其中引用了上下文信息 NEW,表示插入到 t1 的数据,并将其插入到 t2。
CREATE OR REPLACE FUNCTION insert_into_t2()
RETURNS trigger AS $$
BEGIN
INSERT INTO t2 VALUES (NEW.a, NEW.b);
RETURN NEW;
END;
$$ language plpgsql;
Event Trigger
创建 Event Trigger的语法如下,相比 Trigger 的语法要简单很多
CREATE EVENT TRIGGER name
ON event
[ WHEN filter_variable IN (filter_value [, ... ]) [ AND ... ] ]
EXECUTE { FUNCTION | PROCEDURE } function_name()
-
event 表示触发事件,目前仅 支持 ddl_command_start 、 ddl_command_end 、 table_rewrite 和 sql_drop
-
WHEN filter_variable 用于过滤部分事件,当前仅支持 TAG 变量
-
事件触发器并不依赖于表而存在
-
事件触发器不能定义为约束触发器,不可延迟
以下是 PG 官方文档中的一个简单示例,该 Event Trigger 可以在任何 DDL 语句执行之前触发,并抛出异常,禁止执行任何 DDL 语句。
CREATE EVENT TRIGGER abort_ddl ON ddl_command_start
EXECUTE FUNCTION abort_any_command();
CREATE OR REPLACE FUNCTION abort_any_command()
RETURNS event_trigger AS $$
BEGIN
RAISE EXCEPTION 'command % is disabled', tg_tag;
END;
$$ LANGUAGE plpgsql;
创建流程
简单介绍创建触发器时 PG 内核中的函数调用流程。
Trigger
CREATE TRIGGER 命令都属于 DDL 语句,所以会进入 DDL 的处理流程,关键的调用路径为:
ProcessUtilitySlow-->CreateTrigger-->CreateTriggerFiringOn,CreateTriggerFiringOn 函数代码超过 1000 行,因此只介绍其中的关键步骤:
-
根据表的 oid,打开触发器所在的表,加上 ShareRowExclusiveLock 锁
-
进行各种合法性检查,过滤不合理的触发器类型,如果检查不通过,直接报错。(之所以要在内核中检查,是因为语法模块无法进行如此复杂的检查)
-
检查对象类型,触发器只能创建在表、视图上
-
表和分区表上只能创建 BEFORE、AFTER 触发器,不能创建 INSTEAD OF 触发器
-
视图上不能创建行级的 BEFORE、AFTER 触发器,且不能创建 TRUNCATE 触发器
-
外表上不能创建 INSTEAD OF 触发器、TRUNCATE 触发器、约束触发器
-
不允许行级的 TRUNCATE 触发器
-
INSTEAD OF 触发器必须为行级、不能有 WHEN 条件、不能指定列名
-
默认不允许在系统表创建触发器(但是可以通过设置 allow_system_table_mods 参数放开限制)
-
REFERENCING 语句不能使用行变量名,只能使用 OLD、NEW 的表别名
-
外表约束和视图上的约束不能使用 REFERENCING
-
对于约束触发器,还需要对约束所在的表加上 AccessShareLock,防止 drop table
-
……
-
表的 ACL 权限检查
-
解析 WHEN 条件语句,并进行各种限制条件判断,比如语句级触发器的 WHEN 条件不能引用列名、INSERT 触发器的 WHEN 条件不能引用 OLD……
-
获取触发器函数,检查其合法性,检查 ACL 权限,检查返回值是否为 trigger
-
用 table_open 函数 打开 pg_trigger 系统表,对其加上 RowExclusiveLock 锁,并 检查其中是否已经存在同名触发器
-
在同一个表上,触发器不可重名,但是不同表上的触发器可以重名
-
假如使用了 CREATE OR REPLACE TRIGGER 语法,则替换原有的同名触发器(官方文档中并没有提到这种语法,但是代码中却进行了检查)
-
若检查通过,则用 GetNewOidWithIndex 函数为触发器生成一个新的 oid
-
调用 heap_form_tuple 函数, 在 pg_trigger 系统表中增加一个元组,存储该触发器的各项信息
-
对于约束触发器,还需要使用 CreateConstraintEntry 函数 在 pg_constraint 系统表中增加一行
-
用 table_open 函数打开 pg_class 系统表, 更新触发器所在的表在 pg_class 中的信息,并发送信息给后端 更新 relcache 中的系统表信息
-
调用 recordDependencyOn 记录触发器的依赖关系
-
最后,对于分区表上的行级触发器,需要对所有分区都递归创建触发器
Event Trigger
CREATE EVENT TRIGGER 的关键调用路径为:standard_ProcessUtility-->CreateEventTrigger,该函数流程相对简单很多:
-
检查用户权限, 只允许超级用户创建事件触发器,对其他用户报错
-
检查 event,只能为 ddl_command_start 、 ddl_command_end 、 table_rewrite 或 sql_drop
-
检查过滤条件, 当前仅支持 tag
-
调用 SearchSysCache1 函数, 检查同名触发器,重名就报错(与 Trigger 不同,名字必须全局唯一,因为 Event Trigger 不属于任何表)
-
检查 触发器函数的返回值是否为 event_trigger
-
调用 insert_event_trigger_tuple 函数: 在 pg_event_trigger 系统表中插入一条记录
触发器的存储
用户创建的触发器必须持久化到数据库中,具体的存储位置是触发器相关的系统表中。
Trigger
Trigger 存储在pg_trigger 系统表中,表中的关键字段如下,包含触发器所在的表、触发器名、触发器调用的函数、是否可推迟等属性。总之,通过 CREATE TRIGGER 创建触发器时指定的任何信息都会存储到系统表中。
字段 |
描述 |
oid |
触发器的 id |
tgrelid |
触发器所在的表 |
tgname |
触发器名(在同一个表的触发器中必须唯一) |
tgfoid |
触发器调用的函数 |
tgdeferrable |
约束触发器是否可推迟 |
tginitdeferred |
约束触发器是否初始可推迟 |
pg_trigger 系统表的各个字段在内存中用 Trigger 结构体表示,定义如下,可见其成员变量与 pg_trigger 的属性是一一对应的。
typedef struct Trigger
{
Oid tgoid; /* OID of trigger (pg_trigger row) */
/* Remaining fields are copied from pg_trigger, see pg_trigger.h */
char *tgname;
Oid tgfoid;
int16 tgtype;
char tgenabled;
bool tgisinternal;
bool tgisclone;
Oid tgconstrrelid;
Oid tgconstrindid;
Oid tgconstraint;
bool tgdeferrable;
bool tginitdeferred;
int16 tgnargs;
int16 tgnattr;
int16 *tgattr;
char **tgargs;
char *tgqual;
char *tgoldtable;
char *tgnewtable;
} Trigger;
在内存中的 relcache(表缓存)中也同样保存有 Trigger 的信息:
-
RelationData,存放 relcache 的数据
-
TriggerDesc *trigdesc 字段,该表上的触发器信息
-
Trigger *triggers 字段,表上所有触发器组成的数组
Event Trigger
Event Trigger 存储在 pg_event_trigger 系统表中,关键字段如下,包含触发器名、调用的函数等信息。与 Trigger 不同的是,这里并不包含触发器所在的表,因为 Event Trigger 不属于任何一个表。
字段 |
描述 |
evtname |
触发器名(必须唯一) |
evtevent |
触发事件的标识符 |
evtowner |
事件触发器的拥有者 |
evtfoid |
触发器调用的函数 |
触发过程
触发器会在特定事件场景下被触发,它识别这些事件的方式也很简单,就是在对应事件的代码处调用触发器函数。
Trigger
对于普通的触发器,触发时机是 INSERT、UPDATE、DELETE 等操作之前或者之后,所以在 PG 的执行器阶段触发,多数在 ProcessQuery-->ExecutorRun-->ExecModifyTable 函数中
-
对于 语句级触发器,就在 ExecModifyTable 中调用 fireASTriggers 触发(用 fire 一词来表示触发,使用了 Trigger 的另一层含义:扳机,而 fire 表示扣动扳机的动作,生动形象)
-
对于 行级触发器,从 ExecModifyTable 继续向下调用一层,到 ExecInsert、ExecUpdate、ExecDelete 函数对每一行进行操作时触发
我们将执行触发操作的函数称为“触发器的执行函数”,各类触发器的执行函数命名格式比较统一,在此列举几种:
-
ExecBRInsertTriggers,BR 表示 Before Row,Insert 表示插入时触发
-
ExecASUpdateTriggers,AS 表示 After Statement,Update 表示更新时触发
-
ExecIRDeleteTriggers,IR 表示 Instead Of Row,Delete 表示删除时触发
以 ExecBRInsertTriggers 为例说明触发过程:
-
从 ResultRelInfo 结构体获取 TriggerDesc 信息,ResultRelInfo 是执行器阶段的表结构相关信息,TriggerDesc 是触发器信息
-
遍历 TriggerDesc 中的 Trigger *triggers 数组, 检查该表上的每一个 Trigger
-
调用 TRIGGER_TYPE_MATCHES 检查触发器类型是否匹配,对于 ExecBRInsertTriggers 代表的触发器,必须是行级、BEFORE、INSERT,不匹配则跳过
-
调用 TriggerEnabled 检查触发器是否启用,本质是检查 Trigger 结构体的 tgenabled 字段,未启用则跳过
-
填入 TriggerData 结构体的各个字段内容
-
若检查通过, 调用 ExecCallTriggerFunc 执行触发器的函数
Event Trigger
事件触发器支持的事件仅有 ddl_command_start、ddl_command_end、table_rewrite 和 sql_drop 这四类,分别对应四个执行函数,其触发时机说明如下:
-
EventTriggerDDLCommandStart, ddl_command_start 事件的执行函数
-
在 ProcessUtilitySlow 函数开头调用,ProcessUtilitySlow 函数用于处理 DDL 语句,所以该触发器 在 DDL 的开始处触发
-
EventTriggerDDLCommandEnd, ddl_command_end 事件的执行函数
-
在 ProcessUtilitySlow 结尾处调用, 即 DDL 结束时触发
-
EventTriggerSQLDrop, sql_drop 事件的执行函数
-
也是在 ProcessUtilitySlow 结尾处调用
-
EventTriggerTableRewrite, table_rewrite 事件的执行函数
-
在 ATRewriteTables 中调用,在 执行 rewrite table 操作之前调用
以 EventTriggerDDLCommandStart 为例说明触发过程:
-
检查是否为 postmaster,对于 standalone 模式,不允许触发
-
调用 EventTriggerCommonSetup
-
调用 EventCacheLookup, 从缓存中获取该事件的触发器函数列表
-
填入 EventTriggerData 的各个字段内容
-
调用 EventTriggerInvoke, 遍历触发器函数列表,逐个调用
调用功能函数
用户在创建触发器的 EXECUTE { FUNCTION | PROCEDURE } function_name 语句中指定了该触发器要执行的功能函数。在触发器被触发后,会执行该函数。
Trigger
在执行器阶段触发时,ResultRelInfo 结构体中存有表上的各项信息,其中就包括表上的触发器、函数等,所以直接从中就可以拿到触发器信息。关键结构体为 ResultRelInfo、TriggerDesc、Trigger,其嵌套关系如下:
-
ResultRelInfo,执行器中的表信息
-
TriggerDesc *ri_TrigDesc,表上的触发器信息
-
Trigger *triggers,触发器数组
-
Trigger,触发器名字、 函数 oid 等基本信息
-
bool trig_insert_before_row
-
bool trig_insert_after_row
-
……
-
将 ResultRelInfo 中获取的 Trigger 结构体的全部内容都填充到 TriggerData 结构体,ExecCallTriggerFunc 函数再从 TriggerData 中获取函数 oid,并执行该函数。
TriggerData 结构体定义如下,其中除了 Trigger 以外还保存了各种执行上下文信息,heap 表信息等,与函数的执行有关。
typedef struct TriggerData
{
NodeTag type;
TriggerEvent tg_event;
Relation tg_relation;
HeapTuple tg_trigtuple;
HeapTuple tg_newtuple;
Trigger *tg_trigger;
TupleTableSlot *tg_trigslot;
TupleTableSlot *tg_newslot;
Tuplestorestate *tg_oldtable;
Tuplestorestate *tg_newtable;
const Bitmapset *tg_updatedcols;
} TriggerData;
TriggerData 最终会保存到 PLpgSQL_execstate 中,这是 PLpgSQL 执行过程中的一个重要结构体:
typedef struct PLpgSQL_execstate
{
PLpgSQL_function *func; /* function being executed */
TriggerData *trigdata; /* if regular trigger, data about firing */
EventTriggerData *evtrigdata; /* if event trigger, data about firing */
…………
} PLpgSQL_execstate;
触发器的功能函数执行的方法与普通的 PLpgSQL 函数、存储过程执行方法是类似的,关键调用路径是:
ExecCallTriggerFunc-->plpgsql_call_handler-->plpgsql_exec_trigger-->exec_toplevel_block-->exec_stmt_block-->…………
Event Trigger
对于事件触发器,在触发阶段的 EventTriggerCommonSetup 函数中,通过 EventCacheLookup 从缓存中查找触发器功能函数,然后在 EventTriggerInvoke 中根据触发器函数的 oid 进行调用。
EventTriggerCommonSetup 中还会填充 EventTriggerData 结构体,其中保存了调用过程中的一些关键信息:
typedef struct EventTriggerData
{
NodeTag type;
const char *event; /* event name */
Node *parsetree; /* parse tree */
CommandTag tag;
} EventTriggerData;
与普通触发器的 TriggerData 结构一样,EventTriggerData 结构体也会保存到 PLpgSQL_execstate 中,在 PLpgSQL 执行过程中使用:
typedef struct PLpgSQL_execstate
{
PLpgSQL_function *func; /* function being executed */
TriggerData *trigdata; /* if regular trigger, data about firing */
EventTriggerData *evtrigdata; /* if event trigger, data about firing */
…………
} PLpgSQL_execstate;
事件触发器的功能函数实际执行步骤与普通触发器也基本相同,关键调用路径为:
ExecCallTriggerFunc-->plpgsql_call_handler-->plpgsql_exec_event_trigger-->exec_toplevel_block-->exec_stmt_block-->…………
修改触发器
使用 ALTER 语句修改触发器的定义
Trigger
根据 PG 14 官方文档,ALTER TRIGGER 的语法如下:
ALTER TRIGGER name ON table_name RENAME TO new_name
ALTER TRIGGER name ON table_name DEPENDS ON EXTENSION extension_name
仅支持重命名和修改依赖的插件。
重命名触发器的关键调用流程为:standard_ProcessUtility-->ExecRenameStmt-->renametrig,基本原理也是读取 pg_trigger 系统表的信息,修改以后写回系统表。
修改触发器依赖插件的关键调用流程为:standard_ProcessUtility-->ExecAlterObjectDependsStmt,会修改 pg_depend 系统表。
Event Trigger
根据 PG 14 官方文档,ALTER EVENT TRIGGER 语法为:
ALTER EVENT TRIGGER name DISABLE
ALTER EVENT TRIGGER name ENABLE [ REPLICA | ALWAYS ]
ALTER EVENT TRIGGER name OWNER TO { new_owner | CURRENT_USER | SESSION_USER }
ALTER EVENT TRIGGER name RENAME TO new_name
支持对事件触发器进行重命名、禁用、启用、修改 owner 的操作。
ALTER TRIGGER 的关键函数是 AlterEventTrigger,其内容较为简单:
-
table_open 打开 pg_event_trigger 系统表,加 RowExclusiveLock 锁
-
检查事件触发器是否存在,不存在则报错
-
检查当前用户是否为事件触发器的 owner,检查不通过则报 ACL 错误
-
检查通过,更新事件触发器的信息,并写入到 pg_event_trigger 系统表中
删除触发器
使用 DROP 语句删除触发器
Trigger
PG 14 文档中 DROP TRIGGER 语法如下:
DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ]
删除触发器的关键函数是 RemoveTriggerById,调用流程如下:
ProcessUtilitySlow-->ExecDropStmt-->RemoveObjects-->performMultipleDeletions-->deleteObjectsInList-->deleteOneObject-->doDeletion-->RemoveTriggerById
RemoveTriggerById 函数流程:
-
调用 table_open 打开 pg_trigger 系统表,根据 oid 在其中查找触发器
-
调用 table_open 打开触发器所有的表,并检查表类型是否合法,必须是表、视图或者外表,不能为系统表
-
调用 CatalogTupleDelete 删除 pg_trigger 中的对应元组
Event Trigger
PG 14 文档中 DROP EVENT TRIGGER 语法如下:
DROP EVENT TRIGGER [ IF EXISTS ] name [ CASCADE | RESTRICT ]
删除事件触发器的关键函数是 DropObjectById,这是一个公用的函数,可以删除多种类型的对象。
调用流程如下:
ProcessUtilitySlow-->ExecDropStmt-->RemoveObjects-->performMultipleDeletions-->deleteObjectsInList-->deleteOneObject-->doDeletion-->DropObjectById