PostgreSQL技术内幕28:触发器实现原理
PostgreSQL技术内幕28:触发器实现原理
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函数进行删除。
-
- 印度政界吹捧“经济体量全球第四”,却遭到民间冷嘲热讽
-
2026-05-23 18:49:23
-
- 近百艘中国军舰在东海集结!中俄对表:必须要防备日本狗急跳墙了
-
2026-05-23 18:47:09
-
- AI时代,商学教育的价值觉醒
-
2026-05-23 18:44:55
-
- -2℃!上海发现此物即上报!宝山荒地男尸悬案告破!沪一公交站有人咬司机!张
-
2026-05-23 18:42:40
-
- 央视曝光!翻新卫生巾、纸尿裤竟被二次销售!涉及多个知名品牌
-
2026-05-23 18:40:26
-
- 奔驰C200最全进化之路 详细改装效果解说
-
2026-05-23 18:38:11
-
- 这份2026年卫生健康宣传日台历请收好!
-
2026-05-23 18:35:57
-
- 离婚6年后,高圣远转身回美国,找了个年轻貌美富婆,周迅仍单身
-
2026-05-23 18:33:43
-
- 深度剖析企业假账:手法、识别与核查
-
2026-05-23 18:31:28
-
- “阿里女员工被侵害”案一审关键证据披露:王某文脖子“草莓印”成脱罪证据,
-
2026-05-23 18:29:14
-
- 综治中心、共享法庭提供哪些服务?针对“AI换脸”等如何开展公益诉讼监督?浙
-
2026-05-23 18:27:00
-
- 美乌矿产协议落地,特朗普和泽连斯基的较量,赢了面子却输了里子
-
2026-05-23 18:24:45
-
- 没想到,61岁的甄子丹,会因回应记者的一句话,实现“口碑暴增”
-
2026-05-23 13:51:20
-
- 吉雪萍:女儿去世6年后,转行成心理咨询师,仨儿子都像富商老公
-
2026-05-23 13:49:06
-
- 林毅夫:埃塞俄比亚的起落与中国学者的探索
-
2026-05-23 13:46:51
-
- 企退与事退待遇差:不只是养老金,各类补贴的差距更实在
-
2026-05-23 13:44:37
-
- 简明菲律宾百科全书(截至2025年)
-
2026-05-23 13:42:22
-
- 1978年新一届国务院领导同志分工简析
-
2026-05-23 13:40:08
-
- 美方拟加征50%关税引中方坚决回应:贸易战无赢家,对话才是出路
-
2026-05-23 13:37:54
-
- 郑惟桐:荣耀半生功绩著,一朝失足梦沉沦
-
2026-05-23 13:35:39



“娘娘”孙俪公布离婚,原因邓超出轨家暴?她晒甜蜜合照否认
兽人耽美小说爆款推荐!这五部高人气作品让你脸红心跳,入坑不亏