曲演杂坛--一条DELETE引起的思考

场景介绍:数据库

咱们有一张表,专门用来生成自增ID供业务使用,表结构以下:服务器

CREATE TABLE TB001
(
    ID INT IDENTITY(1,1) PRIMARY KEY,
    DT DATETIME
)

每次业务想要获取一个新ID,就执行如下SQL:性能

INSERT INTO TB001(DT)
SELECT GETDATE();
SELECT @@IDENTITY

因为这些数据只需保留最近一天的数据,所以创建一个SQL做业来按期删除数据,删除脚本很简单:测试

DELETE TOP(10000) FROM TB001
WHERE DT<GETDATE()-1

做业每10秒运行一次,天天运行2个小时,最大能删除数据720W数据。优化

问题:spa

因为前台页面没有防刷机制,有恶意用户使用程序攻击,形成天天数据量暴增近1亿(是否是我也能够出去吹下NB!!!),当前做业没法删除这么庞大的数据,得进行调整.3d

 

解决思路:日志

在保证程序不修改的前提下,咱们首先想到的办法是:code

1:提升单次删除的数量,会形成锁阻塞,阻塞严重就会影响到业务,这没法接受;blog

2:延长整个做业运行周期,研发人员担忧影响白天正常业务,要求做业只能夜里低峰区进行

3:提升删除频率,能够考虑,但具体频率须要测试

 

因为方法2只能少许的增长,所以咱们集中在方法3的测试上,因为SQL Agent Job的最小周期是10秒,所以在做业调用的脚本上修改,每次做业调用多条删除语句,删除语句中间使用WAITFOR来间歇执行:

DELETE FROM TB001 WHERE DT<GETDATE()-1 WAITFOR DELAY '0:0:05' DELETE FROM TB001 WHERE DT<GETDATE()-1

测试运行时,发现对业务影响不大,所以就上线修改。

结果半夜做业运行后,研发当即收到报警,程序访问延时严重,到服务器上一查,锁等待超过500000多毫秒,sys.dm_exec_requests中显示有300多回话等待同一个锁资源,停掉做业后程序立马回复正常。

让咱们来测试下这是为啥呢?

首先准备测试数据

CREATE TABLE TB001
(
    ID INT IDENTITY(1,1) PRIMARY KEY,
    DT DATETIME
)
GO
INSERT INTO TB001(DT)
SELECT GETDATE()-1 FROM SYS.all_columns
GO
INSERT INTO TB001
SELECT GETDATE()-1 FROM TB001
GO 13

而后尝试删除数据

BEGIN TRAN
DELETE TOP(10000)  FROM TB001
WHERE DT<GETDATE()-1

查看锁状况:

--上面事务的回话ID为55
sp_lock 55

单次删除数据太大,形成表锁,阻塞程序插入数据,解决办法:调整单次删除数量

PS: SQL SERVER会在行集上得到5000个锁时尝试锁升级,同时也会在内存压力下尝试锁升级。

因而咱们只能尝试更高的删除频率和更小的删除批量,因而将删除代码修改以下:

DECLARE @ID INT
SET @ID=0
WHILE(@ID<100)
BEGIN

DELETE TOP(100)  FROM TB001
WHERE DT<GETDATE()-1
WAITFOR DELAY '0:0:00:400' SET @ID=@ID+1 END

PS: 删除100行只是一个尝试值,应该没有一个最优的删除行数,牛逼的解释是设置该值需考虑:删除须要扫描多少页面/执行屡次时间/表上索引数量/写入多少日志/锁与阻塞等等,不装逼的解释就是多测试直到达到知足需求的值就好。
假设平均删除90行数据会写60k的日志,你删除100行致使须要两次物理写,这是何须呢?

 

使用修改后的版本测试了下,速度飞快,人生如此美好,哪还等啥,更新到生产服务器上,让暴风雨来得更猛烈些吧!!!

 

果真,这不是人生的终点,悲剧出现了,执行不稳定,原本40秒能执行完的SQL,有时候须要4分钟才能完成,这不科学啊,我都测试好几遍的呢!!!

 

细细看看语句,不怪别人,本身写的SQL垃圾,没办法,在看一遍代码:

DELETE TOP(100)  FROM TB001
WHERE DT<GETDATE()-1

这是按照业务逻辑写的,没有问题,可是的可是,DT上没有索引,因为表中DT和ID都是顺序增加的,按照主键ID的升序扫描,排在最前面的ID最小,其插入时间也最先,也是咱们删除的目标,所以只须要几回逻辑读即可以轻松找到知足条件的100行数据,所以消耗也最小,可是理想很丰满,现实很骨感,

