2011-12-26 80 views
98

我正在使用1000個INSERT語句之間的性能對比:1000個值多個INSERT語句與多個值單INSERT

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0) 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1) 
... 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999) 

..versus使用單INSERT語句:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0), 
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1), 
... 
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999) 

令我大吃一驚的是,結果與我的想法相反:

  • 1000 INSERT語句:290毫秒。
  • 1 INSERT語句有1000個VALUES:2800毫秒。

測試與SQL Server Profiler中直接執行在MSSQL Management Studio中用於測量(和我使用的SqlClient,這更加suprising考慮所有的DAL層往返了從C#代碼運行它類似的結果)

這是否合理或以某種方式解釋?如何來,一個據說更快的方法導致10倍(!)更差的性能?

謝謝。

編輯:兩個附加執行計劃: Exec Plans

+1

這些都是乾淨的測試中,沒有什麼是並行執行,沒有重複的數據(每個查詢都有不同的數據,當然爲了避免簡單的緩存) – Borka

+1

有沒有涉及到任何觸發器? –

+1

我將一個程序轉換爲TVP以超過1000的限制值,並獲得了很大的性能提升。我會進行比較。 – Paparazzi

回答

107

增加: SQL Server 2012的顯示在這方面的一些改進性能,但似乎並沒有解決下面指出的具體問題。這 應在apparently be fixed在下一個主要版本 SQL Server 2012!

您的計劃顯示單個插入正在使用參數化過程(可能是自動參數化),因此這些解析/編譯時間應該是最小的。

我想我會更多地關注一下這個循環(script),並嘗試調整VALUES子句的數量並記錄編譯時間。

然後,我將編譯時間除以行數以獲得每個子句的平均編譯時間。結果如下

Graph

直到250項VALUES條款提出條款的編譯時間/數量有輕微上升趨勢,但沒有太戲劇性。

Graph

但後來有一個突然的變化。

該部分數據如下所示。

+------+----------------+-------------+---------------+---------------+ 
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows | 
+------+----------------+-------------+---------------+---------------+ 
| 245 |   528 |   41 |   2400 | 0.167346939 | 
| 246 |   528 |   40 |   2416 | 0.162601626 | 
| 247 |   528 |   38 |   2416 | 0.153846154 | 
| 248 |   528 |   39 |   2432 | 0.157258065 | 
| 249 |   528 |   39 |   2432 | 0.156626506 | 
| 250 |   528 |   40 |   2448 | 0.16   | 
| 251 |   400 |   273 |   3488 | 1.087649402 | 
| 252 |   400 |   274 |   3496 | 1.087301587 | 
| 253 |   400 |   282 |   3520 | 1.114624506 | 
| 254 |   408 |   279 |   3544 | 1.098425197 | 
| 255 |   408 |   290 |   3552 | 1.137254902 | 
+------+----------------+-------------+---------------+---------------+ 

線性增長的緩存計劃大小突然下降,但CompileTime增加7倍,CompileMemory增加。這是作爲自動參數化計劃(具有1,000個參數)的計劃與非參數化計劃之間的切點。此後,它看起來線性效率較低(根據給定時間內處理的值條款的數量)。

不知道這是爲什麼。據推測,當它編譯特定字面值的計劃時,它必須執行一些不能線性縮放的活動(如排序)。

當我嘗試一個完全由重複行組成的查詢時,它似乎不會影響緩存查詢計劃的大小,也不會影響常量表的輸出順序(並且當您插入到無論如何,花在分揀上的時間毫無意義,即使這樣做也是毫無意義的)。

而且如果一個聚集索引添加到表的計劃仍然顯示一個明確的排序步驟,它似乎並沒有在編譯時被排序,以避免一種在運行時。

Plan

我想看看這一個調試器,但似乎對我的版本的SQL Server 2008中的公共符號不提供這樣反而我不得不看的等同UNION ALL建設中的SQL Server 2005年

一個典型的堆棧跟蹤低於

sqlservr.exe!FastDBCSToUnicode() + 0xac bytes 
sqlservr.exe!nls_sqlhilo() + 0x35 bytes  
sqlservr.exe!CXVariant::CmpCompareStr() + 0x2b bytes 
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare() + 0x18 bytes 
sqlservr.exe!CXVariant::CmpCompare() + 0x11f67d bytes 
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion() + 0xe2 bytes 
sqlservr.exe!CConstraintProp::PcnstrUnion() + 0x35e bytes 
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive() + 0x11a bytes  
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler() + 0x18f bytes  
sqlservr.exe!CLogOpArg::DeriveGroupProperties() + 0xa9 bytes 
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties() + 0x40 bytes  
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x18a bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!CQuery::PqoBuild() + 0x3cb bytes 
sqlservr.exe!CStmtQuery::InitQuery() + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal() + 0xf0 bytes 
sqlservr.exe!CStmtDML::Init() + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep() + 0x176 bytes 
sqlservr.exe!CSQLSource::FCompile() + 0x741 bytes 
sqlservr.exe!CSQLSource::FCompWrapper() + 0x922be bytes  
sqlservr.exe!CSQLSource::Transform() + 0x120431 bytes 
sqlservr.exe!CSQLSource::Compile() + 0x2ff bytes 

所以打算關閉在堆棧中的名稱跟蹤它似乎花了很多時間進行比較的字符串。

This KB article表示DeriveNormalizedGroupProperties與過去被稱爲normalization階段查詢處理的

這一階段是現在所謂的結合或algebrizing,它需要從先前的解析階段的表達解析樹輸出,並輸出相關聯的algebrized表達式樹(查詢處理器樹)前進,以優化(在這種情況下,瑣碎計劃優化)[ref]

我想多了一個實驗(Script),這是重新運行原有的測試,但看着三種不同的情況。

  1. 名字和姓氏字符串長度爲10個字符,沒有重複。
  2. 名字和姓氏長度爲50個字符的字符串,沒有重複。
  3. 名字和姓氏長度爲10個字符的字符串,包含所有重複項。

Graph

可以清楚地看出,長串的更糟糕的事情就和相反的更多的副本更好的東西拿到。如前所述,重複項不會影響緩存的計劃大小,因此我認爲在構建代數表達式樹本身時必須存在重複識別過程。

編輯

一個地方這個信息是槓桿是shown by @Lieven here

SELECT * 
FROM (VALUES ('Lieven1', 1), 
      ('Lieven2', 2), 
      ('Lieven3', 3))Test (name, ID) 
ORDER BY name, 1/ (ID - ID) 

因爲在編譯時它能夠確定Name列沒有重複它跳過通過在次級1/ (ID - ID)表達訂購運行時間(計劃中的排序只有一個ORDER BY列),並且不會產生除零錯誤。如果將重複項添加到表中,則排序運算符按列顯示兩個順序,並引發預期的錯誤。

+5

您擁有的幻數是NumberOfRows/ColumnCount = 250.將您的查詢更改爲僅使用三列,並且更改將在333處發生。幻數1000可能與緩存計劃中使用的最大參數數量類似。使用''來生成一個計劃比使用'列表的計劃更容易「。 –

+1

@MikaelEriksson - 同意。具有1000個值的250行一行得到自動參數化,251行不這樣看起來是不同的。不知道爲什麼。也許它花費時間對文字值進行排序,以尋找重複或其他東西。 –

+1

這是一個非常瘋狂的問題,我只是因爲它而感到難過。這是一個很好的答案,謝謝 –

21

這不是太奇怪:對於微小的插入執行計劃被計算一次,然後重複使用1000次。解析和準備計劃很快,因爲它只有四個值可以刪除。另一方面,1000行計劃需要處理4000個值(如果參數化了C#測試,則需要4000個參數)。這可以輕鬆節省您通過消除999次往返SQL Server所節省的時間,尤其是在您的網絡速度不夠慢的情況下。

9

該問題可能與編譯查詢所花費的時間有關。

如果你想加快插入,你真正需要做的是將它們包裝在一個事務:

BEGIN TRAN; 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0); 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1); 
... 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999); 
COMMIT TRAN; 

