原文:曲演杂坛--一条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数据。

问题:

由于前台页面没有防刷机制,有恶意用户使用程序攻击,造成每天数据量暴增近1亿(是不是我也可以出去吹下NB!!!),当前作业无法删除这么庞大的数据,得进行调整.

解决思路:

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

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

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秒删除一次),这种问题就不会出现。

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

依旧是妹子:

曲演杂坛--一条DELETE引发的思考的更多相关文章

  1. 曲演杂坛--当ROW_NUMBER遇到TOP

    值班期间研发同事打来电话,说应用有超时,上服务器上检查发现有SQL大批量地执行,该SQL消耗IO资源较多,导致服务器存在IO瓶颈,细看SQL,发现自己都被整蒙了,不知道这SQL是要干啥,处理完问题赶紧 ...

  2. 曲演杂坛--使用CTE时踩的小坑:No Join Predicate

    在一次系统优化中,意外发现一个比较“坑”的SQL,拿出来供大家分享. 生成演示数据: --====================================== --检查测试表是否存在 IF(O ...

  3. 曲演杂坛--为什么SELECT语句会被其他SELECT阻塞?

    很多刚入门的DBA在捕获阻塞得时候,会问这么一个问题“为什么这个SELECT语句被那个SELECT语句阻塞了,难道不是共享锁么?” 让我们来做个小测试,首先准备一些测试数据: --========== ...

  4. 曲演杂坛--SQLCMD下执行命令失败但没有任何错误提示的坑

    今天使用SQLCMD导入到SQL SERVER数据库中,看着数据文件都成功执行,但是意外发现有一个文件数据没有成功导入,但执行不报错,很容易导致问题被忽略. 使用存在问题的文件做下测试,从界面上看几行 ...

  5. 曲演杂坛--Update的小测试

    今天偶然想起一个UPDATE相关的小问题,正常情况下,如果我们将UPDATE改写成与之对应的SELECT语句,其SELECT查询结果应与UPDATE的目标表存在一对一的关系,例如: 对于UPDATE语 ...

  6. 曲演杂坛--使用ALTER TABLE修改字段类型的吐血教训

    --===================================================================== 事件起因:开发发现有表插入数据失败,查看后发现INT类型 ...

  7. 曲演杂坛--重建索引后,还使用混合分区么?(Are mixed pages removed by an index rebuild?)

    原文来自:http://www.sqlskills.com/blogs/paul/mixed-pages-removed-index-rebuild/ 在SQL SERVER 中,区是管理空间的基本单 ...

  8. 曲演杂坛--HASH的一点理解

    HASH,百度百科上做如下定义: Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列 ...

  9. 曲演杂坛--蛋疼的ROW_NUMBER函数

    使用ROW_NUMBER来分页几乎是家喻户晓的东东了,而且这东西简单易用,简直就是程序员居家必备之杀器,然而ROW_NUMBER也不是一招吃遍天下鲜的无敌BUG般存在,最近就遇到几个小问题,拿出来供大 ...

随机推荐

  1. oracle 10g操作和维护手册

    1.    检查数据库基本状况... 1.1.     检查Oracle实例状态... 1.2.     检查Oracle服务进程... 1.3.     检查Oracle监听状态... 2.    ...

  2. 【DataStructure】Some useful methods for arrays

    Last night it took me about two hours to learn arrays. For the sake of less time, I did not put emph ...

  3. 2014ACM/ICPC亚洲区域赛牡丹江站汇总

    球队内线我也总水平,这所学校得到了前所未有的8地方,因为只有两个少年队.因此,我们13并且可以被分配到的地方,因为13和非常大的数目.据领队谁oj在之上a谁去让更多的冠军.我和tyh,sxk,doub ...

  4. Linux:闪光的宝石,智慧(下一个)

    2005年4月7日.Linus Torvalds公布了一款新型通用工具软件包,叫做"Git"(the Git source code management system).&quo ...

  5. 在ASP.net中的UpdatePanel,弹窗失败解决办法

    原文:在ASP.net中的UpdatePanel,弹窗失败解决办法 最开始我用: Response.Write("<script>alert('和哈呵呵呵呵呵呵!')</s ...

  6. 网页上弹出pop窗口实例,(document).height()与$(window).height()的区别

    #dvbg{background-color:#666666; position:absolute; z-index:99; left:0; top:0; display:none; width:10 ...

  7. Tair LDB基于Prefixkey中期范围内查找性能优化项目总结

    "Tair LDB基于Prefixkey该范围内查找性能优化"该项目是仅一个月.这个月主要是熟悉项目..以下从几个方面总结下个人在该项目上所做的工作及自己的个人所得所感. 项目工作 ...

  8. SQL字符串转换为数组

    /*一.按指定符号分割字符串,返回分割后的元素个数,方法很简单,就是看字符串中存在多少个分隔符号,然后再加一,就是要求的结果. -----rtrim(@str)去掉 @str右边的字符 ltrim(@ ...

  9. C#-简单的定时器(C# ConsoleApp) ---ShinePans

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hpbmVwYW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA ...

  10. Swift中文教程(二)--简单值

    原文:Swift中文教程(二)--简单值 Swift使用let关键字声明常量,var关键字声明变量.常量无需在编译时指定,但至少要被赋值一次.也就是说,赋值一次多次使用: var myVariable ...