Nhiều câu lệnh CHÈN so với CHÈN đơn lẻ với nhiều GIÁ TRỊ


119

Tôi đang chạy so sánh hiệu suất giữa việc sử dụng 1000 câu lệnh 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 bằng cách sử dụng một câu lệnh INSERT với 1000 giá trị:

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)

Trước sự ngạc nhiên lớn của tôi, kết quả hoàn toàn ngược lại với những gì tôi nghĩ:

  • 1000 câu lệnh INSERT: 290 msec.
  • 1 câu lệnh INSERT với 1000 GIÁ TRỊ: 2800 msec.

Kiểm tra được thực hiện trực tiếp trong MSSQL Management Studio với SQL Server Profiler được sử dụng để đo lường (và tôi đã nhận được kết quả tương tự khi chạy nó từ mã C # bằng SqlClient, điều này thậm chí còn đáng ngạc nhiên hơn khi xem xét tất cả các lớp DAL)

Điều này có thể hợp lý hoặc bằng cách nào đó giải thích? Tại sao, một phương pháp được cho là nhanh hơn lại mang lại hiệu suất kém hơn 10 lần (!) ?

Cảm ơn bạn.

CHỈNH SỬA: Đính kèm các kế hoạch thực thi cho cả hai: Thực hiện kế hoạch


1
đây là những bài kiểm tra sạch sẽ, không có gì được thực hiện song song, không có dữ liệu lặp đi lặp lại (mỗi truy vấn là với dữ liệu khác nhau, tất nhiên, để tránh bộ nhớ đệm đơn giản)
Borka

1
có bất kỳ tác nhân nào liên quan không?
AK

2
Tôi đã chuyển đổi một chương trình sang TVP để vượt qua giới hạn 1000 giá trị và đạt được hiệu suất lớn. Tôi sẽ chạy một so sánh.
paparazzo

Câu trả lời:


126

Bổ sung: SQL Server 2012 cho thấy một số hiệu suất được cải thiện trong lĩnh vực này nhưng dường như không giải quyết được các vấn đề cụ thể được lưu ý bên dưới. Điều này rõ ràng sẽ được khắc phục trong phiên bản chính tiếp theo sau SQL Server 2012!

Kế hoạch của bạn cho thấy các chèn đơn đang sử dụng các thủ tục được tham số hóa (có thể được tham số tự động) vì vậy thời gian phân tích cú pháp / biên dịch cho các thủ tục này phải là tối thiểu.

Tôi nghĩ rằng tôi sẽ xem xét điều này nhiều hơn một chút, vì vậy hãy thiết lập một vòng lặp ( tập lệnh ) và thử điều chỉnh số lượng VALUESmệnh đề và ghi lại thời gian biên dịch.

Sau đó, tôi chia thời gian biên dịch cho số hàng để có được thời gian biên dịch trung bình cho mỗi mệnh đề. Kết quả ở bên dưới

Đồ thị

Cho đến khi có 250 VALUESmệnh đề, thời gian biên dịch / số mệnh đề có xu hướng tăng nhẹ nhưng không có gì quá kịch tính.

Đồ thị

Nhưng sau đó có một sự thay đổi đột ngột.

Phần dữ liệu đó được hiển thị bên dưới.

+------+----------------+-------------+---------------+---------------+
| 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   |
+------+----------------+-------------+---------------+---------------+

Kích thước kế hoạch được lưu trong bộ nhớ cache đang tăng lên một cách tuyến tính đột nhiên giảm xuống nhưng CompileTime tăng gấp 7 lần và CompileMemory tăng lên. Đây là điểm cắt giữa kế hoạch được tham số hóa tự động (với 1.000 tham số) với kế hoạch không được tham số hóa. Sau đó, nó dường như trở nên kém hiệu quả hơn một cách tuyến tính (về số lượng mệnh đề giá trị được xử lý trong một thời gian nhất định).

Không chắc chắn tại sao điều này nên được. Có lẽ khi nó đang biên soạn một kế hoạch cho các giá trị chữ cụ thể, nó phải thực hiện một số hoạt động không quy mô tuyến tính (chẳng hạn như sắp xếp).

Nó dường như không ảnh hưởng đến kích thước của kế hoạch truy vấn được lưu trong bộ nhớ cache khi tôi thử truy vấn bao gồm hoàn toàn các hàng trùng lặp và không ảnh hưởng đến thứ tự đầu ra của bảng hằng số (và khi bạn đang chèn vào một đống thời gian dành cho sắp xếp dù sao cũng sẽ vô nghĩa ngay cả khi nó đã xảy ra).

Hơn nữa, nếu một chỉ mục nhóm được thêm vào bảng, kế hoạch vẫn hiển thị một bước sắp xếp rõ ràng vì vậy nó dường như không sắp xếp tại thời điểm biên dịch để tránh sắp xếp trong thời gian chạy.

Kế hoạch

Tôi đã cố gắng xem xét điều này trong trình gỡ lỗi nhưng các ký hiệu công khai cho phiên bản SQL Server 2008 của tôi dường như không có sẵn vì vậy thay vào đó tôi phải xem xét UNION ALLcấu trúc tương đương trong SQL Server 2005.

Dưới đây là một dấu vết ngăn xếp điển hình

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   

Vì vậy, việc loại bỏ các tên trong dấu vết ngăn xếp dường như sẽ tốn rất nhiều thời gian để so sánh các chuỗi.

Bài viết KB này cho biết điều đó DeriveNormalizedGroupPropertiesđược liên kết với những gì từng được gọi là giai đoạn chuẩn hóa của quá trình xử lý truy vấn

Giai đoạn này bây giờ được gọi là ràng buộc hoặc đại số hóa và nó lấy kết quả cây phân tích cú pháp biểu thức từ giai đoạn phân tích cú pháp trước đó và xuất ra cây biểu thức được đại số hóa (cây xử lý truy vấn) để chuyển sang tối ưu hóa (tối ưu hóa kế hoạch tầm thường trong trường hợp này) [ref] .

Tôi đã thử thêm một thử nghiệm ( Tập lệnh ) đó là chạy lại thử nghiệm ban đầu nhưng xem xét ba trường hợp khác nhau.

  1. Họ và Tên Chuỗi có độ dài 10 ký tự không trùng lặp.
  2. Họ và Tên Chuỗi có độ dài 50 ký tự không trùng lặp.
  3. Họ và Tên Chuỗi có độ dài 10 ký tự với tất cả các bản sao.

Đồ thị

Có thể thấy rõ ràng rằng các chuỗi càng dài thì mọi thứ càng trở nên tồi tệ và ngược lại càng nhiều bản sao thì mọi thứ càng tốt hơn. Như đã đề cập trước đó, các bản sao không ảnh hưởng đến kích thước kế hoạch được lưu trong bộ nhớ cache, vì vậy tôi cho rằng phải có một quy trình nhận dạng trùng lặp khi xây dựng chính cây biểu thức đại số hóa.

Biên tập

Một nơi mà thông tin này được tận dụng được hiển thị bởi @Lieven tại đây

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

Bởi vì tại thời điểm biên dịch, nó có thể xác định rằng Namecột không có bản sao nên nó sẽ bỏ qua thứ tự của 1/ (ID - ID)biểu thức phụ tại thời gian chạy (sắp xếp trong kế hoạch chỉ có một ORDER BYcột) và không có lỗi chia cho 0 được đưa ra. Nếu các bản sao được thêm vào bảng thì toán tử sắp xếp sẽ hiển thị hai thứ tự theo cột và lỗi dự kiến ​​sẽ tăng lên.


6
Con số kỳ diệu bạn có là NumberOfRows / ColumnCount = 250. Thay đổi truy vấn của bạn để chỉ sử dụng ba cột và thay đổi sẽ xảy ra ở 333. Con số kỳ diệu 1000 có thể giống như số lượng tham số tối đa được sử dụng trong một kế hoạch được lưu trong bộ nhớ cache. Nó kết hợp để "dễ dàng" hơn trong việc tạo một kế hoạch với một kế hoạch <ParameterList>với một <ConstantScan><Values><Row>danh sách.
Mikael Eriksson

1
@MikaelEriksson - Đồng ý. Hàng 250, một hàng có 1000 giá trị được tự động tham số hóa, hàng 251 không có vẻ là sự khác biệt. Tuy nhiên, không chắc tại sao. Có lẽ nó dành thời gian sắp xếp các giá trị theo nghĩa đen để tìm kiếm các bản sao hoặc một cái gì đó khi nó có những giá trị này.
Martin Smith

1
Đây là một vấn đề khá điên rồ, tôi chỉ cảm thấy đau buồn vì nó. Đây là một câu trả lời tuyệt vời nhờ
Không yêu

1
@MikaelEriksson Ý của bạn là con số kỳ diệu là NumberOfRows * ColumnCount = 1000?
paparazzo

1
@Blam - Có. Khi tổng số phần tử nhiều hơn 1000 (NumberOfRows * ColumnCount), kế hoạch truy vấn được thay đổi để sử dụng <ConstantScan><Values><Row>thay vì <ParameterList>.
Mikael Eriksson

23

Không có gì quá ngạc nhiên: kế hoạch thực thi cho phần chèn nhỏ được tính toán một lần và sau đó được sử dụng lại 1000 lần. Việc phân tích cú pháp và chuẩn bị kế hoạch rất nhanh chóng, bởi vì nó chỉ có bốn giá trị cần phân tích. Mặt khác, kế hoạch 1000 hàng cần xử lý 4000 giá trị (hoặc 4000 tham số nếu bạn đã tham số hóa các bài kiểm tra C # của mình). Điều này có thể dễ dàng tiêu tốn thời gian tiết kiệm mà bạn có được bằng cách loại bỏ 999 vòng đối với SQL Server, đặc biệt nếu mạng của bạn không quá chậm.


9

Vấn đề có thể liên quan đến thời gian biên dịch truy vấn.

Nếu bạn muốn tăng tốc độ chèn, những gì bạn thực sự cần làm là kết hợp chúng trong một giao dịch:

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;

Từ C #, bạn cũng có thể cân nhắc sử dụng tham số có giá trị bảng. Việc phát hành nhiều lệnh trong một lô duy nhất, bằng cách phân tách chúng bằng dấu chấm phẩy, là một cách tiếp cận khác cũng sẽ hữu ích.


1
V / v: "Phát hành nhiều lệnh trong một lô duy nhất": điều đó giúp được một chút, nhưng không nhiều. Nhưng tôi chắc chắn đồng ý với hai lựa chọn còn lại là gói trong GIAO DỊCH (TRANS thực sự hoạt động hay chỉ nên là TRẦN?) Hoặc sử dụng TVP.
Solomon Rutzky,

1

Tôi đã gặp phải tình huống tương tự khi cố gắng chuyển đổi một bảng có vài 100k hàng bằng chương trình C ++ (MFC / ODBC).

Vì thao tác này mất rất nhiều thời gian, tôi đã tính toán việc gộp nhiều phần chèn vào một (lên đến 1000 do các hạn chế của MSSQL ). Tôi đoán rằng rất nhiều câu lệnh chèn đơn lẻ sẽ tạo ra một chi phí tương tự như những gì được mô tả ở đây .

Tuy nhiên, hóa ra quá trình chuyển đổi thực sự lâu hơn một chút:

        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

Vì vậy, 1000 lệnh gọi đơn đến CDatabase :: ExecuteSql mỗi lệnh với một câu lệnh INSERT (phương pháp 1) nhanh hơn gần gấp đôi so với một lệnh gọi đơn đến CDatabase :: ExecuteSql với câu lệnh INSERT nhiều dòng với 1000 bộ giá trị (phương pháp 2).

Cập nhật: Vì vậy, điều tiếp theo tôi đã thử là gói 1000 câu lệnh INSERT riêng biệt thành một chuỗi duy nhất và yêu cầu máy chủ thực thi điều đó (phương pháp 3). Hóa ra cách này thậm chí còn nhanh hơn một chút so với phương pháp 1.

Chỉnh sửa: Tôi đang sử dụng Microsoft SQL Server Express Edition (64-bit) v10.0.2531.0

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.