คำสั่ง INSERT หลายคำเทียบกับ INSERT เดียวที่มีหลายค่า


120

ฉันกำลังทำการเปรียบเทียบประสิทธิภาพระหว่างการใช้คำสั่ง 1000 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)

.. ตรงกันข้ามโดยใช้คำสั่ง INSERT เดียวที่มีค่า 1000:

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)

ที่แปลกใจมากคือผลลัพธ์ที่ได้กลับตรงกันข้ามกับที่ฉันคิดไว้:

  • 1,000 INSERT งบ: 290 msec
  • 1 คำสั่ง INSERT พร้อม 1,000 ค่า: 2800 msec

การทดสอบดำเนินการโดยตรงใน MSSQL Management Studio พร้อมด้วย SQL Server Profiler ที่ใช้สำหรับการวัด (และฉันได้ผลลัพธ์ที่คล้ายกันที่เรียกใช้จากรหัส C # โดยใช้ SqlClient ซึ่งน่าแปลกใจยิ่งกว่าเมื่อพิจารณาจาก Roundtrips เลเยอร์ DAL ทั้งหมด)

สิ่งนี้สมเหตุสมผลหรืออธิบายได้อย่างไร? ทำไมวิธีที่เร็วกว่าที่คาดไว้จะส่งผลให้ประสิทธิภาพแย่ลงถึง 10 เท่า (!) ?

ขอบคุณ.

แก้ไข: การแนบแผนการดำเนินการสำหรับทั้งสอง: แผน Exec


1
นี่คือการทดสอบที่สะอาดไม่มีการดำเนินการควบคู่กันไม่มีข้อมูลซ้ำ (แน่นอนว่าแบบสอบถามแต่ละรายการมีข้อมูลที่แตกต่างกันเพื่อหลีกเลี่ยงการแคชอย่างง่าย)
Borka

1
มีทริกเกอร์ที่เกี่ยวข้องหรือไม่
AK

2
ฉันแปลงโปรแกรมเป็น TVP เพื่อให้พ้นขีด จำกัด 1,000 ค่าและได้รับประสิทธิภาพที่เพิ่มขึ้นอย่างมาก ฉันจะทำการเปรียบเทียบ
paparazzo

1
ที่เกี่ยวข้อง: simple-talk.com/sql/performance/…
ทราบ

คำตอบ:


126

เพิ่มเติม: SQL Server 2012 แสดงประสิทธิภาพที่ดีขึ้นในพื้นที่นี้ แต่ดูเหมือนจะไม่สามารถแก้ไขปัญหาเฉพาะที่ระบุไว้ด้านล่าง เห็นได้ชัดว่าสิ่งนี้ควรได้รับการแก้ไขในเวอร์ชันหลักถัดไปหลังจาก SQL Server 2012!

แผนของคุณแสดงให้เห็นว่าเม็ดมีดเดี่ยวกำลังใช้ขั้นตอนที่กำหนดพารามิเตอร์ (อาจเป็นพารามิเตอร์อัตโนมัติ) ดังนั้นเวลาในการแยกวิเคราะห์ / รวบรวมควรน้อยที่สุด

ฉันคิดว่าฉันจะพิจารณาเรื่องนี้ให้มากขึ้นแม้ว่าจะตั้งค่าลูป ( สคริปต์ ) และลองปรับจำนวนVALUESประโยคและบันทึกเวลาในการคอมไพล์

จากนั้นฉันแบ่งเวลาคอมไพล์ด้วยจำนวนแถวเพื่อให้ได้เวลาคอมไพล์เฉลี่ยต่อข้อ ผลลัพธ์อยู่ด้านล่าง

กราฟ

จนถึง 250 VALUESข้อแสดงเวลาในการคอมไพล์ / จำนวนข้อมีแนวโน้มสูงขึ้นเล็กน้อย แต่ไม่มีอะไรน่าทึ่งเกินไป

กราฟ

แต่แล้วก็มีการเปลี่ยนแปลงอย่างกะทันหัน

ข้อมูลส่วนนั้นแสดงอยู่ด้านล่าง

+------+----------------+-------------+---------------+---------------+
| 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 รายการ) ไปยังจุดที่ไม่ใช่พารามิเตอร์ หลังจากนั้นดูเหมือนว่าจะมีประสิทธิภาพน้อยลงเชิงเส้น (ในแง่ของจำนวนประโยคค่าที่ประมวลผลในเวลาที่กำหนด)

ไม่แน่ใจว่าทำไมถึงเป็นเช่นนี้ สันนิษฐานว่าเมื่อรวบรวมแผนสำหรับค่าตัวอักษรที่เฉพาะเจาะจงจะต้องดำเนินกิจกรรมบางอย่างที่ไม่ได้ปรับขนาดเชิงเส้น (เช่นการเรียงลำดับ)

