場景介紹:數據庫
咱們有一張表,專門用來生成自增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秒刪除一次),這種問題就不會出現。
--=============================
依舊是妹子: