Birden çok INSERT deyimi ile birden çok DEĞERLİ tek INSERT karşılaştırması


119

1000 INSERT deyimi kullanarak bir performans karşılaştırması çalıştırıyorum:

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)

.. 1000 değerle tek INSERT deyimi kullanan yerine:

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)

Büyük sürprizime göre, sonuçlar düşündüğümün tam tersi:

  • 1000 INSERT ifadesi: 290 milisaniye.
  • 1000 DEĞERLİ 1 INSERT ifadesi: 2800 milisaniye.

Test, ölçüm için kullanılan SQL Server Profiler ile doğrudan MSSQL Management Studio'da yürütülür (ve bunu C # kodundan SqlClient kullanarak çalıştırarak benzer sonuçlar aldım, bu da tüm DAL katmanları gidiş-dönüşlerini göz önünde bulundurarak daha da şaşırtıcıdır)

Bu makul olabilir mi veya bir şekilde açıklanabilir mi? Nasıl olur da, sözde daha hızlı bir yöntem 10 kat (!) Daha kötü performansla sonuçlanır ?

Teşekkür ederim.

DÜZENLEME: Her ikisi için yürütme planları ekleme: Yürütme Planları


1
bunlar temiz testler, hiçbir şey paralel olarak yürütülmüyor, tekrarlanan veriler yok (her sorgu, basit önbelleğe almayı önlemek için elbette farklı veriler
içeriyor

1
ilgili herhangi bir tetikleyici var mı?
AK

2
1000 değer sınırını aşmak için bir programı TVP'ye çevirdim ve büyük bir performans kazancı elde ettim. Bir karşılaştırma yapacağım.
paparazzo

Yanıtlar:


126

Ek: SQL Server 2012, bu alanda bazı gelişmiş performans gösterir ancak aşağıda belirtilen belirli sorunların üstesinden gelmiyor gibi görünmektedir. Bu gerektiğini görünüşte sabit bir sonraki büyük versiyonu sonra SQL Server 2012!

Planınız, tekli girişlerin parametreleştirilmiş prosedürleri (muhtemelen otomatik parametreleştirilmiş) kullandığını gösterir, bu nedenle bunlar için ayrıştırma / derleme süresi minimum olmalıdır.

Buna biraz daha bakacağımı düşündüm, bu yüzden bir döngü ( komut dosyası ) kurup VALUEScümle sayısını ayarlamayı ve derleme süresini kaydetmeyi denedim .

Daha sonra cümle başına ortalama derleme süresini elde etmek için derleme süresini satır sayısına böldüm. Sonuçlar aşağıda

grafik

250 VALUEScümle sunulana kadar , derleme süresi / cümle sayısı hafif bir yükseliş eğilimi gösterir, ancak çok dramatik bir şey yoktur.

grafik

Ama sonra ani bir değişiklik olur.

Verilerin bu bölümü aşağıda gösterilmiştir.

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

Doğrusal olarak büyüyen önbelleğe alınmış plan boyutu aniden düşer, ancak CompileTime 7 kat artar ve CompileMemory yükselir. Bu, planın otomatik olarak parametrik hale getirilmiş (1.000 parametreli) ile parametrik olmayan bir plan arasında kesilme noktasıdır. Bundan sonra (belirli bir zamanda işlenen değer cümlelerinin sayısı bakımından) doğrusal olarak daha az verimli hale geliyor gibi görünüyor.

Bunun neden olması gerektiğinden emin değilim. Muhtemelen belirli değişmez değerler için bir plan derlerken, doğrusal olarak ölçeklenmeyen (sıralama gibi) bazı etkinlikler gerçekleştirmesi gerekir.

Tamamen yinelenen satırlardan oluşan bir sorguyu denediğimde önbelleğe alınan sorgu planının boyutunu etkilemiyor gibi görünüyor ve sabitlerin tablosunun çıktısının sırasını etkilemiyor öyle olsa bile anlamsız olurdu).

Dahası, tabloya kümelenmiş bir dizin eklenirse, plan yine de açık bir sıralama adımı gösterir, bu nedenle çalışma zamanında bir sıralamayı önlemek için derleme zamanında sıralanıyor gibi görünmez.

Plan

Buna bir hata ayıklayıcıda bakmaya çalıştım, ancak SQL Server 2008 sürümüm için genel semboller mevcut görünmüyor, bu yüzden bunun yerine UNION ALLSQL Server 2005'teki eşdeğer yapıya bakmak zorunda kaldım .

Tipik bir yığın izi aşağıdadır

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   

Yani yığın izlemesindeki isimleri gözden kaçırmak, dizeleri karşılaştırmak için çok zaman harcıyor gibi görünüyor.

Bu KB makalesi , sorgu işlemenin normalleştirme aşaması DeriveNormalizedGroupPropertiesolarak adlandırılan şeyle ilişkili olduğunu gösterir.

Bu aşama artık bağlama veya cebirleme olarak adlandırılır ve ifade ayrıştırma ağacı çıktısını önceki ayrıştırma aşamasından alır ve optimizasyona (bu durumda önemsiz plan optimizasyonu) ilerlemek için cebirlendirilmiş bir ifade ağacı (sorgu işlemci ağacı) çıkarır [ref] .

Orijinal testi yeniden çalıştıracak, ancak üç farklı duruma bakacak bir deney daha ( Script ) denedim .

  1. 10 karakter uzunluğunda ve yinelenmemiş Ad ve Soyad Dizeleri.
  2. 50 karakter uzunluğunda, yinelenmemiş Ad ve Soyad Dizeleri.
  3. Tüm kopyaları olan 10 karakter uzunluğunda Ad ve Soyadı Dizeleri.

grafik

Açıkça görülebilir ki, dizeler ne kadar uzun olursa, daha kötü şeyler olur ve tersine, ne kadar çok kopya olursa, o kadar iyi olur. Daha önce bahsedildiği gibi, kopyalar önbelleğe alınmış plan boyutunu etkilemez, bu yüzden cebri ifade ağacının kendisini oluştururken bir yinelenen tanımlama süreci olması gerektiğini varsayıyorum.

Düzenle

Bu bilgilerin kullanıldığı bir yer @Lieven tarafından burada gösterilmektedir

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

Derleme zamanında, Namesütunun yinelemeleri olmadığını belirleyebildiğinden 1/ (ID - ID), çalışma zamanında ikincil ifadeye göre sıralamayı atlar (plandaki sıralamada yalnızca bir ORDER BYsütun vardır) ve sıfıra bölme hatası oluşmaz. Tabloya kopyalar eklenirse, sıralama operatörü sütunlara göre iki sıra gösterir ve beklenen hata ortaya çıkar.


6
Sahip olduğunuz sihirli sayı NumberOfRows / ColumnCount = 250'dir. Sorgunuzu yalnızca üç sütun kullanacak şekilde değiştirin ve değişiklik 333'te gerçekleşecektir. 1000 sihirli sayı, önbelleğe alınmış bir planda kullanılan maksimum parametre sayısı gibi bir şey olabilir. Listeli birden <ParameterList>fazla planla bir plan oluşturmak "daha kolay" görünmektedir <ConstantScan><Values><Row>.
Mikael Eriksson

1
@MikaelEriksson - Kabul edildi. 1000 değere sahip 250 satır, 251 satırı otomatik olarak parametrelendirilir, bu nedenle aradaki fark budur. Neden olduğundan emin değilim. Belki de kopyaları ya da bunlara sahip olduğunda bir şeyi aramak için gerçek değerleri sıralamak için zaman harcıyor.
Martin Smith

1
Bu oldukça çılgın bir mesele, bundan dolayı üzüldüm. Bu büyük bir cevap sayesinde
Değil sevilen

1
@MikaelEriksson Sihirli sayının NumberOfRows * ColumnCount = 1000 olduğunu mu söylüyorsunuz?
paparazzo

1
@Blam - Evet. Toplam öğe sayısı 1000'den fazla olduğunda (NumberOfRows * ColumnCount), <ConstantScan><Values><Row>yerine kullanmak için sorgu planı değiştirildi <ParameterList>.
Mikael Eriksson

23

Bu çok şaşırtıcı değil: Küçük uç için yürütme planı bir kez hesaplanır ve ardından 1000 kez yeniden kullanılır. Planın ayrıştırılması ve hazırlanması hızlıdır, çünkü silinecek yalnızca dört değeri vardır. Öte yandan 1000 satırlık bir planın 4000 değerle (veya C # testlerinizi parametrelendirdiyseniz 4000 parametreyle) ilgilenmesi gerekir. Bu, özellikle ağınız aşırı yavaş değilse, SQL Server'a 999 gidiş dönüşü ortadan kaldırarak kazandığınız zaman tasarrufunu kolayca tüketebilir.


9

Sorun muhtemelen sorguyu derlemek için geçen süre ile ilgilidir.

Eklemeleri hızlandırmak istiyorsanız, gerçekten yapmanız gereken onları bir işlemde paketlemektir:

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 # 'dan, tablo değerli bir parametre kullanmayı da düşünebilirsiniz. Birden çok komutu noktalı virgülle ayırarak tek bir toplu iş halinde vermek de yardımcı olacak başka bir yaklaşımdır.


1
Ynt: "Tek bir grupta birden fazla komut vermek": bu biraz yardımcı olur, ama çok da değil. Ama diğer iki seçeneğe kesinlikle katılıyorum: bir İŞLEM'e sarma (TRANS gerçekten çalışıyor mu yoksa sadece TRAN mı olmalı?) Veya bir TVP kullanıyorum.
Solomon Rutzky

1

Bir C ++ programı (MFC / ODBC) ile birkaç 100 bin satır içeren bir tabloyu dönüştürmeye çalışırken benzer bir durumla karşılaştım.

Bu işlem çok uzun sürdüğü için, birden çok eki bir araya toplamayı düşündüm ( MSSQL sınırlamaları nedeniyle 1000'e kadar ). Tahminimce, birçok tekli ekleme deyimi, burada anlatılana benzer bir ek yük oluşturacaktır .

Ancak, dönüşümün aslında biraz daha uzun sürdüğü ortaya çıktı:

        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

Bu nedenle, her biri tek bir INSERT deyimine (yöntem 1) sahip CDatabase :: ExecuteSql'ye yapılan 1000 tekli çağrı, 1000 değer tuplesli çok satırlı INSERT deyimiyle CDatabase :: ExecuteSql'ye yapılan tek bir çağrıdan kabaca iki kat daha hızlıdır (yöntem 2).

Güncelleme: Yani, denediğim bir sonraki şey 1000 ayrı INSERT ifadesini tek bir dizede toplamak ve sunucunun bunu yürütmesini sağlamaktı (yöntem 3). Görünüşe göre bu yöntem 1'den biraz daha hızlı.

Düzenleme: Microsoft SQL Server Express Edition (64-bit) v10.0.2531.0 kullanıyorum

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.