曲苑杂坛--数据库更新探秘

时间:2021-03-14 07:17:16

众所周知,SQL SERVER在更新数据时有两个辅助表(deleted和inserted)供我们使用,但是在数据跟新时,真的是先删除记录在插入记录吗?

让我们来测试下:

PS:以下测试在简单恢复模式的数据库上运行,使用checkpoint来截断日志,使用TRACE FLAG 3505来阻止系统checkpoint。

测试方式:更新数据,查看日志记录

DROP TABLE TB2
GO
CREATE TABLE TB2
(
    C1 INT PRIMARY KEY IDENTITY(1,1),
    C2 NVARCHAR(1000),
    C3 BIGINT
)
INSERT INTO TB2(C2,C3)
SELECT name,OBJECT_ID FROM sys.all_columns
GO
CHECKPOINT
--=====================================
--更新数据
UPDATE  TB2
SET C3=0
WHERE C1=4

--=====================================
--查看生成的日志
SELECT F.[Current LSN],
F.Operation,
F.Context,
F.[Transaction ID],
F.AllocUnitName,
F.[Page ID]
FROM fn_dblog(NULL,NULL) AS F
ORDER BY [Current LSN] DESC

曲苑杂坛--数据库更新探秘

再次测试

DROP TABLE TB2
GO
CREATE TABLE TB2
(
    C1 INT PRIMARY KEY IDENTITY(1,1),
    C2 NVARCHAR(1000),
    C3 BIGINT
)
INSERT INTO TB2(C2,C3)
SELECT name,OBJECT_ID FROM sys.all_columns
GO
CHECKPOINT
--=====================================
--更新数据
UPDATE  TB2
SET C2=REPLICATE('AB',40)
WHERE C1=4

--=====================================
--查看生成的日志
SELECT F.[Current LSN],
F.Operation,
F.Context,
F.[Transaction ID],
F.AllocUnitName,
F.[Page ID],
F.[Slot ID]
FROM fn_dblog(NULL,NULL) AS F
ORDER BY [Current LSN] DESC

曲苑杂坛--数据库更新探秘

测试结论:
1>在更新时,如果更新后的数据不会造成数据移动(如页拆分情况)时,直接修改该行数据即可,而如果造成数据移动如页拆分的话,则采用先删除再插入的方式移动一些数据行(移动的数据行可能不是要更新数据行),来为要更新的行腾出足够空间,来更新数据行。
2>无论采用何种方式更新数据,在触发器中都能使用DELETED和INSERTED表来获取更新前和更新后数据。

3>数据操作导致页拆分时,页拆分操作会被当做单独事务处理,这样可以在回滚数据操作时避免回滚页拆分。(感谢JentleWang指点)。

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

在上面的操作中,更新操作导致数据的变化,因此需要写入日志,记录数据变化,那如果更新操作不导致数据变化呢?

让我们再来测试下:

测试方式:在表TB2中导入5000+数据,表中只有4条数据的C2列值为456,其余行的C2列值为123,我们尝试更新整表数据的C2列值为123(针对5000+数据更新,但实际发生数据变化只有4行)

DROP TABLE TB2
GO
CREATE TABLE TB2
(
    C1 INT PRIMARY KEY IDENTITY(1,1),
    C2 BIGINT
)
INSERT INTO TB2(C2)
SELECT 123 FROM sys.all_columns
GO
UPDATE TB2
SET C2=456
WHERE C1<5
GO
CHECKPOINT
--=====================================
--更新数据
UPDATE TB2
SET C2=123

--=====================================
--查看生成的日志
SELECT F.[Current LSN],
F.Operation,
F.Context,
F.[Transaction ID],
F.AllocUnitName,
F.[Page ID],
F.[Slot ID]
FROM fn_dblog(NULL,NULL) AS F
ORDER BY [Current LSN] DESC

曲苑杂坛--数据库更新探秘

可以发现,虽然提示消息显示5134行数据受影响,但实际上只有四条日志记录被写入日志,这四条日志记录分别对应发生数据变化的行(依据page ID和solt ID来确定)。

出查看日志外,我们也可以使用数据胀页来查看

