SQL Server系列之 刪除大量數據


一、寫在前面 - 想說愛你不容易

  為了升級數據庫至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等的優化操作組合,我們的目的就是找出最優最快最好的方法。為了便於測試,准備了一張測試表Employee

--Create table Employee
CREATE TABLE [dbo].[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].[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].[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].[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].[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].[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].[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].[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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM