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:
Odpowiedzi:
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ę
VALUES
klauzul i zarejestrować czas kompilacji.Następnie podzieliłem czas kompilacji przez liczbę wierszy, aby uzyskać średni czas kompilacji na klauzulę. Wyniki poniżej
Aż do 250
VALUES
klauzul czas kompilacji / liczba klauzul ma niewielką tendencję wzrostową, ale nic zbyt dramatycznego.Ale potem następuje nagła zmiana.
Ta sekcja danych jest pokazana poniżej.
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.
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 ALL
konstrukcji w SQL Server 2005.Poniżej znajduje się typowy ślad stosu
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
DeriveNormalizedGroupProperties
jest 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.
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
Ponieważ w czasie kompilacji może określić, że
Name
kolumna nie ma duplikatów, pomija porządkowanie według1/ (ID - ID)
wyrażenia pomocniczego w czasie wykonywania (sortowanie w planie ma tylko jednąORDER BY
kolumnę) 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.źródło
<ParameterList>
niż jeden z<ConstantScan><Values><Row>
listy.<ConstantScan><Values><Row>
zamiast<ParameterList>
.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.
źródło
Problem prawdopodobnie ma związek z czasem kompilacji zapytania.
Jeśli chcesz przyspieszyć wstawianie, tak naprawdę musisz zawrzeć je w transakcji:
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.
źródło
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:
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
źródło