在频繁地运行DELETE语句后,使用SET STATISTICS IO ON来查看,一样的执行计划:

可是形成的逻辑IO彻底不同,从4次到几千次,此现象在高频率删除下尤为明显(测试时能够连续运行10000次删除查看)

 

尝试其余写法,强制走ID索引扫描:

DECLARE @ID INT
SET @ID=0

WHILE(@ID<10000)
BEGIN

;WITH T1 AS(
SELECT TOP(100)* FROM TB001
WHERE DT<GETDATE()-1
ORDER BY ID
)
DELETE FROM T1


SET @ID=@ID+1
END

测试发现依然是一样问题,难道无解么?

再次研究业务发现,咱们能够查出一个要要删除的最大ID,而后删除小于这个ID的数据,并且能够避免一个潜在风险,因为DT没有索引,当一天前的数据被清除后,若是做业继续运行,要查找知足条件的100行数据来进行删除,便会对表进行一次全表扫描,消耗更庞大数量的逻辑IO。

DECLARE @MaxID INT

SELECT @MaxID=MAX(ID) 
FROM TB001 WITH(NOLOCK)
WHERE DT<GETDATE()

DECLARE @ID INT
SET @ID=0

WHILE(@ID<10000)
BEGIN

;WITH T1 AS(
SELECT TOP(100)* FROM TB001
WHERE ID<@MaxID
ORDER BY ID
)
DELETE FROM T1


SET @ID=@ID+1
END

从逻辑IO上看,性能没有明显提高,可是从CPU的角度来看,CPU的使用明显下降,猜想有两方面缘由:
1:日期比较消耗要大于INT(日期相似浮点数的存储,处理须要消耗额外的CPU资源)

2:因为ID索引排序的缘由,可能不须要对页的全部数据逐行比较来判断这些数据是否知足条件(我的猜想,请勿当真)

 

因为ID是自增连续的,虽然可能有由于事务回滚或DBA干预致使不连续的状况,但这不是重点,重点是咱们不必定要每次都删除100行数据,所以咱们能够按ID来进行区间删除,抛弃TOP的方式:

DECLARE @MaxID INT
DECLARE @MinID INT

SELECT @MaxID=MAX(ID),@MinID=MIN(ID)
FROM TB001 WITH(NOLOCK)
WHERE DT<GETDATE()-1

DECLARE @ID INT
SET @ID=0

WHILE(@ID<10000)
BEGIN

DELETE  FROM TB001
WHERE ID>=@MinID+@ID*100
AND ID<@MinID+(@ID+1)*100
AND ID<@MaxID


SET @ID=@ID+1
END

测试发现,每次删除的逻辑IO都很稳定且消耗很低,这才是最完美的东东啊!!

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

总结:

原本看似一个很简单的SQL,须要考虑不少方面,各类折腾,各类困惑,多看点基础原理的资料,没有坏处;大胆猜想,谨慎论证,多测试是验证推断的惟一办法;

 

提点额外话:

1. 关于业务:在不少时候,DBA不了解业务就进行优化,是很糟糕的事情,并且不少优化的最佳地方是程序而不是数据库,勇于否认开发人员所谓的“业务需求”也是DBA的一项必备技能。有一次优化发现,开发对上千万数据排序分页,问询开发获得答复“用户没有输入过滤条件”,难道用户不输入就不能设置点默认条件么?若是用户查询最新记录,咱们能够默认值查询最近三天的数据。

2. 关于场景:有一些初学者,很指望得到一些绝对性的推论,而不考虑场景的影响,且缺少测试,武断地下结论,这一样是很可怕的事情,适合你场景的解决方案,才是最佳的解决方案。

 

遗留问题:

1. 针对本文提到的业务场景,还有一些其余解决方案,好比分区方式,按期进行分区切换再删除数据,又好比使用SQL SERVER 2012中新增的“序列”;

2. 猜想上面所提到的问题根源是SQL Server删除行的实现方式,在删除时仅标示数据行被删除而不是真正的从页面删除,在高频率不间断地删除过程当中,这些数据页没有被及时回收删除掉,

SQL Server扫描了“本该”删除的数据页,形成逻辑读较高;而使用ID的区间范围查找,能够避免扫描到这些数据页,直接移动到真正须要访问的数据页;当删除频率较低时(好比3秒删除一次),这种问题就不会出现。

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

依旧是妹子:

相关文章
相关标签/搜索