PostgreSQL技术内幕28:触发器实现原理
haoteby 2025-05-14 13:32 3 浏览
0.简介
在PostgreSQL(简称PG)数据库中,触发器(Trigger)能够在特定的数据库数据变化事件(如插入、更新、删除等)或数据库事件(DDL)发生时自动执行预定义的操作。触发器的实现原理涉及多个方面,包括触发器的概念、触发器的创建、触发器的存储、触发器的执行、触发器的修改以及触发器函数的删除等。本文将对PG触发器的实现原理进行简要分析,以帮助读者更好地理解其工作机制。
1.触发器的概念
触发器是数据库管理系统(DBMS)中的一种对象,在PG中是一个特殊的函数,整体分为两类,当某个表或者视图数据变化时触发的触发器称为数据变更触发器(DML触发器),当某个数据库事件发生时触发的触发器称为事件触发器(DDL触发器)。触发器主要是其如何存储和调用时机(在哪些流程中处理),后文会详细分析代码调用流程。
触发器可以在事件前(before)或者事件后(after)执行:
1)BEFORE触发器:在触发事件发生之前执行。它可以在数据写入或更新到数据库之前执行一些检查或操作。
2)AFTER触发器:在触发事件发生之后执行。它可以在数据写入或更新到数据库之后执行一些后续操作,如记录日志、更新相关数据等。
其中数据变更触发器支持两种级别:
1)行级触发器(Row-level Trigger):针对表中的每一行数据变化触发一次。当对表中的某一行进行插入、更新或删除操作时,行级触发器会被激活。
2)语句级触发器(Statement-level Trigger):针对一次数据操作(如一次INSERT、UPDATE或DELETE语句)触发一次。与行级触发器不同,语句级触发器是在整个SQL语句执行之前或之后被激活的。
2.触发器的创建
2.1 创建触发器
2.1.1 创建语法
CREATE [ OR REPLACE ] [ 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
详细含义可见官方文档:
http://postgres.cn/docs/14/sql-createtrigger.html
2.1.2 创建流程分析
创建触发器其调用函数为:CreateTrigger,其中直接调用CreateTriggerFiringOn函数,下面对其中主要流程进行描述:
1)检查oid合法性并开表加锁。
if (OidIsValid(relOid))
rel = heap_open(relOid, ShareRowExclusiveLock);
else
rel = heap_openrv(stmt->relation, ShareRowExclusiveLock);
2)进行各种合法性检查:是否是创建在表或视图上,是否是对于支持的触发器类型等,下面是一个检查例子。
else if (rel->rd_rel->relkind == RELKIND_VIEW)
{
/*
* Views can have INSTEAD OF triggers (which we check below are
* row-level), or statement-level BEFORE/AFTER triggers.
*/
if (stmt->timing != TRIGGER_TYPE_INSTEAD && stmt->row)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is a view",
RelationGetRelationName(rel)),
errdetail("Views cannot have row-level BEFORE or AFTER triggers.")));
/* Disallow TRUNCATE triggers on VIEWs */
if (TRIGGER_FOR_TRUNCATE(stmt->events))
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is a view",
RelationGetRelationName(rel)),
errdetail("Views cannot have TRUNCATE triggers.")));
}
3)权限检查。
/* permission checks */
if (!isInternal)
{
aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
ACL_TRIGGER);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
RelationGetRelationName(rel));
if (OidIsValid(constrrelid))
{
aclresult = pg_class_aclcheck(constrrelid, GetUserId(),
ACL_TRIGGER);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(constrrelid)),
get_rel_name(constrrelid));
}
}
4)分区表对于行级触发器需要递归创建触发器,此处先加锁。
/*
* When called on a partitioned table to create a FOR EACH ROW trigger
* that's not internal, we create one trigger for each partition, too.
*
* For that, we'd better hold lock on all of them ahead of time.
*/
partition_recurse = !isInternal && stmt->row &&
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
if (partition_recurse)
list_free(find_all_inheritors(RelationGetRelid(rel),
ShareRowExclusiveLock, NULL));
5)获取触发器函数并检查合法性。
/*
* Find and validate the trigger function.
*/
if (!OidIsValid(funcoid))
funcoid = LookupFuncName(stmt->funcname, 0, fargtypes, false);
if (!isInternal)
{
aclresult = pg_proc_aclcheck(funcoid, GetUserId(), ACL_EXECUTE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_FUNCTION,
NameListToString(stmt->funcname));
}
funcrettype = get_func_rettype(funcoid);
if (funcrettype != TRIGGEROID)
6)打开存储触发器的系统表pg_trigger并加锁进行同名检查。
*
* Scan pg_trigger for existing triggers on relation. We do this only to
* give a nice error message if there's already a trigger of the same
* name. (The unique index on tgrelid/tgname would complain anyway.) We
* can skip this for internally generated triggers, since the name
* modification above should be sufficient.
*
* NOTE that this is cool only because we have ShareRowExclusiveLock on
* the relation, so the trigger set won't be changing underneath us.
*/
if (!isInternal)
{
ScanKeyInit(&key,
Anum_pg_trigger_tgrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 1, &key);
while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
{
Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(tuple);
if (namestrcmp(&(pg_trigger->tgname), trigname) == 0)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("trigger \"%s\" for relation \"%s\" already exists",
trigname, RelationGetRelationName(rel))));
}
systable_endscan(tgscan);
}
7)对系统表增加触发器信息存储。
tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
8)记录触发器的依赖关系。
/*
* Record dependencies for trigger. Always place a normal dependency on
* the function.
*/
myself.classId = TriggerRelationId;
myself.objectId = trigoid;
myself.objectSubId = 0;
referenced.classId = ProcedureRelationId;
referenced.objectId = funcoid;
referenced.objectSubId = 0;
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
9)分区表对于行级触发器递归创建。
/*
* Lastly, create the trigger on child relations, if needed.
*/
if (partition_recurse)
{
PartitionDesc partdesc = RelationGetPartitionDesc(rel);
List *idxs = NIL;
List *childTbls = NIL;
ListCell *l;
int i;
MemoryContext oldcxt,
perChildCxt;
...
}
2.2 创建事件触发器
2.2.1 创建语法
CREATE EVENT TRIGGER name
ON event
[ WHEN filter_variable IN (filter_value [, ... ]) [ AND ... ] ]
EXECUTE { FUNCTION | PROCEDURE } function_name()
详细含义可见官方文档:
http://postgres.cn/docs/14/sql-createeventtrigger.html
2.2.2 创建流程分析
创建触发器其调用函数为CreateEventTrigger,其流程如下:
1)检查用户权限,只有超级用户可以创建。
/*
* It would be nice to allow database owners or even regular users to do
* this, but there are obvious privilege escalation risks which would have
* to somehow be plugged first.
*/
if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create event trigger \"%s\"",
stmt->trigname),
errhint("Must be superuser to create an event trigger.")));
2)检查event,只能是特定类型的event。
/* Validate event name. */
if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
strcmp(stmt->eventname, "ddl_command_end") != 0 &&
strcmp(stmt->eventname, "sql_drop") != 0 &&
strcmp(stmt->eventname, "table_rewrite") != 0)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized event name \"%s\"",
stmt->eventname)));
3)检查过滤的条件,目前仅仅支持tag,关于tag后面会写一篇文章专门去介绍。
/* Validate filter conditions. */
foreach(lc, stmt->whenclause)
{
DefElem *def = (DefElem *) lfirst(lc);
if (strcmp(def->defname, "tag") == 0)
{
if (tags != NULL)
error_duplicate_filter_variable(def->defname);
tags = (List *) def->arg;
}
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized filter variable \"%s\"", def->defname)));
}
4)检查是否存在重名。
/*
* Give user a nice error message if an event trigger of the same name
* already exists.
*/
tuple = SearchSysCache1(EVENTTRIGGERNAME, CStringGetDatum(stmt->trigname));
if (HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("event trigger \"%s\" already exists",
stmt->trigname)));
5)检查触发器函数。
/* Find and validate the trigger function. */
funcoid = LookupFuncName(stmt->funcname, 0, fargtypes, false);
funcrettype = get_func_rettype(funcoid);
if (funcrettype != EVTTRIGGEROID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("function %s must return type %s",
NameListToString(stmt->funcname), "event_trigger")));
6)在pg_event_trigger系统表插入一条记录。
/* Insert catalog entries. */
return insert_event_trigger_tuple(stmt->trigname, stmt->eventname,
evtowner, funcoid, tags);
3.触发器的执行
3.1 触发器的触发执行
普通触发器的触发是在insert、update、delete等操作的之前或者之后,由于触发器分为两个级别(语句级和行级)所以分别进行分析:
3.1.1 语句级触发器执行
对于语句级的触发,在ExecModifyTable 中调用,其分为before和after:
/*
* On first call, fire BEFORE STATEMENT triggers before proceeding.
*/
if (node->fireBSTriggers)
{
fireBSTriggers(node);
node->fireBSTriggers = false;
}
。。。
/*
* We're done, but fire AFTER STATEMENT triggers before exiting.
*/
fireASTriggers(node);
node->mt_done = true;
其中以fireBSTriggers中代码来看,其就是根据类型来执行不同触发器操作:
static void
fireBSTriggers(ModifyTableState *node)
{
ModifyTable *plan = (ModifyTable *) node->ps.plan;
ResultRelInfo *resultRelInfo = node->resultRelInfo;
/*
* If the node modifies a partitioned table, we must fire its triggers.
* Note that in that case, node->resultRelInfo points to the first leaf
* partition, not the root table.
*/
if (node->rootResultRelInfo != NULL)
resultRelInfo = node->rootResultRelInfo;
switch (node->operation)
{
case CMD_INSERT:
ExecBSInsertTriggers(node->ps.state, resultRelInfo);
if (plan->onConflictAction == ONCONFLICT_UPDATE)
ExecBSUpdateTriggers(node->ps.state,
resultRelInfo);
break;
case CMD_UPDATE:
ExecBSUpdateTriggers(node->ps.state, resultRelInfo);
break;
case CMD_DELETE:
ExecBSDeleteTriggers(node->ps.state, resultRelInfo);
break;
default:
elog(ERROR, "unknown operation");
break;
}
}
继续以插入的执行函数ExecBSInsertTriggers分析流程:
1)获取TriggerDesc(表上所有触发器)信息。
2)遍历TriggerDesc:检查触发器类型是否匹配,检查是否启用,检查通过就调用ExecCallTriggerFunc 执行触发器函数。
3.1.2 行级触发器
对于行级触发器,其和语句级的差异在于调用位置更为底层,针对每一行每一种处理时调用不同函数(底层已经明确知道触发器的类型,无需再有类似fireBSTriggers中类型屈服),如插入的函数ExecInsert中的调用:
3.2 事件触发器的触发执行
事件触发器支持四种事件:
1)
ddl_command_start:EventTriggerDDLCommandStart函数,在ddl执行的开始处调用。
2)
ddl_command_end:EventTriggerDDLCommandEnd函数,在ddl的执行结束处调用。
3)
table_rewrite:EventTriggerTableRewrite函数,在执行 rewrite table 操作之前调用。
4)
sql_drop:EventTriggerSQLDrop函数,在 ddl结尾处调用。
下面以其中
EventTriggerDDLCommandStart函数为例进行分析:
1)检查是否standalone模式(是的话不可以触发,原因可见注释)和是否postmaster。
/*
* Event Triggers are completely disabled in standalone mode. There are
* (at least) two reasons for this:
*
* 1. A sufficiently broken event trigger might not only render the
* database unusable, but prevent disabling itself to fix the situation.
* In this scenario, restarting in standalone mode provides an escape
* hatch.
*
* 2. BuildEventTriggerCache relies on systable_beginscan_ordered, and
* therefore will malfunction if pg_event_trigger's indexes are damaged.
* To allow recovery from a damaged index, we need some operating mode
* wherein event triggers are disabled. (Or we could implement
* heapscan-and-sort logic for that case, but having disaster recovery
* scenarios depend on code that's otherwise untested isn't appetizing.)
*/
if (!IsUnderPostmaster)
return;
2)执行EventTriggerCommonSetup:获取函数列表并填充触发器结构信息。
3)遍历执行并清理。
4.触发器的修改
4.1 普通触发器修改
4.1.1 修改语法
ALTER TRIGGER name ON table_name RENAME TO new_name
ALTER TRIGGER name ON table_name DEPENDS ON EXTENSION extension_name
详细说明可见:
http://postgres.cn/docs/14/sql-altertrigger.html
4.1.2 修改逻辑
根据创建流程我们知道其创建时对系统表pg_trigger和其依赖的pg_depend进行了修改,那么修改逻辑就是对其中的信息进行名称修改。可见renametrig函数和
ExecAlterObjectDependsStmt函数。
4.2 事件触发器的修改
4.2.1 修改语法
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
详细说明可见:
http://postgres.cn/docs/14/sql-altereventtrigger.html
4.2.2 修改逻辑
事件触发器在创建流程中对pg_event_trigger系统表进行了增加,其修改也就是对增加的数据进行修改后再次写入,可见函数 AlterEventTrigger。
5.触发器的删除
5.1 触发器删除
5.1.1 删除语法
DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ]
详细内容可见:
http://postgres.cn/docs/14/sql-droptrigger.html
5.1.2 删除逻辑
删除主要就是删除系统表中的数据,整体分为合法性检查阶段和删除阶段,可见函数RemoveTriggerById。
5.2 事件触发器删除
5.2.1 删除语法
DROP EVENT TRIGGER [ IF EXISTS ] name [ CASCADE | RESTRICT ]
详细说明可见:
http://postgres.cn/docs/14/sql-dropeventtrigger.html
5.2.2 删除逻辑
事件触发器是全局的对象,直接使用DropObjectById函数进行删除。
相关推荐
- 网站seo该怎么优化
-
一、网站定位在建设一个网站之前,我们首先要做的就是一个网站清晰的定位,会带来转化率相对较高的客户群体,我们建站的目的就是为了营销,只有集中来做某一件事,才会更好的展现我们的网站。在做SEO优化的同时...
- 3个小技巧教你如何做好SEO优化
-
想半路出家做SEO?可是,怎么才做的好呢?关于SEO专业技术弄懂搜索引擎原理,咱们做搜索引擎排名的首先就是要了解搜索引擎的工作原理,对SEO优化有更深入了解之后再来做SEO,你就能从搜索引擎的视点...
- SEO指令分享:filetype指令
-
filetype用于搜索特定的文件格式。百度和谷歌都支持filetype指令。比如搜索filetype:pdf今日头条返回的就是包含今日头条这个关键词的所有pdf文件,如下图:百度只支持:pdf...
- 网站seo优化技巧大全
-
SEO在搜索引擎中对检索结果进行排序,看谁最初是在用户的第一眼中看到的。实际上,这些排名都是通过引擎的内部算法来实现的。例如,百度算法很有名。那么,对百度SEO的优化有哪些小技巧?下面小编就会说下针对...
- 小技巧#10 某些高级的搜索技巧
-
由于某些原因,我的实验场所仅限百度。1.关键词+空格严格说来这个不能算高级,但关键词之间打空格的办法确实好用。我习惯用右手大拇指外侧敲击空格键,这个习惯在打英文报告时尤其频繁。2.site:(请不要忽...
- MYSQL数据库权限与安全
-
权限与安全数据库的权限和数据库的安全是息息相关的,不当的权限设置可能会导致各种各样的安全隐患,操作系统的某些设置也会对MySQL的安全造成影响。1、权限系统的工作原理...
- WPF样式
-
UniformGrid容器<UniformGridColumns="3"Rows="3"><Button/>...
- MySQL学到什么程度?才有可以在简历上写精通
-
前言如今互联网行业用的最多就是MySQL,然而对于高级Web面试者,尤其对于寻找30k下工作的求职者,很多MySQL相关知识点基本都会涉及,如果面试中,你的相关知识答的模糊和不切要点,基...
- jquery的事件名称和命名空间的方法
-
我们先看一些代码:当然,我们也可以用bind进行事件绑定。我们看到上面的代码,我们可以在事件后面,以点号,加我们的名字,就是事件命名空间。所谓事件命名空间,就是事件类型后面以点语法附加一个别名,以便引...
- c#,委托与事件,发布订阅模型,观察者模式
-
什么是事件?事件(Event)基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。通过委托使用事件事件在类中声明且生成,且通过...
- 前端分享-原生Popover已经支持
-
传统网页弹窗开发需要自己处理z-index层级冲突、编写点击外部关闭的逻辑、管理多个弹窗的堆叠顺序。核心优势对比:...
- Axure 8.0 综合帖——新增细节内容
-
一、钢笔工具与PS或者AI中的钢笔工具一样的用法。同样有手柄和锚点,如果终点和起点没有接合在一起,只要双击鼠标左键即可完成绘画。画出来的是矢量图,可以理解为新的元件。不建议通过这个工具来画ICON图等...
- PostgreSQL技术内幕28:触发器实现原理
-
0.简介在PostgreSQL(简称PG)数据库中,触发器(Trigger)能够在特定的数据库数据变化事件(如插入、更新、删除等)或数据库事件(DDL)发生时自动执行预定义的操作。触发器的实现原理涉及...
- UWP开发入门(十七)--判断设备类型及响应VirtualKey
-
蜀黍我做的工作跟IM软件有关,UWP同时会跑在电脑和手机上。电脑和手机的使用习惯不尽一致,通常我倾向于根据窗口尺寸来进行布局的变化,但是特定的操作习惯是依赖于设备类型,而不是屏幕尺寸的,比如聊天窗口的...