Krótko mówiąc, aktualizujemy małe tabele osób o wartościach z bardzo dużej tabeli osób. W ostatnim teście uruchomienie tej aktualizacji zajmuje około 5 minut.
Natknęliśmy się na coś, co wydaje się najgłupszą możliwą optymalizacją, która na pozór działa idealnie! To samo zapytanie jest teraz uruchamiane w mniej niż 2 minuty i daje te same wyniki, idealnie.
Oto zapytanie. Ostatni wiersz jest dodawany jako „optymalizacja”. Dlaczego intensywne skrócenie czasu zapytania? Czy coś przeoczyliśmy? Czy może to prowadzić do problemów w przyszłości?
UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
Uwagi techniczne: Zdajemy sobie sprawę, że lista liter do przetestowania może wymagać kilku dodatkowych liter. Jesteśmy również świadomi oczywistego marginesu błędu przy korzystaniu z „RÓŻNICY”.
Plan zapytań (zwykły): https://www.brentozar.com/pastetheplan/?id=rypVrypy84V
Plan zapytań (z „optymalizacją”): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E
AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AI
powinien robić, co chcesz, bez konieczności wymieniania wszystkich znaków i trudnego do odczytania koduWHERE
jest fałszywy? W szczególności zauważ, że w porównaniu może być rozróżniana wielkość liter.Latin1_General_100_CI_AI
. W przypadku SQL Server 2012 i nowszych (przynajmniej SQL Server 2019) najlepiej jest używać zestawień z obsługą znaków uzupełniających w najwyższej wersji dla używanych ustawień narodowych. Tak byłobyLatin1_General_100_CI_AI_SC
w tym przypadku. Wersje> 100 (jak dotąd tylko japońskie) nie mają (lub nie potrzebują)_SC
(npJapanese_XJIS_140_CI_AI
.).Odpowiedzi:
Zależy to od danych w twoich tabelach, twoich indeksów, ... Trudno powiedzieć bez możliwości porównania planów wykonania / statystyki czasu io +.
Różnica, której oczekiwałbym, to dodatkowe filtrowanie przed JOIN między dwiema tabelami. W moim przykładzie zmieniłem aktualizacje na wybory do ponownego użycia moich tabel.
Plan wykonania z „optymalizacją”
Plan wykonania
Wyraźnie widać, że ma miejsce operacja filtrowania, w moich danych testowych nie ma zapisów, które zostały odfiltrowane, w wyniku czego nie wprowadzono żadnych ulepszeń.
Plan wykonania bez „optymalizacji”
Plan wykonania
Filtr zniknął, co oznacza, że będziemy musieli polegać na sprzężeniu, aby odfiltrować niepotrzebne rekordy.
Inny powód (powody) Innym powodem / konsekwencją zmiany zapytania może być fakt, że podczas zmiany zapytania utworzono nowy plan wykonania, który okazuje się być szybszy. Przykładem tego jest silnik wybierający innego operatora Join, ale to tylko zgadywanie w tym momencie.
EDYTOWAĆ:
Wyjaśnienie po uzyskaniu dwóch planów zapytań:
Zapytanie odczytuje z dużego stołu 550 mln wierszy i odfiltrowuje je.
Oznacza to, że predykat wykonuje większość filtrowania, a nie predykat seek. W rezultacie dane są odczytywane, ale znacznie mniej zwracane.
Zmuszenie serwera sql do użycia innego indeksu (planu zapytań) / dodanie indeksu może rozwiązać ten problem.
Dlaczego więc zapytanie optymalizacyjne nie ma tego samego problemu?
Ponieważ używany jest inny plan zapytań, ze skanem zamiast wyszukiwania.
Bez szukania, ale zwracanie do pracy tylko 4M wierszy.
Następna różnica
Pomijając różnicę aktualizacji (nic nie jest aktualizowane w zoptymalizowanym zapytaniu) dopasowanie zoptymalizowane jest używane w zoptymalizowanym zapytaniu:
Zamiast łączenia zagnieżdżonego w pętli w niezoptymalizowanym:
Pętla zagnieżdżona jest najlepsza, gdy jeden stół jest mały, a drugi duży. Ponieważ oba są zbliżone do tego samego rozmiaru, argumentowałbym, że dopasowanie skrótu jest lepszym wyborem w tym przypadku.
Przegląd
Zoptymalizowane zapytanie
Plan zoptymalizowanego zapytania ma paralelizm, wykorzystuje sprzężenie z dopasowaniem mieszającym i wymaga mniejszego resztkowego filtrowania we / wy. Wykorzystuje również mapę bitową, aby wyeliminować kluczowe wartości, które nie mogą wytworzyć żadnych wierszy łączenia. (Również nic nie jest aktualizowane)
Non-zoptymalizowane zapytania Plan nieoptymalizowanym kwerendy ma parallellism, wykorzystuje Łączenie zagnieżdżone, i musi zrobić resztkowego IO filtrowanie na 550m rekordów. (Trwa także aktualizacja)
Co możesz zrobić, aby poprawić niezoptymalizowane zapytanie?
Zmienianie indeksu na imię i nazwisko na liście kluczowych kolumn:
CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name na dbo.largeTableOfPeople (data urodzenia, imię, nazwisko) obejmują (id)
Ale ze względu na użycie funkcji i dużą tabelę może to nie być optymalne rozwiązanie.
(HASH JOIN, MERGE JOIN)
do zapytaniaDane testowe + wykorzystane zapytania
źródło
Nie jest jasne, czy drugie zapytanie jest w rzeczywistości ulepszeniem.
Plany wykonania zawierają QueryTimeStats, które pokazują znacznie mniej dramatyczną różnicę niż podano w pytaniu.
Powolny plan upłynął
257,556 ms
(4 minuty 17 sekund). Szybki plan upłynął190,992 ms
(3 minuty 11 sekund), mimo że działał ze stopniem równoległości równym 3.Ponadto drugi plan działał w bazie danych, w której po dołączeniu nie było żadnej pracy.
Pierwszy plan
Drugi plan
Aby ten dodatkowy czas można było wyjaśnić pracą potrzebną do zaktualizowania 3,5 miliona wierszy (praca wymagana przez operatora aktualizacji do zlokalizowania tych wierszy, zatrzaśnięcia strony, zapisania aktualizacji na stronie i dziennika transakcji nie jest nieistotna)
Jeśli jest to w rzeczywistości odtwarzalne przy porównywaniu z podobnymi, to wyjaśnienie jest takie, że masz szczęście w tym przypadku.
Filtr z 37
IN
warunkami wyeliminował tylko 51 wierszy z 4 008 334 w tabeli, ale optymalizator uznał, że wyeliminuje znacznie więcejTakie niepoprawne oszacowania liczności są zwykle złe. W tym przypadku stworzono plan o innym kształcie (i równoległym), który najwyraźniej (?) Działał lepiej dla ciebie, pomimo wycieków skrótu spowodowanych ogromnym niedoszacowaniem.
Bez
TRIM
SQL Server jest w stanie przekonwertować to na przedział zakresu w histogramie kolumny podstawowej i podać znacznie dokładniejsze oszacowania, ale dzięki temuTRIM
można tylko zgadywać .Charakter zgadywania może się różnić, ale szacunek dla jednego orzeczenia
LEFT(TRIM(largeTbl.last_name), 1)
jest w niektórych okolicznościach * tylko oszacowanytable_cardinality/estimated_number_of_distinct_column_values
.Nie jestem pewien, jakie dokładnie okoliczności - rozmiar danych wydaje się odgrywać rolę. Byłem w stanie odtworzyć to z szerokimi typami danych o stałej długości, jak tutaj, ale otrzymałem inny, wyższy zgadywanka z
varchar
(który właśnie użył płaskiego 10% zgadnięcia i oszacował 100 000 wierszy). @Solomon Rutzky zwraca uwagę, że jeślivarchar(100)
jest wypełniony spacjami końcowymi, jak to się dzieje wchar
przypadku niższej wartości szacunkowejIN
Lista rozpręża sięOR
i SQL Server używa wykładniczy backoff z maksymalnie 4 orzeczników pod uwagę. Tak więc219.707
oszacowanie jest uzyskiwane w następujący sposób.źródło