原文:曲演杂坛--一条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. C语言文件操作函数大全

    http://blog.csdn.net/mu0206mu/article/details/18980913 clearerr(清除文件流的错误旗标) 相关函数 feof表头文件 #include&l ...

  2. XeTeX中文解决方案(temp)

    临时记录一下XeTeX的中文解决方案.一些包的文档只是走马观花得到的解决方法,所以可能有诸多纰漏. 另个人还是比较看好LuaTeX,但是在里边鼓捣中文还是一团糟,等探索一下再回来补充. 我使用的包是x ...

  3. Android KK台,联系人列表#集团放置A~Z之前

    更改文件ContactLocaleUtils.java两 (Path:packages/contactsprovider/src/com/android/providers/contacts) 1. ...

  4. 至Android虚拟机发送短信和拨打电话

    Android的emulator是已经包括了gsm 模块,能够模拟电话与短信进行调试(就不用花太多冤枉钱) 首先,肯定是打开虚拟机: emulator -avd XXXXXX -scale 0.8&a ...

  5. UML对象图和包图

    UML九已经介绍过的基本图,然后,我们再来看看对象图和包图.  一.对象图 谈到对象.我们不得不说一下对象.对象(Object)是对象类的实例(Instance),用于模型化特定的实体.对象是唯一的. ...

  6. DevExpress XtraReports 入门一 创建 Hello World 报表

    原文:DevExpress XtraReports 入门一 创建 Hello World 报表 本文只是为了帮助初次接触或是需要DevExpress XtraReports报表的人群使用的,为了帮助更 ...

  7. debugging python with IDLE

    1. start IDLE "Python 2.5"→"IDLE(Python GUI)" 2. open your source file window Fr ...

  8. 基于GruntJS前端性能优化

    在本文中,如何使用GruntJS为了使治疗简单的前端性能优化自己主动,我写了一个完整的样本放在Github上.能够參考一下.关于Yahoo的前端优化规则请參考:Best Practices for S ...

  9. js日期操作

    1.最基本的日期操作 var mydate = new Date(); set/get   FullYear,Month,Date,Hour,Minutes,Second可以随意拼接 toLocale ...

  10. linux通过key区别登陆的人

    key区分登录用户 脚本放 /etc/profile.d,会默认登录的时候执行, 类似于 #!/bin/bash # filename: /etc/profile.d/set_log_file.sh ...