Wiele instrukcji INSERT a pojedyncze INSERT z wieloma wartościami

119

Przeprowadzam porównanie wydajności między użyciem 1000 instrukcji 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 używając pojedynczej instrukcji INSERT z 1000 wartościami:

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)

Ku mojemu wielkiemu zdziwieniu wyniki są odwrotne do tego, co myślałem:

  • 1000 instrukcji INSERT: 290 msek.
  • 1 instrukcja INSERT z 1000 WARTOŚCI: 2800 msek.

Test jest wykonywany bezpośrednio w MSSQL Management Studio z SQL Server Profiler używanym do pomiaru (i mam podobne wyniki, uruchamiając go z kodu C # przy użyciu SqlClient, co jest jeszcze bardziej zaskakujące, biorąc pod uwagę wszystkie obie strony warstw DAL)

Czy może to być rozsądne lub w jakiś sposób wyjaśnione? Jak to się dzieje, że rzekomo szybsza metoda daje 10 razy (!) Gorszą wydajność?

Dziękuję Ci.

EDYCJA: Dołączanie planów wykonania dla obu: Plany wykonania

Borka
źródło
1
to są czyste testy, nic nie jest wykonywane równolegle, brak powtarzających się danych (każde zapytanie jest oczywiście z innymi danymi, aby uniknąć prostego buforowania)
Borka
1
czy są zaangażowane jakieś wyzwalacze?
AK
2
Przekonwertowałem program na TVP, aby przekroczyć limit wartości 1000 i uzyskałem duży wzrost wydajności. Przeprowadzę porównanie.
paparazzo,

Odpowiedzi:

126

Dodatek: SQL Server 2012 wykazuje pewną poprawę wydajności w tym obszarze, ale wydaje się, że nie rozwiązuje określonych problemów opisanych poniżej. Najwyraźniej powinno to zostać naprawione w następnej głównej wersji po SQL Server 2012!

Twój plan pokazuje, że pojedyncze wstawki używają sparametryzowanych procedur (prawdopodobnie parametryzowanych automatycznie), więc czas analizy / kompilacji dla nich powinien być minimalny.

Pomyślałem, że przyjrzę się temu nieco bardziej, więc skonfigurowałem pętlę ( skrypt ) i spróbowałem dostosować liczbę VALUESklauzul i zarejestrować czas kompilacji.

Następnie podzieliłem czas kompilacji przez liczbę wierszy, aby uzyskać średni czas kompilacji na klauzulę. Wyniki poniżej

Wykres

Aż do 250 VALUESklauzul czas kompilacji / liczba klauzul ma niewielką tendencję wzrostową, ale nic zbyt dramatycznego.

Wykres

Ale potem następuje nagła zmiana.

Ta sekcja danych jest pokazana poniżej.

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

Rozmiar planu w pamięci podręcznej, który wzrastał liniowo, nagle spada, ale CompileTime zwiększa się siedmiokrotnie, a CompileMemory rośnie. Jest to punkt odcięcia między planem sparametryzowanym automatycznie (z 1000 parametrów) a niesparametryzowanym. Od tego czasu wydaje się, że staje się on liniowo mniej wydajny (pod względem liczby klauzul wartości przetwarzanych w danym czasie).

Nie wiem, dlaczego tak powinno być. Przypuszczalnie podczas kompilowania planu dla określonych wartości literalnych musi wykonać jakąś czynność, która nie jest skalowana liniowo (np. Sortowanie).

Wydaje się, że nie wpływa to na rozmiar zbuforowanego planu zapytań, gdy próbowałem kwerendy składającej się wyłącznie z zduplikowanych wierszy i nie wpływa na kolejność danych wyjściowych tabeli stałych (i gdy wstawiasz do sterty czas spędzony na sortowaniu i tak byłoby bezcelowe, nawet gdyby tak było).

Co więcej, jeśli indeks klastrowy zostanie dodany do tabeli, plan nadal pokazuje jawny krok sortowania, więc nie wydaje się, aby sortował w czasie kompilacji, aby uniknąć sortowania w czasie wykonywania.

Plan

Próbowałem przyjrzeć się temu w debugerze, ale symbole publiczne mojej wersji SQL Server 2008 nie wydają się być dostępne, więc zamiast tego musiałem przyjrzeć się równoważnej UNION ALLkonstrukcji w SQL Server 2005.

Poniżej znajduje się typowy ślad stosu

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   

Więc opuszczenie nazw w śladzie stosu wydaje się spędzać dużo czasu na porównywaniu ciągów.

Ten artykuł z bazy wiedzy wskazuje, że DeriveNormalizedGroupPropertiesjest on powiązany z tak zwanym etapem normalizacji przetwarzania zapytań

Ten etap jest teraz nazywany wiązaniem lub algebrizacją i pobiera dane wyjściowe drzewa parsowania wyrażenia z poprzedniego etapu analizy i generuje algebrizowane drzewo wyrażeń (drzewo procesora zapytań), aby przejść do optymalizacji (w tym przypadku trywialna optymalizacja planu) [ref] .

Spróbowałem jeszcze jednego eksperymentu ( skrypt ), który polegał na ponownym uruchomieniu oryginalnego testu, ale przyjrzałem się trzem różnym przypadkom.

  1. Imię i nazwisko Ciągi o długości 10 znaków bez duplikatów.
  2. Imię i nazwisko Ciągi o długości 50 znaków bez duplikatów.
  3. Imię i nazwisko Ciągi o długości 10 znaków ze wszystkimi duplikatami.

Wykres

Wyraźnie widać, że im dłuższe struny, tym gorsze rzeczy, i odwrotnie, im więcej duplikatów, tym lepsze rzeczy. Jak wspomniano wcześniej, duplikaty nie wpływają na rozmiar planu w pamięci podręcznej, więc zakładam, że musi istnieć proces podwójnej identyfikacji podczas tworzenia samego algebrizowanego drzewa wyrażeń.

Edytować

Miejsce, w którym wykorzystuje się te informacje, pokazuje @Lieven tutaj

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

Ponieważ w czasie kompilacji może określić, że Namekolumna nie ma duplikatów, pomija porządkowanie według 1/ (ID - ID)wyrażenia pomocniczego w czasie wykonywania (sortowanie w planie ma tylko jedną ORDER BYkolumnę) i nie jest zgłaszany błąd dzielenia przez zero. Jeśli do tabeli zostaną dodane duplikaty, operator sortowania pokazuje dwie kolejności według kolumn i zgłaszany jest oczekiwany błąd.

Martin Smith
źródło
6
Magiczna liczba, którą masz, to NumberOfRows / ColumnCount = 250. Zmień zapytanie tak, aby wykorzystywało tylko trzy kolumny, a zmiana nastąpi przy 333. Magiczna liczba 1000 może być czymś w rodzaju maksymalnej liczby parametrów używanych w planie buforowanym. Szwy się „łatwiej”, aby wygenerować plan, <ParameterList>niż jeden z <ConstantScan><Values><Row>listy.
Mikael Eriksson
1
@MikaelEriksson - Zgoda. Wiersz 250 z 1000 wartości jest automatycznie parametryzowany, a wiersz 251 nie, więc wydaje się, że jest to różnica. Nie wiem jednak, dlaczego. Być może spędza czas na sortowaniu dosłownych wartości w poszukiwaniu duplikatów lub czegoś takiego, gdy je ma.
Martin Smith
1
To dość szalona sprawa, po prostu mnie zasmuciła. To świetna odpowiedź, dziękuję
Nie kocham
1
@MikaelEriksson Czy masz na myśli magiczną liczbę NumberOfRows * ColumnCount = 1000?
paparazzo,
1
@Blam - Tak. Gdy łączna liczba elementów jest większa niż 1000 (NumberOfRows * ColumnCount), plan kwerend został zmieniony na użycie <ConstantScan><Values><Row>zamiast <ParameterList>.
Mikael Eriksson
23

Nie jest to zbyt zaskakujące: plan wykonania małej wkładki jest obliczany raz, a następnie 1000 razy ponownie używany. Przetwarzanie i przygotowywanie planu jest szybkie, ponieważ ma tylko cztery wartości do usunięcia. Z drugiej strony plan 1000-wierszowy musi obsługiwać 4000 wartości (lub 4000 parametrów, jeśli sparametryzowano testy C #). Może to z łatwością pochłonąć oszczędność czasu, którą uzyskasz, eliminując 999 powrotów do SQL Server, zwłaszcza jeśli Twoja sieć nie jest zbyt wolna.

dasblinkenlight
źródło
9

Problem prawdopodobnie ma związek z czasem kompilacji zapytania.

Jeśli chcesz przyspieszyć wstawianie, tak naprawdę musisz zawrzeć je w transakcji:

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;

W języku C # można również rozważyć użycie parametru wycenianego w tabeli. Wydawanie wielu poleceń w jednej partii, oddzielając je średnikami, to kolejne podejście, które również pomoże.

RickNZ
źródło
1
Re: „Wydawanie wielu poleceń w jednej partii”: to trochę pomaga, ale niewiele. Ale zdecydowanie zgadzam się z pozostałymi dwiema opcjami albo zawinięcia TRANSAKCJI (czy TRANS faktycznie działa, czy powinien to być po prostu TRAN?) Lub korzystania z TVP.
Solomon Rutzky
1

Podobną sytuację natknąłem się, próbując przekonwertować tabelę z kilkoma 100 000 wierszami za pomocą programu C ++ (MFC / ODBC).

Ponieważ ta operacja zajęła bardzo dużo czasu, pomyślałem, że łączę wiele wstawek w jedną (do 1000 ze względu na ograniczenia MSSQL ). Domyślam się, że wiele pojedynczych instrukcji wstawiania spowodowałoby obciążenie podobne do opisanego tutaj .

Okazuje się jednak, że konwersja trwała właściwie trochę dłużej:

        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

Tak więc 1000 pojedynczych wywołań CDatabase :: ExecuteSql, każde z jedną instrukcją INSERT (metoda 1), jest mniej więcej dwa razy szybszych niż pojedyncze wywołanie CDatabase :: ExecuteSql z wielowierszową instrukcją INSERT z krotkami o wartości 1000 (metoda 2).

Aktualizacja: Następną rzeczą, jaką próbowałem, było spakowanie 1000 oddzielnych instrukcji INSERT w jeden ciąg i zlecenie serwerowi wykonania tego (metoda 3). Okazuje się, że jest to nawet nieco szybsze niż metoda 1.

Edycja: używam programu Microsoft SQL Server Express Edition (64-bitowy) w wersji 10.0.2531.0

uceumern
źródło