ดูเหมือนว่าจะไม่มีผลกับขนาดของแผนแบบสอบถามที่แคชไว้เมื่อฉันลองใช้แบบสอบถามที่ประกอบด้วยแถวที่ซ้ำกันทั้งหมดและไม่มีผลต่อลำดับของผลลัพธ์ของตารางของค่าคงที่ (และในขณะที่คุณกำลังแทรกลงในเวลาฮีปที่ใช้ในการเรียงลำดับ จะไม่มีจุดหมายต่อไปแม้ว่าจะเป็นเช่นนั้นก็ตาม)

ยิ่งไปกว่านั้นหากมีการเพิ่มดัชนีคลัสเตอร์ลงในตารางแผนจะยังคงแสดงขั้นตอนการจัดเรียงที่ชัดเจนดังนั้นจึงดูเหมือนว่าจะไม่มีการเรียงลำดับในเวลาคอมไพล์เพื่อหลีกเลี่ยงการเรียงลำดับในขณะรัน

วางแผน

ฉันพยายามดูสิ่งนี้ในดีบักเกอร์ แต่ดูเหมือนว่าสัญลักษณ์สาธารณะสำหรับ 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   

ดังนั้นการปิดชื่อในการติดตามสแต็กดูเหมือนว่าจะใช้เวลามากในการเปรียบเทียบสตริง

บทความ KB นี้ระบุว่าDeriveNormalizedGroupPropertiesเกี่ยวข้องกับสิ่งที่เคยเรียกว่าการทำให้เป็นมาตรฐานขั้นตอนการของการประมวลผลแบบสอบถาม

สเตจนี้เรียกว่าการโยงหรือ algebrizing และใช้เอาต์พุตแผนภูมิการแยกนิพจน์จากขั้นตอนการแยกวิเคราะห์ก่อนหน้านี้และส่งออกต้นไม้นิพจน์อัลจีไบรซ์ (แผนผังตัวประมวลผลแบบสอบถาม) เพื่อไปข้างหน้าเพื่อการปรับให้เหมาะสม (การเพิ่มประสิทธิภาพแผนเล็กน้อยในกรณีนี้) [อ้างอิง] [อ้างอิง]

ฉันลองอีกหนึ่งการทดสอบ ( สคริปต์ ) ซึ่งเป็นการเรียกใช้การทดสอบเดิมอีกครั้ง แต่ดูสามกรณีที่แตกต่างกัน

  1. First Name and Last Name สตริงความยาว 10 อักขระที่ไม่มีรายการซ้ำกัน
  2. First Name and Last Name สตริงความยาว 50 อักขระโดยไม่มีรายการซ้ำกัน
  3. First Name and Last Name สตริงความยาว 10 ตัวอักษรที่ซ้ำกันทั้งหมด

กราฟ

จะเห็นได้ชัดเจนว่ายิ่งสตริงนานเท่าไหร่สิ่งที่แย่ลงก็ยิ่งได้รับและในทางกลับกันยิ่งซ้ำกันมากขึ้นสิ่งที่ดีกว่าจะได้รับ ตามที่กล่าวไว้ก่อนหน้านี้รายการที่ซ้ำกันไม่ส่งผลต่อขนาดของแผนแคชดังนั้นฉันจึงสันนิษฐานว่าต้องมีกระบวนการในการระบุตัวตนที่ซ้ำกันเมื่อสร้างแผนผังนิพจน์อัลจีไบรซ์เอง

แก้ไข

สถานที่แห่งหนึ่งที่ข้อมูลนี้ถูกนำมาใช้แสดงโดย @Lieven ที่นี่

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

เนื่องจากในเวลาคอมไพล์สามารถระบุได้ว่าNameคอลัมน์ไม่มีรายการซ้ำจึงข้ามลำดับโดย1/ (ID - ID)นิพจน์รองในขณะรัน (การเรียงลำดับในแผนมีเพียงORDER BYคอลัมน์เดียว) และไม่มีการหารด้วยข้อผิดพลาดเป็นศูนย์ หากมีการเพิ่มรายการที่ซ้ำกันในตารางตัวดำเนินการเรียงลำดับจะแสดงสองลำดับตามคอลัมน์และข้อผิดพลาดที่คาดไว้จะเพิ่มขึ้น


