Mam zapytanie, które działa w akceptowalnym czasie, ale chcę wycisnąć z niego jak największą wydajność.
Operacja, którą próbuję ulepszyć, to „Wyszukiwanie indeksu” po prawej stronie planu, od węzła 17.
Dodałem odpowiednie indeksy, ale szacunki, które otrzymuję dla tej operacji, są o połowę mniejsze niż powinny.
Szukałem zmiany indeksów, dodania tabeli tymczasowej i ponownego napisania zapytania, ale nie mogłem tego bardziej uprościć, aby uzyskać prawidłowe oszacowania.
Czy ktoś ma jakieś sugestie, co jeszcze mogę spróbować?
Pełny plan i jego szczegóły można znaleźć tutaj .
Nieanonimizowany plan można znaleźć tutaj.
Aktualizacja:
Mam wrażenie, że początkowa wersja pytania wywołała wiele zamieszania, więc dodam oryginalny kod z kilkoma wyjaśnieniami.
create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
set nocount on;
declare @dist_ca_id int;
select *
into #temp
from @customAttrValIds
where id is not null;
select @dist_ca_id = count(distinct CustomAttrID)
from CustomAttributeValues c
inner join #temp a on c.Id = a.id;
select a.Id
, a.AssortmentId
from Assortments a
inner join AssortmentCustomAttributeValues acav
on a.Id = acav.Assortment_Id
inner join CustomAttributeValues cav
on cav.Id = acav.CustomAttributeValue_Id
where a.AssortmentType = @asType
and acav.CustomAttributeValue_Id in (select id from #temp)
group by a.AssortmentId
, a.Id
having count(distinct cav.CustomAttrID) = @dist_ca_id
option(recompile);
end
Odpowiedzi:
Dlaczego dziwne początkowe nazewnictwo w łączu pasteThePlan?
Odpowiedź : Ponieważ użyłem anonimowego planu z Eksploratora SQL Sentry.
Dlaczego
OPTION RECOMPILE
?Odpowiedź : Ponieważ mogę sobie pozwolić na rekompilacje w celu uniknięcia wąchania parametrów (dane są / mogłyby być zniekształcone). Przetestowałem i cieszę się z planu generowanego przez Optymalizator podczas jego używania
OPTION RECOMPILE
.WITH SCHEMABINDING
?Odpowiedź : Naprawdę chciałbym tego uniknąć i używałbym tego tylko wtedy, gdy mam widok indeksowany. W każdym razie jest to funkcja systemowa (
COUNT()
), więc nie maSCHEMABINDING
tu zastosowania.
Odpowiedzi na więcej możliwych pytań:
Dlaczego używać
INSERT INTO #temp FROM @customAttrributeValues
?Odpowiedź : Ponieważ zauważyłem i teraz wiem, że przy użyciu zmiennych podłączonych do zapytania, wszelkie szacunki, które wynikają z pracy ze zmienną, to zawsze 1. I przetestowałem umieszczenie danych w tabeli tymczasowej, a Szacowany jest wtedy równy Rzeczywistym Wierszom .
Dlaczego użyłem
and acav.CustomAttributeValue_Id in (select id from #temp)
?Odpowiedź : Mogłem go zastąpić JOIN na #temp, ale programiści byli bardzo zdezorientowani i zaproponowali tę
IN
opcję. Nie sądzę, żeby istniała różnica, nawet poprzez wymianę i tak czy inaczej, nie ma z tym problemu.
źródło
#temp
tworzenie i użytkowanie będą stanowić problem dla wydajności, a nie zysk. Zapisujesz do nieindeksowanej tabeli, z której możesz skorzystać tylko raz. Spróbuj całkowicie go usunąć (i ewentualnie zmień toin (select id from #temp)
naexists
podkwerendę.select id from @customAttrValIds
zamiast,select id from #temp
a szacowana liczba wierszy była1
dla zmiennej i3
dla #temp (które pasowały do rzeczywistej liczby wierszy). Dlatego otrzymuje@
się#
. I DO pamiętam rozmowę (od Brent O lub Aaron Bertrand), gdzie powiedział, że podczas korzystania zmienną tbl szacunki na który zawsze będzie 1. i jako poprawa aby uzyskać lepsze szacunki będą korzystać z tabeli tymczasowej.Odpowiedzi:
Plan został skompilowany na wystąpieniu SQL Server 2008 R2 RTM (kompilacja 10.50.1600). Należy zainstalować dodatek Service Pack 3 (kompilacja 10.50.6000), a następnie najnowsze łaty, aby doprowadzić go do (bieżącej) ostatniej kompilacji 10.50.6542. Jest to ważne z wielu powodów, w tym z bezpieczeństwa, poprawek błędów i nowych funkcji.
Optymalizacja osadzania parametrów
W związku z niniejszym pytaniem program SQL Server 2008 R2 RTM nie obsługiwał optymalizacji osadzania parametrów (PEO)
OPTION (RECOMPILE)
. W tej chwili ponosisz koszty ponownej kompilacji, nie zdając sobie sprawy z jednej z głównych korzyści.Gdy PEO jest dostępny, SQL Server może wykorzystywać dosłowne wartości przechowywane w zmiennych lokalnych i parametrach bezpośrednio w planie zapytań. Może to prowadzić do dramatycznych uproszczeń i wzrostu wydajności. Więcej informacji na ten temat znajduje się w moim artykule Parch Sniffing, Osadzanie i Opcje RECOMPILE .
Hash, sortuj i wymieniaj wycieki
Są one wyświetlane tylko w planach wykonania, gdy zapytanie zostało skompilowane na SQL Server 2012 lub nowszym. We wcześniejszych wersjach musieliśmy monitorować wycieki, gdy zapytanie było wykonywane przy użyciu Profiler lub Extended Events. Wycieki zawsze powodują fizyczne we / wy do (i od) stałej pamięci masowej tempdb , co może mieć ważne konsekwencje dla wydajności, szczególnie jeśli wyciek jest duży lub ścieżka we / wy jest pod presją.
W twoim planie wykonania są dwa operatory dopasowania mieszania (agregacji). Pamięć zarezerwowana dla tabeli skrótów jest oparta na szacunkach dla wierszy wyjściowych (innymi słowy, jest proporcjonalna do liczby grup znalezionych w czasie wykonywania). Przydzielona pamięć jest ustalana tuż przed rozpoczęciem wykonywania i nie może rosnąć podczas wykonywania, niezależnie od ilości wolnej pamięci, jaką ma instancja. W dostarczonym planie oba operatory dopasowania mieszania (agregujące) generują więcej wierszy niż oczekiwany optymalizator, więc może wystąpić wyciek do tempdb w czasie wykonywania.
W planie jest także operator dopasowania mieszania (przyłączenia wewnętrznego). Pamięć zarezerwowana dla tabeli skrótów jest oparta na szacunkach dla wierszy wejściowych po stronie sondy . Dane wejściowe sondy szacują 847,399 wierszy, ale w czasie wykonywania napotkano 1 223 636. Nadmiar ten może również powodować wyciek mieszania.
Zbędne agregaty
Dopasowanie mieszania (agregacja) w węźle 8 wykonuje operację grupowania
(Assortment_Id, CustomAttrID)
, ale wiersze wejściowe są równe wierszom wyjściowym:Sugeruje to, że kombinacja kolumn jest kluczem (więc grupowanie jest semantycznie niepotrzebne). Koszt wykonania redundantnej agregacji jest zwiększony przez konieczność dwukrotnego przekazania 1,4 miliona wierszy przez wymiany partycjonowania mieszającego (operatory równoległości po obu stronach).
Biorąc pod uwagę, że zaangażowane kolumny pochodzą z różnych tabel, przekazanie tej informacji o unikatowości do optymalizatora jest trudniejsze niż zwykle, aby uniknąć zbędnej operacji grupowania i niepotrzebnych wymian.
Niewystarczająca dystrybucja wątków
Jak zauważono w odpowiedzi Joe Obbisha , wymiana w węźle 14 używa partycjonowania mieszającego do rozdzielania wierszy między wątkami. Niestety niewielka liczba wierszy i dostępnych harmonogramów oznacza, że wszystkie trzy wiersze kończą się na jednym wątku. Pozornie równoległy plan przebiega szeregowo (z równoległym napowietrznym) aż do wymiany w węźle 9.
Możesz rozwiązać ten problem (w celu uzyskania podziału na rundy lub podziału na partycje), eliminując Odrębne sortowanie w węźle 13. Najprostszym sposobem na to jest utworzenie klastrowego klucza podstawowego na
#temp
tabeli i wykonanie odrębnej operacji podczas ładowania tabeli:Tymczasowe buforowanie statystyk tabeli
Pomimo użycia
OPTION (RECOMPILE)
SQL Server nadal może buforować tymczasowy obiekt tabeli i powiązane statystyki między wywołaniami procedur. Jest to ogólnie pożądana optymalizacja wydajności, ale jeśli tymczasowa tabela jest zapełniona podobną ilością danych przy sąsiednich wywołaniach procedur, ponownie skompilowany plan może być oparty na niepoprawnych statystykach (buforowanych z poprzedniego wykonania). Jest to szczegółowo opisane w moich artykułach, Tabele tymczasowe w procedurach przechowywanych i Objaśnienie buforowania tabel tymczasowych .Aby tego uniknąć, należy używać
OPTION (RECOMPILE)
razem z jawnymUPDATE STATISTICS #TempTable
po zapełnieniu tabeli tymczasowej i przed odwołaniem do niej w zapytaniu.Zapytanie przepisz
W tej części założono, że zmiany w tworzeniu
#Temp
tabeli zostały już wprowadzone.Biorąc pod uwagę koszty możliwych wycieków skrótu i zbędnego agregatu (i otaczających go giełd), opłaca się zmaterializować zestaw w węźle 10:
PRIMARY KEY
Dodaje się w oddzielnym etapie, aby zapewnić budowanie indeksu ma dokładnych informacji liczności, oraz w celu uniknięcia tymczasowe statystyki tabel buforowanie problemu.Ta materializacja najprawdopodobniej wystąpi w pamięci (unikając tempdb I / O), jeśli instancja ma wystarczającą ilość dostępnej pamięci. Jest to jeszcze bardziej prawdopodobne po uaktualnieniu do SQL Server 2012 (SP1 CU10 / SP2 CU1 lub nowszy), który poprawił zachowanie Eager Write .
Ta akcja dostarcza optymalizatorowi dokładnych informacji o liczności zbioru pośredniego, pozwala mu tworzyć statystyki i pozwala nam zadeklarować
(Assortment_Id, CustomAttrID)
jako klucz.Plan dla populacji
#Temp2
powinien wyglądać następująco (zwróć uwagę na skanowanie indeksu klastrowego#Temp
, brak sortowania odrębnego, a wymiana wykorzystuje teraz partycjonowanie rzędów w trybie round-robin):Po udostępnieniu tego zestawu końcowe zapytanie staje się:
Możemy ręcznie przepisać
COUNT_BIG(DISTINCT...
jako prostyCOUNT_BIG(*)
, ale dzięki nowym kluczowym informacjom optymalizator robi to za nas:Ostateczny plan może wykorzystywać sprzężenie pętli / mieszania / scalania w zależności od informacji statystycznych o danych, do których nie mam dostępu. Jeszcze jedna mała uwaga: założyłem, że
CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
istnieje taki indeks .W każdym razie ważną rzeczą w ostatecznych planach jest to, że oszacowania powinny być znacznie lepsze, a złożona sekwencja operacji grupowania została zredukowana do pojedynczego agregatu strumienia (który nie wymaga pamięci, a zatem nie może się rozlać na dysk).
Trudno jest powiedzieć, że wydajność będzie rzeczywiście być lepiej w tym przypadku z dodatkowym tabeli tymczasowej, ale szacunki i wybory plan będzie znacznie bardziej odporne na zmiany objętości i dystrybucji danych w czasie. W dłuższej perspektywie może to być cenniejsze niż niewielki wzrost wydajności. W każdym razie masz teraz znacznie więcej informacji, na których możesz oprzeć swoją ostateczną decyzję.
źródło
Szacunki dotyczące liczności w zapytaniu są w rzeczywistości bardzo dobre. Rzadko zdarza się, aby liczba szacowanych wierszy dokładnie odpowiadała liczbie rzeczywistych wierszy, zwłaszcza gdy masz tak wiele złączeń. Szacunki dotyczące liczby dołączeń są trudne dla optymalizatora, aby uzyskać poprawność. Należy zauważyć, że liczba szacowanych wierszy dla wewnętrznej części zagnieżdżonej pętli przypada na wykonanie tej pętli. Kiedy więc SQL Server mówi, że 463869 wierszy zostanie pobranych z indeksem, prawdziwym szacunkiem jest w tym przypadku liczba wykonań (2) * 463869 = 927738, co nie jest tak dalekie od rzeczywistej liczby wierszy, 1391608. Zaskakujące, liczba szacowanych wierszy jest prawie idealna natychmiast po połączeniu zagnieżdżonej pętli w węźle o numerze 10.
Słabe oszacowania liczności są głównie problemem, gdy optymalizator zapytań wybiera niewłaściwy plan lub nie przyznaje wystarczającej ilości pamięci do planu. Nie widzę żadnych wycieków do tempdb dla tego planu, więc pamięć wygląda dobrze. Dla wywoływanego połączenia zagnieżdżonej pętli masz mały zewnętrzny stół i indeksowany wewnętrzny stół. Co z tym jest nie tak? Mówiąc ściślej, czego oczekiwałbyś od optymalizatora zapytań inaczej?
Pod względem poprawy wydajności wyróżnia mnie to, że SQL Server używa algorytmu mieszającego do dystrybucji równoległych wierszy, co powoduje, że wszystkie są w tym samym wątku:
W rezultacie jeden wątek wykonuje całą pracę z wyszukiwaniem indeksu:
Oznacza to, że zapytanie faktycznie nie jest uruchamiane równolegle, dopóki operator partycji nie przesyła strumieniowo w węźle o identyfikatorze 9. To, czego prawdopodobnie potrzebujesz, to okrągłe partycjonowanie robin, tak aby każdy wiersz kończył się na swoim własnym wątku. Pozwoli to dwóm wątkom na wyszukiwanie indeksu dla identyfikatora węzła 17. Dodanie zbędnego
TOP
operatora może doprowadzić do podziału partycji robin. Mogę tutaj dodać szczegóły, jeśli chcesz.Jeśli naprawdę chcesz skupić się na szacunkach liczności, możesz umieścić wiersze po pierwszym złączeniu w tabeli tymczasowej. Jeśli zbierzesz statystyki dotyczące tabeli tymczasowej, która daje optymalizatorowi więcej informacji na temat tabeli zewnętrznej dla wywołanego połączenia zagnieżdżonej pętli. Może to również prowadzić do partycjonowania robinów okrągłych.
Jeśli nie używasz flag śledzenia 4199 lub 2301, możesz je rozważyć. Flaga śledzenia 4199 oferuje wiele różnych poprawek optymalizatora, ale mogą zmniejszyć niektóre obciążenia. Flaga śledzenia 2301 zmienia niektóre założenia dotyczące liczności łączenia optymalizatora zapytań i sprawia, że jest ono trudniejsze. W obu przypadkach przetestuj dokładnie, zanim je włączysz.
źródło
Wierzę, że uzyskanie lepszych danych szacunkowych dla tego sprzężenia nie zmieni planu, chyba że 1,4 miliona jest wystarczającą częścią tabeli, aby optymalizator wybrał skanowanie indeksu (a nie klastra) z łączeniem mieszającym lub scalającym. Podejrzewam, że nie byłoby to tutaj prawdą, ani faktycznie nie byłoby pomocne, ale możesz przetestować efekty, zastępując sprzężenie wewnętrzne przez CustomAttributeValues wewnętrznym łączeniem mieszającym i łączeniem scalającym .
Spojrzałem też szerzej na kod i nie widzę żadnego sposobu, aby go ulepszyć - oczywiście chciałbym udowodnić, że się mylę. A jeśli masz ochotę opublikować pełną logikę tego, co próbujesz osiągnąć, byłbym zainteresowany innym spojrzeniem.
źródło
OPTION(FORCE ORDER)
, co zapobiega zmianie kolejności połączeń przez optymalizator z sekwencji tekstowej i wielu innych optymalizacji.Nie zamierzasz się poprawiać z [nieklastrowego] indeksu szukania. Jedyną lepszą rzeczą niż wyszukiwanie indeksów nieklastrowych jest Wyszukiwanie indeksów klastrowych.
Ponadto przez ostatnie dziesięć lat byłem SQL DBA, a przez pięć lat programistą SQL. Z mojego doświadczenia wynika, że bardzo rzadko można znaleźć ulepszenie zapytania SQL poprzez przestudiowanie planu wykonania, którego nie można znaleźć za pomocą innych środków. Głównym powodem generowania planu wykonania jest to, że często sugeruje brakujące indeksy, które można dodać w celu poprawy wydajności.
Głównym wzrostem wydajności będzie dostosowanie samego zapytania SQL, jeśli występuje tam jakaś nieefektywność. Na przykład kilka miesięcy temu dostałem funkcję SQL, która działa 160 razy szybciej, przepisując
SELECT UNION SELECT
tabelę przestawną w stylu, aby używać standardowegoPIVOT
operatora SQL .Zobaczmy więc,
SELECT * INTO
jest ogólnie mniej wydajny niż standardINSERT Object1 (column list) SELECT column list
. Więc przepisałbym to. Następnie, jeśli funkcja 1 została zdefiniowana bez aWITH SCHEMABINDING
, dodanieWITH SCHEMABINDING
klauzuli powinno pozwolić jej działać szybciej.Wybrałeś wiele aliasów, które nie mają sensu, np. Aliasing Object2 jako Object3. Powinieneś wybrać lepsze aliasy, które nie zaciemniają kodu. Masz „Object7.Column5 in (wybierz Kolumnę1 z Object1)”.
IN
klauzule tego rodzaju są zawsze bardziej efektywnie napisane jakoEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)
. Być może powinienem to napisać w drugą stronę.EXISTS
zawsze będzie co najmniej tak dobry, jakIN
. Nie zawsze jest lepiej, ale zwykle jest.Wątpię również, czy
option(recompile)
poprawia się tutaj wydajność zapytania. Chciałbym przetestować usunięcie go.źródło