測試和分析
依據上文件最小化日志的判斷邏輯,對常見的BULK INSERT和INSERT INTO...SELECT做測試和分析
創建測試環境和基准

--創建表tb_source並插入10000條數據 use master go create database test; alter database test set recovery bulk_logged with no_wait; go use test go create table tb_source (id int,val char(100)); insert into tb_source select top(10000) ROW_NUMBER() over (order by sysdatetime()),'HeHe' from master..spt_values a,master..spt_values b go 創建基准表tb_benchmark,將tb_source的數據導出到文件,再導入到基准表。然后獲取最小化日志的統計做為測試基准。 create table tb_benchmark (id int,val char(100)); /** CMD中導出數據: C:\Users\Administrator>bcp test.dbo.tb_source out D:\ss\source.csv -S. -T -c **/ --導入數據並並統計日志 create table tb_benchmark (id int,val char(100)); bulk insert tb_benchmark from 'd:\ss\source.csv' with (tablock) ; SELECT COUNT(*)AS numrecords, CAST((COALESCE(SUM([Log Record LENGTH]), 0)) / 1024. / 1024. AS NUMERIC(12, 2)) AS size_mb FROM sys.fn_dblog(NULL, NULL) AS D WHERE AllocUnitName = 'dbo.tb_benchmark' OR AllocUnitName LIKE 'dbo.tb_benchmark.%'; SELECT Operation, Context, AVG([Log Record LENGTH]) AS AvgLen, COUNT(*) AS Cnt FROM sys.fn_dblog(NULL, NULL) AS D WHERE AllocUnitName = 'dbo.tb_benchmark' OR AllocUnitName LIKE 'dbo.tb_benchmark.%' GROUP BY Operation, Context, ROUND([Log Record LENGTH], -2) ORDER BY AvgLen, Operation, Context;
從結果可以看出插入10000行,只產生了170條日志。沒有大於行大小(>104)的日志記錄。確定是最小化日志記錄。
1.bulk insert非空堆表
前面的基准測試可以看到空堆表的insert,最小化日志記錄成功。非空的話,向基准表再導入一次數據。
從結果看,最小日志也是成立的。
2. bulk insert空聚集表

create table tb_btree1 (id int ,val char(100)) create clustered index cix_tb_btree1 on tb_btree1 (id) go bulk insert tb_btree1 from 'd:\ss\source.csv' with (tablock)
這個最小化也是成立的,日志記錄多於空堆表的情況。
使用TF-610,而不使用tablock:
create table tb_btree2 (id int ,val char(100)) create clustered index cix_tb_btree2 on tb_btree2 (id) go dbcc traceon(610) bulk insert tb_btree2 from 'd:\ss\source.csv'
這種情況下不並完全是最小化日志記錄。從測試來看空聚集索引使用tablock產生的日志量會更少一些。這里為什么會有71行插入是完整日志記錄的呢?一個表至少有一個數據頁,向已有的數據頁上插入行是完整日志記錄,新分配的頁是最小日志記錄。至於為什么是71行,下面3.非空聚集表中一起分析。
3. bulk insert非空聚集表
先創建表tb_btree,然后向其中插入60條記錄。觀察完整日志記錄的情況。
create table tb_btree (id int ,val char(100)) create clustered index cix_tb_btree on tb_btree (id) go declare @i int=0 while @i<60 begin set @i=@i+1 insert into tb_btree values(@i,'HaHa') end
插入60條記錄,有64條日志記錄。聚集索引日志60條,平均長度212(大於104)。
然后再向tb_tree中插入數據,對比日志情況。注意:導入數據我是從ID=61開始導入的。因為原表中有ID=[1,60]的行了,如果導入數據重復,會發生行移動和頁拆分等操作,這樣就會增加很多額外的日志,不便分析。
dbcc traceon(610) bulk insert tb_btree from 'd:\ss\source.csv' with (ORDER(ID),FIRSTROW=61)
與前面對比,可以看出新插入9960行數據,只新增了913條日志。有意思的是,索引葉級頁插入(LOP_INSERT_ROWS&LCX_CLUSTERED)增加了11條,這11條是完整日志記錄的,其它行插入是最小化日志插入的。
這是為什么呢?
這是因為原來tb_btree中只有一個數據頁,且只存放了60行數據。而這個數據頁上最多只能存放71行數據。也就是,在已經存在的數據頁中插入數據是完整日志記錄的,新分配的數據頁插入數據是最小化日志記錄的。
為什么最多只能存放71條數據?
直接通過DBCC PAGE查看對應數據頁最直觀。或者通過理論來計算:
通過dbcc showcontig ('tb_btree') with tableresults得到行大小為111。假設頁上可以存N行數據,
則:頁頭+偏移矩陣+行容量<=8KB-->96+2*N+111*N<=8192-->N<=71.65-->N=71
4. INSERT INTO...WITH(TABLOCK)...SELECT,向堆表中插入數據
--空堆表的情況,確認是最小化日志。
create table tb_heap1 (id int ,val char(100)) ; INSERT INTO tb_heap1 with(tablock) select * from tb_source;
--非空堆表,將上面的數據再插入一遍即可。確認是最小化日志。
INSERT INTO tb_heap1 with(tablock) select * from tb_source;
5. INSERT INTO...SELECT...ODER BY(...),向聚集表中插入數據
--空的聚集表 create table tb_cix (id int ,val char(100)) create clustered index cix_tb_cix on tb_cix (id) go dbcc traceon(610) insert into tb_cix select * from tb_source order by id
同樣,已有的數據頁上是完整日志記錄,其它是最小化日志記錄。
--非空聚集表
--先插入70行數據,是完整日志記錄的。 create table tb_cix2 (id int ,val char(100)) create clustered index cix_tb_cix2 on tb_cix2 (id) go declare @i int=0 while @i<70 begin set @i=@i+1; insert into tb_cix2 values(@i,'HoHo') end
--再從tb_source插入ID>70的9930行 dbcc traceon(610) insert into tb_cix2 select * from tb_source where id>70 order by id
可以看到只新了一條完整日志的記錄,其它是最小化日志記錄的。
6. INSERT INTO...SELECT...ODER BY(...),並行導入聚集表
前文提到SQL 2008之后結合鍵范圍鎖,不會鎖定整個表,只鎖定某部分的鍵值區間,其它操作可以並行訪問此區間外的數據。
這里我打開三個session,同時插入三個區間的數據[1,1000],[3000,5000],[7000,9000]
create table tb_cix3 (id int ,val char(100)) create clustered index cix_tb_cix3 on tb_cix3 (id) go --session 1 dbcc traceon(610) insert into tb_cix3 select * from tb_source where id between 1 and 1000 order by id --session 2 dbcc traceon(610) insert into tb_cix3 select * from tb_source where id between 3000 and 5000 order by id option (querytraceon 2332) --session 3 dbcc traceon(610) insert into tb_cix3 select * from tb_source where id between 7000 and 9000 order by id option (querytraceon 2332)
並行導入最好用ETL工具實現,特別是數據排序這一步。我在測試時,踩到一個坑:
當第一個insert 完成后,其它的insert的oder by會失效,造成無法最小化日志。除了第一個被執行的insert外,其它的執行計划中不會有SORT操作符。只好使用TF-2332,強制數據修改操作進行排序,也就是querytraceon 2332。還有就是數據量太少,都是瞬間完成,不好控制並發。
7. 在SSIS實際導數中的一個應用簡單例子
這是實際項目中導數Destination的設置。一個聚集表導到另一個聚集表,實現了最小化日志。目標實例啟用了TF-610,FastLoadOptions設置為根據目標表聚集索引鍵進行排序。