首页 > 代码库 > 杂谈--DML触发器学习

杂谈--DML触发器学习

触发器按类型分为三类:

1. DML 触发器,在数据变更时触发;

2. DDL 触发器,在修改数据库级别或实例级别对象时触发;

3. Login 触发器,在用户登录时触发;

 

最常见的是DML触发器,DML触发器又可以分为两类: INSTEAD OF触发器和AFTER触发器(部分书上有提到FOR触发器,其实就是AFTER 触发器,只是写法不同而已)。

从功能来看,INSTEAD OF触发器用来替换实际的数据修改操作,而AFTER触发器用来在实际操作完成后进行后续操作。例如对于DELETE操作,如果我们期望只修改数据状态来标示数据已被删除而不是将数据从表中删除,那么我们可以使用INSTEAD OF触发器来实现;如果我们期望在删除数据后在其他表记录删除操作的发生时间,那么我们可以使用AFTER触发器来实现。

从执行来看,INSTEAD OF触发器和AFTER触发器的所处的执行时期不同,SQL Server中的触发顺序为:

1. 触发INSTEAD OF触发器

2. 触发DEFAULT 约束

3. 触发主键/唯一/CHECK约束

4. 触发外键约束

5. 触发AFTER 触发器

因此如果期望修改操作顺利执行而不触发约束导致回滚的话,可以使用INSTEAD OF触发器来将实现(在INSTEAD OF 触发器中修改使数据满足约束条件)。

 

因为INSTEAD OF 触发器改写了实际要发生的修改操作,因此每个表上每种修改类型(DELETE/INSERT/UPDATE)只能有一个INSTEAD OF 触发器;而AFTER 触发器没有类似限制,可以创建多个AFTER触发器。

问题来了,在存在多个AFTER触发器情况下,AFTER触发器按什么顺序来执行呢?SQL Server允许针对每种修改类型(DELETE/INSERT/UPDATE)指定一个最先触发和最后触发的AFTER触发器,但不能控制其余的触发器触发顺序。

指定最先执行的AFTER触发器:

--指定针对INSERT操作最先触发的AFTER触发器EXEC sys.sp_settriggerorder@triggername=‘tr_TB1_INSERT‘,@order=‘First‘,@stmttype=‘INSERT‘

 

说完触发顺序,再来说道说道触发次数,装逼的说法为:DML trrigers have statement scope and only fire just once regardless of how many rows affected.通俗说法就是对于一条语句,不管语句修改了多少行(0行或者1000行),对应该操作类型的触发器都会被触发并且只触发一次。

PS:上面说的Fire only once只是针对执行的SQL语句,并不包含该触发器内部的SQL语句

 

SQL server中有两种特殊的触发器:嵌套(Nested)触发器和递归(Recursive)触发器,由Demo来解释下:

嵌套(Nested)触发器:在TB1和TB2上创建触发器,当TB1上TR_TB1_INSERT1被触发时,TR_TB1_INSERT1中的语句执行导致TB2上TR_TB2_INSERT1被触发

技术分享

--================================--在TB1和TB2上创建触发器,当TB1上TR_TB1_INSERT1被--触发时,TR_TB1_INSERT1中的语句执行导致TB2上--TR_TB2_INSERT1被触发,即属于Nested触发器CREATE TRIGGER TR_TB1_INSERT1ON dbo.TB1
AFTER INSERTASBEGININSERT INTO TB2(C1)SELECT C1 FROM insertedENDGOCREATE TRIGGER TR_TB2_INSERT1ON dbo.TB2
AFTER INSERTASBEGINSELECT 1END

技术分享

 

递归(Recursive)触发器可分为直接递归(Directed Recursive)触发器和间接递归(Indirect Recursive)触发器

直接递归(Directed Recursive)触发器:

在TB1创建触发器,当TB1上TR_TB1_INSERT1被触发时,TR_TB1_INSERT1中的语句执行导致TB1上TR_TB1_INSERT1再次被触发

技术分享

--================================--在TB1创建触发器,当TB1上TR_TB1_INSERT1被触发时,--TR_TB1_INSERT1中的语句执行导致TB1上TR_TB1_INSERT1--再次被触发,即属于直接递归(Directed Recursive)触发器。ALTER TRIGGER TR_TB1_INSERT1ON dbo.TB1
AFTER INSERTASBEGIN--限制递归层数为10层
    IF(@@NESTLEVEL<10)    BEGIN
        INSERT INTO TB1(C1)        SELECT C1+1 FROM inserted    ENDENDGO