6
หมายเลขวิเศษที่คุณมีคือ NumberOfRows / ColumnCount = 250 เปลี่ยนข้อความค้นหาของคุณให้ใช้เพียงสามคอลัมน์และการเปลี่ยนแปลงจะเกิดขึ้นที่ 333 จำนวนเวทย์มนตร์ 1000 อาจเป็นจำนวนพารามิเตอร์สูงสุดที่ใช้ในแผนแคช มัน seams ที่จะ "ง่ายขึ้น" เพื่อสร้างแผนได้<ParameterList>มากกว่าหนึ่งกับ<ConstantScan><Values><Row>รายการ
Mikael Eriksson

1
@MikaelEriksson - เห็นด้วย 250 แถวหนึ่งที่มีค่า 1,000 ค่าจะได้รับการกำหนดพารามิเตอร์โดยอัตโนมัติที่ 251 แถวนั้นดูเหมือนจะไม่แตกต่างกัน ไม่แน่ใจว่าทำไม บางทีอาจใช้เวลาในการเรียงลำดับค่าตามตัวอักษรเพื่อค้นหารายการที่ซ้ำกันหรือบางสิ่งบางอย่างเมื่อมีสิ่งเหล่านี้
Martin Smith

1
นี่เป็นปัญหาที่ค่อนข้างบ้าฉันเพิ่งรู้สึกเศร้ากับมัน นี่คือคำตอบที่ดีขอบคุณ
ไม่ได้รัก

1
@MikaelEriksson คุณหมายถึงเลขวิเศษคือ NumberOfRows * ColumnCount = 1000 หรือไม่?
paparazzo

1
@ บลัม - ครับ เมื่อจำนวนองค์ประกอบทั้งหมดมากกว่า 1,000 (NumberOfRows * ColumnCount) แผนการสืบค้นจะเปลี่ยนไปใช้<ConstantScan><Values><Row>แทน<ParameterList>.
Mikael Eriksson

23

ไม่น่าแปลกใจเกินไป: แผนการดำเนินการสำหรับเม็ดมีดขนาดเล็กจะคำนวณเพียงครั้งเดียวแล้วนำกลับมาใช้อีก 1,000 ครั้ง การแยกวิเคราะห์และจัดเตรียมแผนทำได้อย่างรวดเร็วเนื่องจากมีค่าสี่ค่าเท่านั้นที่จะวิเคราะห์ ในทางกลับกันแผน 1,000 แถวจำเป็นต้องจัดการกับค่า 4000 (หรือพารามิเตอร์ 4000 หากคุณกำหนดพารามิเตอร์การทดสอบ C # ของคุณ) สิ่งนี้สามารถประหยัดเวลาได้อย่างง่ายดายโดยการกำจัด 999 roundtrips ไปยัง 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
Re: "การออกคำสั่งหลายคำสั่งในชุดเดียว": ที่ช่วยได้เล็กน้อย แต่ไม่มาก แต่ฉันเห็นด้วยกับอีกสองตัวเลือกของการรวมใน TRANSACTION (TRANS ใช้งานได้จริงหรือควรเป็น TRAN?) หรือใช้ TVP
Solomon Rutzky

1

ฉันพบสถานการณ์ที่คล้ายกันโดยพยายามแปลงตารางที่มีแถว 100k หลายแถวด้วยโปรแกรม C ++ (MFC / ODBC)

เนื่องจากการดำเนินการนี้ใช้เวลานานมากฉันจึงคิดว่าการรวมเม็ดมีดหลายตัวเข้าด้วยกัน (มากถึง 1,000 อันเนื่องจากข้อ จำกัด ของ MSSQL ) ฉันเดาว่าคำสั่งแทรกเดี่ยวจำนวนมากจะสร้างค่าใช้จ่ายที่คล้ายกับที่อธิบายไว้ที่นี่ที่นี่

อย่างไรก็ตามปรากฎว่าการแปลงใช้เวลานานกว่าเล็กน้อย:

        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

ดังนั้น 1,000 การเรียกไปยัง CDatabase :: ExecuteSql แต่ละครั้งด้วยคำสั่ง INSERT เดียว (วิธีที่ 1) เร็วกว่าการเรียกครั้งเดียวไปยัง CDatabase :: ExecuteSql โดยใช้คำสั่ง INSERT แบบหลายบรรทัดพร้อมค่า tuples 1,000 รายการ (วิธีที่ 2)

อัปเดต: ดังนั้นสิ่งต่อไปที่ฉันพยายามคือการรวมคำสั่ง INSERT ที่แยกต่างหาก 1,000 รายการเป็นสตริงเดียวและให้เซิร์ฟเวอร์ดำเนินการนั้น (วิธีที่ 3) ปรากฎว่านี่เร็วกว่าวิธีที่ 1 เล็กน้อย

แก้ไข: ฉันใช้ Microsoft SQL Server Express Edition (64 บิต) v10.0.2531.0

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.