Dlaczego jest to szybsze i bezpieczne w użyciu? (GDZIE pierwsza litera jest w alfabecie)

10

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

JohnF
źródło
4
Mała odpowiedź na twoją uwagę techniczną: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIpowinien robić, co chcesz, bez konieczności wymieniania wszystkich znaków i trudnego do odczytania kodu
Erik A
Czy masz wiersze, w których końcowy warunek WHEREjest fałszywy? W szczególności zauważ, że w porównaniu może być rozróżniana wielkość liter.
jpmc26
@ErikvonAsmuth stanowi doskonały punkt. Ale tylko mała uwaga techniczna: w przypadku SQL Server 2008 i 2008 R2 najlepiej jest użyć sortowania w wersji „100” (jeśli jest dostępne dla używanej kultury / ustawień regionalnych). Tak by było 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łoby Latin1_General_100_CI_AI_SCw tym przypadku. Wersje> 100 (jak dotąd tylko japońskie) nie mają (lub nie potrzebują) _SC(np Japanese_XJIS_140_CI_AI.).
Solomon Rutzky

Odpowiedzi:

9

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ą” wprowadź opis zdjęcia tutaj

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” wprowadź opis zdjęcia tutaj

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. wprowadź opis zdjęcia tutaj

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.

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

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:

wprowadź opis zdjęcia tutaj

Zamiast łączenia zagnieżdżonego w pętli w niezoptymalizowanym:

wprowadź opis zdjęcia tutaj

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 wprowadź opis zdjęcia tutaj

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 wprowadź opis zdjęcia tutaj 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.

  • Aktualizuję statystyki, używając rekompilacji, aby uzyskać lepszy plan.
  • Dodanie OPCJI (HASH JOIN, MERGE JOIN)do zapytania
  • ...

Dane testowe + wykorzystane zapytania

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Randi Vertongen
źródło
8

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

wprowadź opis zdjęcia tutaj

Drugi plan

wprowadź opis zdjęcia tutaj

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 INwarunkami wyeliminował tylko 51 wierszy z 4 008 334 w tabeli, ale optymalizator uznał, że wyeliminuje znacznie więcej

wprowadź opis zdjęcia tutaj

   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', 'æ', 'ä', 'ø', 'å' ) 

Takie 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 TRIMSQL Server jest w stanie przekonwertować to na przedział zakresu w histogramie kolumny podstawowej i podać znacznie dokładniejsze oszacowania, ale dzięki temu TRIMmoż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 oszacowany table_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śli varchar(100)jest wypełniony spacjami końcowymi, jak to się dzieje w charprzypadku niższej wartości szacunkowej

INLista rozpręża się ORi SQL Server używa wykładniczy backoff z maksymalnie 4 orzeczników pod uwagę. Tak więc 219.707oszacowanie jest uzyskiwane w następujący sposób.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Martin Smith
źródło