技术分享

 

间接递归(Indirect Recursive)触发器:

在TB1和TB2上创建触发器,当TB1上TR_TB1_INSERT1被触发时,TR_TB1_INSERT1中的语句执行导致TB2上TR_TB2_INSERT1被触发,而TB2上TR_TB2_INSERT1的触发器执行时又导致TB1上TR_TB1_INSERT1被触发,从而引发循环。

技术分享

--================================--在TB1和TB2上创建触发器,当TB1上TR_TB1_INSERT1被--触发时,TR_TB1_INSERT1中的语句执行导致TB2上--TR_TB2_INSERT1被触发,而TB2上TR_TB2_INSERT1的--触发器执行时又导致TB1上TR_TB1_INSERT1被触发,从而--引发循环,即间接递归(Indirect Recursive)触发器CREATE TRIGGER TR_TB1_INSERT1ON dbo.TB1
AFTER INSERTASBEGIN
    IF(@@NESTLEVEL<10)    BEGIN
        INSERT INTO TB2(C1)        SELECT C1 FROM inserted    ENDENDGOCREATE TRIGGER TR_TB2_INSERT1ON dbo.TB2
AFTER INSERTASBEGIN
    IF(@@NESTLEVEL<10)    BEGIN
        INSERT INTO TB1(C1)        SELECT C1 FROM inserted    ENDEND

技术分享


需要注意的是:

1. 嵌套(Nested)触发器在sys.configurations中配置,默认开启

2. (Recursive)触发器在数据库级别配置,默认为关闭,即不允许直接递归(Directed Recursive)触发器,但不影响间接递归(Indirect Recursive)触发器,如果需要禁用递归(Indirect Recursive)触发器,需要同时禁用嵌套(Nested)触发器和(Recursive)触发器

3. 由于嵌套触发器会消耗大量资源(需要保留每层触发器的上下文以便回滚),因此默认限制最多嵌套32层。

 

行版本(Row version)

在SQL Server多中功能中使用到row version来保留多个版本的数据,这些功能有:

1. MARS

2. Triggers

3. Online indexing

4. Optimistic Transaction Isolation Levels

因此在使用触发器时,应考虑到可能会为表增加额外14bytes的行版本存储指针

如下面例子中,表中数据被删除一半,但由于数据只是表示为gost,尚未真正移除,而由于触发器存在,每行额外增加14byte的数据,从而导致页拆分,最终使得删除操作完成后表反而增大。

测试代码:

技术分享

USE tempdb--================================--创建测试表DROP TABLE TB1GOCREATE TABLE TB1
(
    ID INT IDENTITY(1,1) PRIMARY KEY,
    C2 INT NOT NULL,
    C3 VARCHAR(MAX)
)GO--================================--创建Delete触发器CREATE TRIGGER TR_TB1_DELETEON dbo.TB1
AFTER DELETEASBEGINRETURN ENDGO--================================--插入5w数据INSERT INTO TB1(C2)SELECT TOP(5000) 1 AS C2 FROM sys.all_columns TGO 10--================================--查看表TB1使用的页DBCC TRACEON(3604)GODBCC IND(‘tempdb‘,‘TB1‘,1)GO--================================--删除一半的数据DELETE FROM  dbo.TB1WHERE ID%2=0GO--================================--查看表TB1使用的页DBCC TRACEON(3604)GODBCC IND(‘tempdb‘,‘TB1‘,1)GO

技术分享

PS: 如果表中不存在LOB或者VARCHAR(MAX)之类的大字段,不存在ROW_OVERFLOW数据页,则SQL Server不会为每行增加14byte的行版本存储指针

--==============================================================

--额外补充

1. 如果使用Merge并且设置了INSERT/DELETE/UPDATE方法,那么即使没有满足条件的数据进行INSERT/DELETE/UPDATE,也会触发INSERT/DELETE/UPDATE相关的触发器。

 

--==================================================

新一年,换换口味,来点萌妹子吧!

技术分享


杂谈--DML触发器学习