一、寫在前面 - 想說愛你不容易
為了升級數據庫至SQL Server 2008 R2,拿了一台現有的PC做測試,數據庫從正式庫Restore(3個數據庫大小誇張地達到100G+),而機器內存只有可憐的4G,不僅要承擔DB Server角色,同時也要作為Web Server,可想而知這台機器的命運是及其慘烈的,只要MS SQL Server一啟動,內存使用率立馬飆升至99%。沒辦法,只能升內存,兩根8G共16G的內存換上,結果還是一樣,內存瞬間被秒殺(CPU利用率在0%徘徊)。由於是PC機,內存插槽共倆,目前市面上最大的單根內存為16G(價格1K+),就算買回來估計內存還是不夠(卧槽,PC機傷不起啊),看樣子別無它法 -- 刪數據!!!
刪除數據 - 說的容易, 不就是DELETE嗎?靠,如果真這么干,我XXX估計能“知道上海凌晨4點的樣子”(KB,Sorry,誰讓我是XXX的Programmer,哥在這方面絕對比你牛X),而且估計會暴庫(磁盤空間不足,產生的日志文件太大了)。
二、沙場點兵 - 眾里尋他千百度
為了更好地闡述我所遇到的困難和問題,有必要做一些必要的測試和說明,同時這也是對如何解決問題的一種探究。因為畢竟這個問題的根本是如何來更好更快的操作數據,說到底就是DELETE、UPDATE、INSERT、TRUNCATE、DROP等的優化操作組合,我們的目的就是找出最優最快最好的方法。為了便於測試,准備了一張測試表tmp_employee
--Create table tmp_employee CREATE TABLE [dbo].[tmp_employee] ( [EmployeeNo] INT PRIMARY KEY, [EmployeeName] [nvarchar](50) NULL, [CreateUser] [nvarchar](50) NULL, [CreateDatetime] [datetime] NULL );
1. 數據插入PK
1.1. 循環插入,執行時間為38026毫秒
--循環插入 SET STATISTICS TIME ON; DECLARE @Index INT = 1; DECLARE @Timer DATETIME = GETDATE(); WHILE @Index <= 100000 BEGIN INSERT [dbo].[tmp_employee](EmployeeNo, EmployeeName, CreateUser, CreateDatetime) VALUES(@Index, 'Employee_' + CAST(@Index AS CHAR(6)), 'system', GETDATE()); SET @Index = @Index + 1; END SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
1.2. 事務循環插入,執行時間為6640毫秒
--事務循環 BEGIN TRAN; SET STATISTICS TIME ON; DECLARE @Index INT = 1; DECLARE @Timer DATETIME = GETDATE(); WHILE @Index <= 100000 BEGIN INSERT [dbo].[tmp_employee](EmployeeNo, EmployeeName, CreateUser, CreateDatetime) VALUES(@Index, 'Employee_' + CAST(@Index AS CHAR(6)), 'system', GETDATE()); SET @Index = @Index + 1; END SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF; COMMIT;
1.3. 批量插入,執行時間為220毫秒
SET STATISTICS TIME ON; DECLARE @Timer DATETIME = GETDATE(); INSERT [dbo].[tmp_employee](EmployeeNo, EmployeeName, CreateUser, CreateDatetime) SELECT TOP(100000) EmployeeNo = ROW_NUMBER() OVER (ORDER BY C1.[OBJECT_ID]), 'Employee_', 'system', GETDATE() FROM SYS.COLUMNS AS C1 CROSS JOIN SYS.COLUMNS AS C2 ORDER BY C1.[OBJECT_ID] SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
1.4. CTE插入,執行時間也為220毫秒
SET STATISTICS TIME ON; DECLARE @Timer DATETIME = GETDATE(); ;WITH CTE(EmployeeNo, EmployeeName, CreateUser, CreateDatetime) AS( SELECT TOP(100000) EmployeeNo = ROW_NUMBER() OVER (ORDER BY C1.[OBJECT_ID]), 'Employee_', 'system', GETDATE() FROM SYS.COLUMNS AS C1 CROSS JOIN SYS.COLUMNS AS C2 ORDER BY C1.[OBJECT_ID] ) INSERT [dbo].[tmp_employee] SELECT EmployeeNo, EmployeeName, CreateUser, CreateDatetime FROM CTE; SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
小結:
- 按執行時間,效率依次為:CTE和批量插入效率相當,速度最快,事務插入次之,單循環插入速度最慢;
- 單循環插入速度最慢是由於INSERT每次都有日志,事務插入大大減少了寫入日志次數,批量插入只有一次日志,CTE的基礎是CLR,善用速度是最快的。
2. 數據刪除PK
2.1. 循環刪除,執行時間為1240毫秒
SET STATISTICS TIME ON; DECLARE @Timer DATETIME = GETDATE(); DELETE FROM [dbo].[tmp_employee]; SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
2.2. 批量刪除,執行時間為106毫秒
SET STATISTICS TIME ON; DECLARE @Timer DATETIME = GETDATE(); SET ROWCOUNT 100000; WHILE 1 = 1 BEGIN BEGIN TRAN DELETE FROM [dbo].[tmp_employee]; COMMIT IF @@ROWCOUNT = 0 BREAK; END SET ROWCOUNT 0; SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
2.3. TRUNCATE刪除,執行時間為0毫秒
SET STATISTICS TIME ON; DECLARE @Timer DATETIME = GETDATE(); TRUNCATE TABLE [dbo].[tmp_employee]; SELECT DATEDIFF(MS, @Timer, GETDATE()) AS [執行時間(毫秒)]; SET STATISTICS TIME OFF;
小結:
- TRUNCATE太快了,清除10W數據一點沒壓力,批量刪除次之,最后的DELTE太慢了;
- TRUNCATE快是因為它屬於DDL語句,只會產生極少的日志,普通的DELETE不僅會產生日志,而且會鎖記錄。
三、磨刀霍霍 - 猶抱琵琶半遮面
由上面的第二點我們知道,插入最快和刪除最快的方式分別是批量插入和TRUNCATE,所以為了達到刪除大數據的目的,我們也將采用這兩種方式的組合,其中心思想是先把需要保留的數據存放之新表中,然后TRUNCATE原表中的數據,最后再批量把數據插回去,當然實現方式也可以隨便變通。
1. 保留需要的數據之新表中->TRUNCATE原表數據->還原之前保留的數據之原表中
腳本類似如下
SELECT * INTO #keep FROM Original WHERE CreateDate > '2011-12-31' TRUNCATE TABLE Original INSERT Original SELECT * FROM #keep
第一條語句會把所有要保留的數據先存放至表#keep中(表#keep無需手工創建,由SELECT INTO生效),#keep會Copy原始表Original的表結構。PS:如果你只想創建表結構,但不拷貝數據,則對應的腳本如下
SELECT * INTO #keep FROM Original WHERE 1 = 2
第二條語句用於清除整個表中數據,產生的日志文件基本可以忽略;第三條語句用於還原保留數據。
幾點說明:
- 你可以不用SELECT INTO,自己通過寫腳本(或拷貝現有表)來創建#keep,但是后者有一個弊端,即無法通過SQL腳本來獲得對應的表生成Script(我的意思是和原有表完全一致的腳本,即基本列,屬性,索引,約束等),而且當要操作的表比較多時,估計你肯定會抓狂;
- 既然第一點欠妥,那考慮新建一個同樣的數據庫怎么樣?既可以使用現有腳本,而且生成的數據庫基本一致,但是我告訴你最好別這么做,因為第一要跨庫,第二,你得准備足夠的磁盤空間。
2. 新建表結構->批量插入需要保留的數據->DROP原表->重命名新表為原表
CREATE TABLE #keep AS (xxx) xxx -- 使用上面提到的方法(使用既有表的創建腳本),但是不能夠保證完全一致;
INSERT #keep SELECT * FROM Original where clause
DROP TBALE Original
EXEC SP_RENAME '#keep','Original'
這種方式比第一種方法略快點,因為省略了數據還原(即最后一步的數據恢復),但是稍微麻煩點,因為你需要創建一張和以前原有一模一樣的表結構,包括基本列、屬性、約束、索性等等。
三、數據收縮 - 秋風少落葉
數據刪除后,發現數據庫占用空間大小並沒有發生變化,此時我們就用借助強悍的數據收縮功能了,腳本如下,運行時間不定,取決於你的數據庫大小,多則幾十分鍾,少則瞬間秒殺
DBCC SHRINKDATABASE(DB_NAME)
出處:https://www.cnblogs.com/panchunting/archive/2013/04/27/SQL_Tech_001.html
=====================================================================================
在SQL Server中,如何快速刪除大表中的數據呢? 回答這個問題前,我們必須弄清楚上下文環境和以及結合實際、具體的需求,不同場景有不同的應對方法。
1: 整張表的數據全部刪除
如果是整張表的數據全部清空、刪除,這種場景倒是非常簡單,TRUNCATE TABLE肯定是最快的。 反而用DELETE處理的話,就是一個糟糕的策略。
2: 大表中刪除一部分數據
對於場景1、非常簡單,但是很多實際業務場景,並不能使用TRUNCATE這種方法,實際情況可能只是刪除表中的一部分數據或者進行數據歸檔后的刪除。假設我們遇到的表為TEST,需要刪除TEST表中的部分數據。那么首先我們需要對表的數據量和被刪除的數據量做一個匯總統計,具體,我們應該采用下面方法:
· 檢查表的數據量,以及要刪除的數據量。然后計算刪除的比例,
sp_spaceused'dbo.TEST';
SELECT COUNT(*) AS DELETE_RCD WHERE TEST WHERE ......<刪除條件>
2.1 刪除大表中絕大部分的數據,但是這個絕大部分怎么定義不好量化,所以我們這里就量化為60%。如果刪除的數據比例超過60%,就采用下面方法:
1: 新建表TEST_TMP
2: 將要保留的數據轉移到TEST_TMP
3: 將原表TEST重命名為TEST_OLD, 而將TEST_TMP重命名為TEST
4: 檢查相關的觸發器、約束,進行觸發器或約束的重命名
5: 核對操作是否正確后,原表(TEST_OLD)要么TRUANCATE后,再DROP掉。要么保留一段時間,保險起見。
注:至於這個比例60%是怎么來的。這個完全是個經驗值,有簡單的測試,但是沒有很精確和科學的概率統計驗證。
另外,還要考慮業務情況,如果一直有應用程序訪問這個表,其實這種方式也是比較麻煩的,因為涉及數據的一致性,業務中斷等等很多情況。但是,如果程序較少訪問,或者在某個時間段沒有訪問,那么完全可以采用這種方法。
2.2 刪除大表中部分數據,如果比例不超過60%
1:先刪除或禁用無關索引(無關索引,這里指執行計划不用到的索引,這里是指對當前DELETE語句無用的索引)。因為DELETE操作屬於DML操作,而且大表的索引一般也非常大,大量DELETE將會對索引進行維護操作,產生大量額外的IO操作。
2:用小批量,分批次刪除(批量刪除比一次性刪除性能要快很多)。不要一次性刪除大量數據。一次性刪除大量記錄。會導致鎖的粒度范圍很大,並且鎖定的時間非常長,而且還可能產生阻塞,嚴重影響業務等等。而且數據庫的事務日志變得非常大。執行的時間變得超長,性能非常糟糕。
批量刪除時,到底一次性刪除多少數量的記錄數,SQL效率最高呢? 這個真沒有什么規則計算,個人測試對比過,一次刪除10000或100000,沒有發現什么特別規律。(有些你發現的“規律”,換個案例,發現不一樣的結果,這個跟環境有關,有時候可能是一個經驗值)。不過一般用10000,在實際操作過程,個人建議可以通過做幾次實驗對比后,選擇一個合適的值即可。
案例1:
DECLARE @delete_rows INT;
DECLARE @delete_sum_rows INT =0;
DECLARE @row_count INT=100000
WHILE 1 = 1
BEGIN
DELETE TOP ( @row_count )
FROM dbo.[EmployeeDayData]
WHERE WorkDate < CONVERT(DATETIME, '2012-01-01 00:00:00',120);
SELECT @delete_rows = @@ROWCOUNT;
SET @delete_sum_rows +=@delete_rows
IF @delete_rows = 0
BREAK;
END;
SELECT @delete_sum_rows;
案例2:
DECLARE @r INT;
DECLARE @Delete_ROWS BIGINT;
SET @r = 1;
SET @Delete_ROWS =0
WHILE @r > 0
BEGIN
BEGIN TRANSACTION;
DELETE TOP (10000) -- this will change
YourSQLDba..YdYarnMatch
WHERE Remark='今日未入' and Operation_Date<CONVERT(datetime, '2019-05-30',120);
SET @r = @@ROWCOUNT;
SET @Delete_ROWS += @r;
COMMIT TRANSACTION;
PRINT(@Delete_ROWS);
END
該表有下面兩個索引
USE [YourSQLDba]
GO
IF EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[YdYarnMatch]') AND name = N'IX_YdYarnMatch_N2')
DROP INDEX [IX_YdYarnMatch_N2] ON [dbo].[YdYarnMatch] WITH ( ONLINE = OFF )
GO
USE [YourSQLDba]
GO
CREATE NONCLUSTERED INDEX [IX_YdYarnMatch_N2] ON [dbo].[YdYarnMatch]
(
[Job_No] ASC,
[GK_No] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
USE [YourSQLDba]
GO
IF EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[YdYarnMatch]') AND name = N'IX_YdYarnMatch_N1')
DROP INDEX [IX_YdYarnMatch_N1] ON [dbo].[YdYarnMatch] WITH ( ONLINE = OFF )
GO
USE [YourSQLDba]
GO
CREATE NONCLUSTERED INDEX [IX_YdYarnMatch_N1] ON [dbo].[YdYarnMatch]
(
[Operation_Date] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
重點:實踐證明,如果新建一個索引,能夠避免批量刪除過程中執行計划走全表掃描,也能大大加快刪除的速度。個人對這個案例進行了測試、驗證。發現加上合適索引后,讓DELETE語句走Index Seek后,刪除效率確實大大提升。
刪除索引IX_YdYarnMatch_N2,保留索引IX_YdYarnMatch_N1,但是發現SQL執行計划走全表掃描,執行SQL時,刪除非常慢
刪除索引IX_YdYarnMatch_N1,重新創建索引IX_YdYarnMatch_N1后,執行計划走Index Seek,刪除效率大大提示。
CREATENONCLUSTEREDINDEX [IX_YdYarnMatch_N1] ON [dbo].[YdYarnMatch]
(
[Operation_Date] ASC,
Remark
)
注意:此處索引名相同,但是索引對應的字段不一樣。
所以正確的做法是:
1:先刪除或禁用無關索引(對當前DELETE語句無用的索引),刪除前生成對應的SQL,以便完成數據刪除后,重新創建索引。注意,前提是在操作階段,這個操作不會影響應用。否則應重新考慮。
2:檢查測試當前SQL的執行計划,能否創建合適的索引,加快DELETE操作。如上面例子所示
3:批量循環刪除記錄。
4:在ORACLE數據庫中,有些表的設置可以減少對應DML操作的日志生成量,但是SQL Server沒有這些功能,但是要及時關注或調整事務日志的備份情況。
如果我們能將將數據庫的恢復模式設置為SIMPLE,那么可以減少日志備份引起的額外的IO開銷。但是很多生產環境不能切換用戶數據庫的恢復模式。
其實說了這么多,SQL Server中大表快速刪除索引的方法就是將一次性刪除改成分批刪除,逐次提交而已。其它的方式都是一些輔助方式而已。另外,如果你想親自做一些細節測試,建議參考博客https://sqlperformance.com/2013/03/io-subsystem/chunk-deletes
出處:https://www.cnblogs.com/kerrycode/p/12448322.html
========================================================================
我自己進行了部分優化,更適合自己的項目應用
DECLARE @currDelCount INT; DECLARE @totalCount INT; DECLARE @actualCount BIGINT; set @totalCount = 1000; --計划最多需要刪除多少條記錄 set @currDelCount = 1; --當前循環中實際刪除的行數 set @actualCount = 0; --事件總的刪除行數 PRINT 'plan delete : '+ cast(@totalCount as varchar) WHILE (@totalCount > 0 and @currDelCount != 0) BEGIN BEGIN TRANSACTION; DELETE TOP (100) -- this will change from Employee2 --WHERE Remark='今日未入' and Operation_Date<CONVERT(datetime, '2019-05-30',120); where id<54040 SET @currDelCount = @@ROWCOUNT; SET @actualCount += @currDelCount; SET @totalCount -= @currDelCount; COMMIT TRANSACTION; PRINT ' ====== Plan to delete remaining quantity : '+ cast(@totalCount as varchar) PRINT ' ====== Actual deleted quantity: '+ cast(@actualCount as varchar) + char(10) END