Beberapa pernyataan INSERT vs. satu INSERT dengan beberapa VALUES

119

Saya menjalankan perbandingan kinerja antara menggunakan 1000 pernyataan 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 menggunakan pernyataan INSERT tunggal dengan 1000 nilai:

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)

Yang sangat mengejutkan saya, hasilnya berlawanan dengan yang saya pikirkan:

  • 1000 pernyataan INSERT: 290 msec.
  • 1 pernyataan INSERT dengan 1000 VALUES: 2800 msec.

Pengujian dijalankan langsung di MSSQL Management Studio dengan SQL Server Profiler yang digunakan untuk pengukuran (dan saya mendapatkan hasil serupa yang menjalankannya dari kode C # menggunakan SqlClient, yang bahkan lebih mengejutkan lagi mengingat semua lapisan DAL pulang pergi)

Apakah ini masuk akal atau dijelaskan? Kenapa, metode yang seharusnya lebih cepat menghasilkan 10 kali (!) Kinerja yang lebih buruk ?

Terima kasih.

EDIT: Melampirkan rencana eksekusi untuk keduanya: Jalankan Rencana

Borka
sumber
1
ini adalah tes bersih, tidak ada yang dijalankan secara paralel, tidak ada data berulang (setiap kueri memiliki data yang berbeda, tentu saja, untuk menghindari caching sederhana)
Borka
1
apakah ada pemicu yang terlibat?
AK
2
Saya mengonversi sebuah program ke TVP untuk melewati batas 1000 nilai dan mendapatkan perolehan kinerja yang besar. Saya akan membuat perbandingan.
paparazzo

Jawaban:

126

Tambahan: SQL Server 2012 menunjukkan beberapa peningkatan kinerja di area ini tetapi tampaknya tidak mengatasi masalah khusus yang disebutkan di bawah ini. Ini tampaknya harus diperbaiki di versi utama berikutnya setelah SQL Server 2012!

Rencana Anda menunjukkan bahwa sisipan tunggal menggunakan prosedur berparameter (mungkin diparameterisasi otomatis) sehingga waktu parse / kompilasi untuk ini harus diminimalkan.

Saya pikir saya akan melihat ini sedikit lebih banyak, jadi siapkan loop ( skrip ) dan coba sesuaikan jumlah VALUESklausa dan catat waktu kompilasi.

Saya kemudian membagi waktu kompilasi dengan jumlah baris untuk mendapatkan waktu kompilasi rata-rata per klausa. Hasilnya di bawah ini

Grafik

Hingga 250 VALUESklausa yang ada, waktu kompilasi / jumlah klausa memiliki tren sedikit naik tetapi tidak terlalu dramatis.

Grafik

Tapi kemudian terjadi perubahan mendadak.

Bagian data itu ditunjukkan di bawah ini.

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

Ukuran rencana cache yang telah tumbuh secara linier tiba-tiba turun tetapi CompileTime meningkat 7 kali lipat dan CompileMemory meningkat. Ini adalah titik potong antara paket yang paketnya diparameterisasi otomatis (dengan 1.000 parameter) ke yang tidak diparameterisasi. Setelah itu tampaknya menjadi kurang efisien secara linier (dalam hal jumlah klausul nilai yang diproses dalam waktu tertentu).

Tidak yakin mengapa ini harus terjadi. Agaknya ketika menyusun rencana untuk nilai literal tertentu, ia harus melakukan beberapa aktivitas yang tidak berskala linier (seperti pengurutan).

Tampaknya tidak memengaruhi ukuran rencana kueri yang di-cache ketika saya mencoba kueri yang seluruhnya terdiri dari baris duplikat dan tidak memengaruhi urutan output tabel konstanta (dan saat Anda memasukkan ke dalam tumpukan waktu yang dihabiskan untuk menyortir akan menjadi sia-sia bahkan jika itu terjadi).

Selain itu, jika indeks berkerumun ditambahkan ke tabel, rencana tersebut masih menunjukkan langkah pengurutan eksplisit sehingga tampaknya tidak menyortir pada waktu kompilasi untuk menghindari pengurutan pada waktu proses.

Rencana

Saya mencoba melihat ini di debugger tetapi simbol publik untuk versi SQL Server 2008 saya tampaknya tidak tersedia, jadi saya harus melihat UNION ALLkonstruksi yang setara di SQL Server 2005.

Jejak tumpukan tipikal ada di bawah

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   

Jadi menghapus nama di pelacakan tumpukan tampaknya menghabiskan banyak waktu untuk membandingkan string.

Artikel KB ini menunjukkan yang DeriveNormalizedGroupPropertiesterkait dengan apa yang dulu disebut tahap normalisasi pemrosesan kueri

Tahap ini sekarang disebut mengikat atau algebrizing dan mengambil keluaran pohon parse ekspresi dari tahap penguraian sebelumnya dan keluaran pohon ekspresi algebrized (pohon prosesor kueri) untuk maju ke pengoptimalan (pengoptimalan rencana trivial dalam kasus ini) [ref] .

Saya mencoba satu eksperimen lagi ( Script ) yang akan menjalankan ulang pengujian asli tetapi melihat tiga kasus berbeda.

  1. String Nama Depan dan Nama Belakang dengan panjang 10 karakter tanpa duplikat.
  2. String Nama Depan dan Nama Belakang dengan panjang 50 karakter tanpa duplikat.
  3. String Nama Depan dan Nama Belakang dengan panjang 10 karakter dengan semua duplikat.

Grafik

Dapat dilihat dengan jelas bahwa semakin panjang string semakin buruk hal-hal dan sebaliknya semakin banyak duplikat semakin baik hal-hal. Seperti yang disebutkan sebelumnya, duplikat tidak memengaruhi ukuran rencana yang di-cache, jadi saya berasumsi bahwa harus ada proses identifikasi duplikat saat membangun pohon ekspresi algebrized itu sendiri.

Edit

Satu tempat di mana informasi ini dimanfaatkan ditunjukkan oleh @Lieven di sini

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

Karena pada waktu kompilasi dapat menentukan bahwa Namekolom tidak memiliki duplikat, pengurutannya dilewati oleh 1/ (ID - ID)ekspresi sekunder pada waktu proses (pengurutan dalam rencana hanya memiliki satu ORDER BYkolom) dan tidak ada kesalahan bagi dengan nol yang dimunculkan. Jika duplikat ditambahkan ke tabel maka operator pengurutan memperlihatkan dua urutan menurut kolom dan kesalahan yang diharapkan dimunculkan.

Martin Smith
sumber
6
Angka ajaib yang Anda miliki adalah NumberOfRows / ColumnCount = 250. Ubah kueri Anda untuk hanya menggunakan tiga kolom dan perubahan akan terjadi pada 333. Angka ajaib 1000 bisa menjadi seperti jumlah maksimum parameter yang digunakan dalam rencana cache. Tampaknya menjadi "lebih mudah" untuk menghasilkan rencana dengan <ParameterList>daripada yang memiliki <ConstantScan><Values><Row>daftar.
Mikael Eriksson
1
@MikaelEriksson - Setuju. 250 baris satu dengan 1000 nilai mendapat parameter otomatis, sedangkan baris 251 tidak jadi itu tampaknya menjadi perbedaan. Tidak yakin mengapa. Mungkin ia menghabiskan waktu untuk menyortir nilai literal mencari duplikat atau sesuatu yang memiliki ini.
Martin Smith
1
Ini adalah masalah yang cukup gila, saya baru saja berduka karenanya. Ini adalah jawaban yang bagus terima kasih
Tidak dicintai
1
@MikaelEriksson Maksud Anda angka ajaib adalah NumberOfRows * ColumnCount = 1000?
paparazzo
1
@Blam - Ya. Ketika jumlah total elemen lebih dari 1000 (NumberOfRows * ColumnCount) rencana kueri berubah untuk digunakan <ConstantScan><Values><Row>sebagai pengganti <ParameterList>.
Mikael Eriksson
23

Hal ini tidak terlalu mengejutkan: rencana eksekusi untuk sisipan kecil dihitung sekali, dan kemudian digunakan kembali 1000 kali. Mengurai dan menyiapkan rencana itu cepat, karena hanya memiliki empat nilai untuk dipahami. Rencana 1000 baris, di sisi lain, perlu menangani 4000 nilai (atau 4000 parameter jika Anda membuat parameter pada tes C # Anda). Ini dapat dengan mudah menghabiskan penghematan waktu yang Anda peroleh dengan menghilangkan 999 perjalanan bolak-balik ke SQL Server, terutama jika jaringan Anda tidak terlalu lambat.

dasblinkenlight
sumber
9

Masalahnya mungkin berkaitan dengan waktu yang dibutuhkan untuk mengompilasi kueri.

Jika Anda ingin mempercepat penyisipan, yang benar-benar perlu Anda lakukan adalah membungkusnya dalam sebuah transaksi:

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;

Dari C #, Anda juga dapat mempertimbangkan untuk menggunakan parameter nilai tabel. Menerbitkan beberapa perintah dalam satu batch, dengan memisahkannya dengan titik koma, adalah pendekatan lain yang juga akan membantu.

RickNZ
sumber
1
Re: "Mengeluarkan beberapa perintah dalam satu batch": itu membantu sedikit, tapi tidak banyak. Tapi saya sangat setuju dengan dua pilihan lain, baik membungkus dalam TRANSAKSI (apakah TRANS benar-benar berfungsi atau hanya TRAN?) Atau menggunakan TVP.
Solomon Rutzky
1

Saya mengalami situasi serupa saat mencoba mengubah tabel dengan beberapa baris 100k dengan program C ++ (MFC / ODBC).

Karena operasi ini memakan waktu yang sangat lama, saya membayangkan menggabungkan beberapa sisipan menjadi satu (hingga 1000 karena keterbatasan MSSQL ). Dugaan saya bahwa banyak pernyataan penyisipan tunggal akan menciptakan overhead yang mirip dengan apa yang dijelaskan di sini .

Namun, ternyata konversi tersebut memakan waktu lebih lama:

        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

Jadi, 1000 panggilan tunggal ke CDatabase :: ExecuteSql masing-masing dengan satu pernyataan INSERT (metode 1) kira-kira dua kali lebih cepat dari satu panggilan ke CDatabase :: ExecuteSql dengan pernyataan INSERT multi-baris dengan 1000 tupel nilai (metode 2).

Pembaruan: Jadi, hal berikutnya yang saya coba adalah menggabungkan 1000 pernyataan INSERT terpisah menjadi satu string dan meminta server menjalankannya (metode 3). Ternyata ini lebih cepat daripada metode 1.

Sunting: Saya menggunakan Microsoft SQL Server Express Edition (64-bit) v10.0.2531.0

uceumern
sumber