--=============================================
--确保表中只有4条数据的C2列不为123
UPDATE TB2
SET C2=123
GO
UPDATE TB2
SET C2=456
WHERE C1<5
GO
CHECKPOINT
GO
--=============================================
--更新前查看数据胀页
--PS: 在checkpoint结束后立即检查胀页,会发现还有
--少量胀页存在,需要等待一段时间
WAITFOR DELAY '0:0:10'
GO
SELECT * FROM sys.dm_os_buffer_descriptors
WHERE database_id = DB_ID() AND is_modified = 1
ORDER BY page_id;
--=====================================
--更新数据
UPDATE TB2
SET C2=123
--=============================================
--更新后查看数据胀页
SELECT * FROM sys.dm_os_buffer_descriptors
WHERE database_id = DB_ID() AND is_modified = 1
ORDER BY page_id;

运行上面code会发现,在表TB2进行整表更新后,只有一个数据胀页(表TB2共有14个数据页),由此我们也可以推断出只有部分数据页受影响。

测试结论:在数据更新时,如果当前行数据没有发生变化,那么不会在日志中记录该行数据,也不会因此将该行所在的页标示为胀页。

PS: 上述结论基于INT/DATETIME/CHAR/VARCHAR/NCHAR/NVARCHAR类型进行测试得出,在对TEXT/NTEXT测试时发现,即使值未发生改变,仍产生日志和数据胀页。

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

关于deleted和inserted,最常见的应用场景就是在触发器中,使用在OUTPUT中较少,做个demo:

--=======================================
--创建测试表
CREATE TABLE TB1
(
    ID INT IDENTITY(1,1) PRIMARY KEY,
    C1 NVARCHAR(200)
)
GO 
--=======================================
--向测试表中导入100条数据
INSERT INTO TB1(C1)
SELECT TOP(100)
name 
FROM sys.all_columns
GO
--=======================================
--更新测试表中10条数据,并找出被更新的行的ID
DECLARE @Tem TABLE
(
ID INT
)
UPDATE TOP(10) TB1
SET C1='ABC'
OUTPUT inserted.ID INTO @Tem(ID)

SELECT * FROM @Tem

在上面的demo中,我们可以很容易地利用deleted和inserted来查看受影响的数据行。利用inserted,我们可以在批量插入自增表中获取所有生成的自增值(@@IDENTITY只能获取最后一行的值)。

对于deleted和inserted,会记录操作中影响的所有行(即使行上的值未发生变化),同样对于在触发器中使用的UPDATE()函数,该函数同样只判断列是否受影响,而不判断值是否改变。

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

黄色背景中描述的删除插入以及更新均指在数据页上的操作,请勿和DML语句中的操作相混淆。

网上流传的版本:

UPDATE操作会被转换成两种方式中的一种:

1. XXX条件下,直接update数据行

2. XXX条件下,先删除数据旧行,再插入数据新行

对于这种版本,我曾经也认为时对的,并且记录在笔记本中,时间太久,找不到原出处。先仔细推敲测试,方觉得不对头,诸君可以发表下看法。

个人测试结论:

1.对于固定存储空间的数据列进行更新时,由于数据长度肯定不会发生变化,因此直接修改数据,slotID 不会发生变化,行在页上的位置不发生变化。

2.对于不固定存储空间的数据列进行更新时,可能会直接update数据,也可能先删除在插入数据行(即使更新后的数据长度比更新前要小,也可能会导致先删除再更新的情况),slotID 不会发生变化,删除插入会导致行在页上的位置发生变化,即行在页上的偏移量发生变化。

3.对于不固定存储空间的数据列进行更新时,即使更新行所在位置后面有充足空间(未被其他数据行使用),也可能发生删除插入的情况。

4. 无论是在原位置上直接更新还是删除插入导致行偏移量变化,都不会记录日志

有兴趣的同志可以阅读下这篇:http://www.cnblogs.com/wwwwgou/

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

参考来源:

http://www.cnblogs.com/nzperfect/archive/2012/12/12/2814554.html

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

妹子来啦

曲苑杂坛--数据库更新探秘