Zmień zapytanie, aby poprawić szacunki operatora

14

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.

wprowadź opis zdjęcia tutaj

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:

  1. Dlaczego dziwne początkowe nazewnictwo w łączu pasteThePlan?

    Odpowiedź : Ponieważ użyłem anonimowego planu z Eksploratora SQL Sentry.

  2. 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.

  3. 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 ma SCHEMABINDINGtu zastosowania.

Odpowiedzi na więcej możliwych pytań:

  1. 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 .

  2. 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ę INopcję. Nie sądzę, żeby istniała różnica, nawet poprzez wymianę i tak czy inaczej, nie ma z tym problemu.

Radu Gheorghiu
źródło
Sądzę, że #temptworzenie 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ń to in (select id from #temp)na existspodkwerendę.
ypercubeᵀᴹ
@ ypercubeᵀᴹ To prawda, niewiele mniej stron czyta przy użyciu zmiennej zamiast tabeli tymczasowej.
Radu Gheorghiu
Nawiasem mówiąc, zmienna tabeli zapewni prawidłowe oszacowanie liczby wierszy, gdy zostanie użyta z Opcją (Ponowna kompilacja) - ale nadal nie ma szczegółowych statystyk, liczności itp.
TH
@TH Cóż, spojrzałem na rzeczywisty plan wykonania na szacunki, kiedy użyłem select id from @customAttrValIdszamiast, select id from #tempa szacowana liczba wierszy była 1dla zmiennej i 3dla #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.
Radu Gheorghiu
@RaduGheorghiu Tak, ale w świecie tych facetów opcja (rekompilacja) rzadko jest opcją, a oni również wolą tabele tymczasowe z innych ważnych powodów. Być może szacunek po prostu zawsze wyświetla się niepoprawnie jako 1, ponieważ zmienia on plan, jak widać tutaj: theboreddba.com/Categories/FunWithFlags/...
TH

Odpowiedzi:

12

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:

Węzeł 8 Hash Match (agregacja)

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 #temptabeli i wykonanie odrębnej operacji podczas ładowania tabeli:

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

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 jawnym UPDATE STATISTICS #TempTablepo 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 #Temptabeli 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:

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

PRIMARY KEYDodaje 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 #Temp2powinien 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):

# Populacja Temp2

Po udostępnieniu tego zestawu końcowe zapytanie staje się:

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

Możemy ręcznie przepisać COUNT_BIG(DISTINCT...jako prosty COUNT_BIG(*), ale dzięki nowym kluczowym informacjom optymalizator robi to za nas:

Ostateczny plan

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ę.

Paul White 9
źródło
9

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:

brak równowagi nici

W rezultacie jeden wątek wykonuje całą pracę z wyszukiwaniem indeksu:

poszukiwanie nierównowagi nici

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 TOPoperatora 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.

Joe Obbish
źródło
-2

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.

TH
źródło
3
Istnieje bardzo duża przestrzeń planów dla tego zapytania, z wieloma opcjami kolejności łączenia i zagnieżdżania, równoległością, agregacją lokalną / globalną itp. Itp., Na którą większość miałaby wpływ zmiana statystyk pochodnych (rozkład oraz pierwotna liczność) w węźle planu 10. Należy również pamiętać, że ogólnie należy unikać wskazówek dotyczących łączenia, ponieważ są one dostępne w trybie cichym OPTION(FORCE ORDER), co zapobiega zmianie kolejności połączeń przez optymalizator z sekwencji tekstowej i wielu innych optymalizacji.
Paul White 9
-12

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 SELECTtabelę przestawną w stylu, aby używać standardowego PIVOToperatora SQL .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Zobaczmy więc, SELECT * INTOjest ogólnie mniej wydajny niż standard INSERT Object1 (column list) SELECT column list. Więc przepisałbym to. Następnie, jeśli funkcja 1 została zdefiniowana bez a WITH SCHEMABINDING, dodanie WITH SCHEMABINDINGklauzuli 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)”.

INklauzule tego rodzaju są zawsze bardziej efektywnie napisane jako EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Być może powinienem to napisać w drugą stronę. EXISTSzawsze będzie co najmniej tak dobry, jak IN. 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.

Matthew Sontum
źródło
6
Jeśli wyszukiwanie indeksów nieklastrowych obejmuje zapytanie, prawie zawsze będzie lepsze niż wyszukiwanie indeksów klastrowych, ponieważ z definicji indeks klastrowany zawiera wszystkie kolumny, a indeks nieklastrowany ma mniej kolumn, dlatego będzie wymagał mniejszej liczby wyszukiwań stron (i mniej poziomów kroków do b-drzewa), aby pobrać dane. Nie można więc powiedzieć, że wyszukiwanie indeksów klastrowych zawsze będzie lepsze.
ErikE