百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

PostgreSQL技术内幕28:触发器实现原理

haoteby 2025-05-14 13:32 26 浏览

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函数进行删除。

相关推荐

在线抓取网页源码(爬虫获取网页源代码)

经验分享:如何解决爬虫抓取时的网页源码不全问题爬虫是一种常用的数据采集工具,可以帮助我们快速获取互联网上的各种信息。然而,很多人在使用爬虫时都会遇到一个普遍的问题,那就是爬虫抓取的网页源码总是不完整。...

40个图源二维码分享及使用方法(图片二维码生成器在线制作二维码)

我们曾在《35个图源二维码分享及使用方法》一文中,为你分享了35个图源二维码。...

45个图源二维码分享及使用方法(图源是啥)

我们曾在《40个图源二维码分享及使用方法》一文中,为你分享了40个图源二维码。...

电子书管理软件Calibre 8.0.1发布:改善支持Kobo,优化新闻源等

IT之家3月22日消息,Calibre是一款免费、开源的电子书管理工具,支持整理、转换和阅读多种格式的电子书。最新发布的8.0.1版本带来了多项功能改进和问题修复,进一步提升了用户体验。...

软网推荐:一键生成不同网站RSS订阅源

为了关注自己喜欢的网站内容,以前最常见的方法是订阅RSS源,这样在内容更新的时候就会自动进行推送。但由于RSS现在已经不是主流的阅读方式,所以并不是所有的网站都提供RSS订阅,这时我们就得另辟蹊径了。...

消费曝光台|29.9元充100元话费?到手是15张满减优惠券

近日,山西大同的王先生向澎湃质量报告投诉平台反映,他在使用手机时收到一条弹窗广告,称他获得了“29.9元充值100元话费券”。王先生充值并下载了APP潮物圈后才发现,100元的话费实际上是15张5元和...

玩Kodi的IPTV插件必备技能——直播源地址抓包教程

准备工具:谷歌浏览器(Google)或者其他浏览器(不常用的或再安装一个浏览器)Potplayerm3u8直播源文件或者Gitee个人工作台...

i.MX6加载Ubuntu镜像的教程(ubuntu添加镜像源)

基于迅为IMX6开发板安装好虚拟机之后,用户就可以加载Ubuntu12.04.2镜像。用户可以在网盘中下载“编译好的镜像”,该镜像已经安装好了编译Android4.4.2所需要的大部分软件...

嵌入式软件开发人员有必要学习系统移植的知识吗?ppt见文末

《从零开始学ARM》的配套视频说明...

ARM体系结构(10)-GPIO LED闪烁汇编代码实现(基于tiny4412)

接上一篇,我将介绍如何使用GPIO引脚GPM4_1来控制Tiny4412开发板上的LED2闪烁。本文将从原理、汇编代码、Makefile实现、程序编译和烧写等多个方面进行介绍,以便读者能够全面了解如何...

物联网学习路线图(物联网必学课程)

物联网技术近几年在我国获得了很好的发展,从目前的发展趋势来看,未来物联网发展前景一片大好。由此学习的人员也是越来越多,但是在学习物联网时很多人都容易忽略这样一件事——从未准备一份详尽的物联网学习路线图...

网卡DM9000裸机驱动开发详解(pro1000网卡驱动)

一、网卡1.概念网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有MAC地址,因此属于OSI模型的第2层。它使得用户可以通过电缆或无线相互连接。...

Win10高分屏更改DPI后字体模糊?试试这个小工具

如果你使用的是大尺寸显示屏,拥有1920x1080或更大分辨率,可能你会选择设置一个较高的DPI缩放级别,比如125%或更高。这样屏幕上的内容看起来会更大一些,更容易阅读。在WindowsVista...

微软Windows竟内嵌Linux?这样玩挺有趣

在Windows上运行Linux?这其实不是新鲜事,20年前,就有黑客这么干过。微软的开发者博客刚刚公布,下一个Windows10版本,不仅自带Linux内核,而且还会通过Win...

上厕所别带手机!9 个错误动作增加你感染病毒的风险

随着复工的正式开始,很多小伙伴开始担心如何在上班过程中更好地保护自己。口罩准备好了,免洗洗手液准备好了,是不是就万无一失了?还真不是!你摸完电梯按钮的手,或者扶过地铁栏杆的手,不经意间揉一下眼睛、摸一...