從C#中,您也可以考慮使用表值參數。在一個批處理中發出多個命令,用分號分隔它們是另一種方法,這也將有所幫助。

+1

回覆:「頒發多個命令在一個批次中「:這有點幫助,但不是很多。但我絕對同意其他兩種選擇:在TRANSACTION中進行包裝(TRANS實際上是工作還是應該只是TRAN?)或使用TVP。 –

1

我遇到過類似的情況,試圖用一個C++程序(MFC/ODBC)轉換一個帶有幾個100k行的表。

由於此操作花費了很長時間,我想將多個插入事件綁定到一個(由於MSSQL limitations而最多可以有1000個)。我猜想大量的單插入語句會產生類似於here所述的開銷。

然而,事實證明,轉換居然拿了相當長的時間:

 Method 1  Method 2  Method 3 
     Single Insert Multi Insert Joined Inserts 
Rows 1000   1000   1000 
Insert 390 ms   765 ms  270 ms 
per Row 0.390 ms  0.765 ms  0.27 ms 

所以,1000所單調用的CDatabase ::的ExecuteSQL每一個INSERT語句(方法1)的大約兩倍使用帶有1000個值元組的多行INSERT語句(方法2)對CDatabase :: ExecuteSql進行單個調用的速度很快。

更新:因此,我嘗試的下一件事是將1000個單獨的INSERT語句捆綁到一個字符串中並讓服務器執行該方法(方法3)。事實證明,這是比方法快甚至有點1.

編輯:我使用的是Microsoft SQL Server速成版(64位)v10.0.2531.0