Database
 sql >> Baza danych >  >> RDS >> Database

Poprawa rozwiązania mediany górnej/górnej opadającej

W styczniu 2015 roku mój dobry przyjaciel i kolega MVP SQL Server, Rob Farley, napisał o nowatorskim rozwiązaniu problemu znajdowania mediany w wersjach SQL Server sprzed 2012 roku. świetny przykład do wykorzystania w celu zademonstrowania zaawansowanej analizy planu wykonania i podkreślenia pewnych subtelnych zachowań optymalizatora zapytań i silnika wykonywania.

Pojedyncza mediana

Chociaż artykuł Roba skupia się w szczególności na obliczeniu zgrupowanej mediany, zacznę od zastosowania jego metody do dużego problemu z pojedynczą medianą, ponieważ najdobitniej podkreśla ona najważniejsze kwestie. Przykładowe dane będą ponownie pochodzić z oryginalnego artykułu Aarona Bertranda:

CREATE TABLE dbo.obj
(
    id  integer NOT NULL IDENTITY(1,1), 
    val integer NOT NULL
);
 
INSERT dbo.obj WITH (TABLOCKX) 
    (val)
SELECT TOP (10000000) 
    AO.[object_id]
FROM sys.all_columns AS AC
CROSS JOIN sys.all_objects AS AO
CROSS JOIN sys.all_objects AS AO2
WHERE AO.[object_id] > 0
ORDER BY 
    AC.[object_id];
 
CREATE UNIQUE CLUSTERED INDEX cx 
ON dbo.obj(val, id);

Zastosowanie techniki Roba Farleya do tego problemu daje następujący kod:

-- 5800ms
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
--    SELECT COUNT_BIG(*) 
--    FROM dbo.obj AS O
--);
 
SELECT
    Median = AVG(0E + f.val)
FROM
( 
    SELECT TOP (2 - @Count % 2)
        t.val
    FROM 
    ( 
        SELECT TOP (@Count / 2 + 1) 
            z.val
        FROM dbo.obj AS z
        ORDER BY 
            z.val ASC
    ) AS t 
    ORDER BY 
        t.val DESC 
) AS f;
 
SELECT RF = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());

Jak zwykle skomentowałem liczenie wierszy w tabeli i zastąpiłem je stałą, aby uniknąć źródła wariancji wydajności. Plan wykonania ważnego zapytania jest następujący:

To zapytanie trwa 5800ms na moim komputerze testowym.

Wyciek sortowania

Główna przyczyna tej słabej wydajności powinna być jasna, patrząc na powyższy plan wykonania:ostrzeżenie operatora Sort pokazuje, że niewystarczający przydział pamięci obszaru roboczego spowodował rozlanie poziomu 2 (wieloprzebiegowego) do fizycznego pliku tempdb dysk:

W wersjach programu SQL Server sprzed 2012 r. trzeba by oddzielnie monitorować zdarzenia rozlania sortowania, aby to zobaczyć. W każdym razie niewystarczająca rezerwacja pamięci sortowania jest spowodowana błędem oszacowania liczności, jak pokazuje plan przed wykonaniem (szacowany):

Szacunkowa liczność dla 100 wierszy na wejściu Sort to (dziko niedokładne) odgadnięcie przez optymalizator, ze względu na lokalne wyrażenie zmiennej w poprzednim operatorze Top:

Należy zauważyć, że ten błąd oszacowania kardynalności nie jest problemem związanym z celami rzędu. Zastosowanie flagi śledzenia 4138 usunie efekt wiersz-cel poniżej pierwszego Top, ale oszacowanie post-Top nadal będzie zgadywać 100 wierszy (więc rezerwacja pamięci dla sortowania nadal będzie zbyt mała):

Wskazówka na wartość zmiennej lokalnej

Istnieje kilka sposobów rozwiązania tego problemu z oszacowaniem kardynalności. Jedną z opcji jest użycie podpowiedzi w celu dostarczenia optymalizatorowi informacji o wartości przechowywanej w zmiennej:

-- 3250ms
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
--    SELECT COUNT_BIG(*) 
--    FROM dbo.obj AS O
--);
 
SELECT
    Median = AVG(0E + f.val)
FROM
( 
    SELECT TOP (2 - @Count % 2)
        t.val
    FROM 
    ( 
        SELECT TOP (@Count / 2 + 1) 
            z.val
        FROM dbo.obj AS z
        ORDER BY 
            z.val ASC
    ) AS t 
    ORDER BY 
        t.val DESC 
) AS f
OPTION (MAXDOP 1, OPTIMIZE FOR (@Count = 11000000)); -- NEW!
 
SELECT RF = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());

Użycie podpowiedzi poprawia wydajność do 3250 ms od 5800 ms. Z planu powykonawczego wynika, że ​​sortowanie już się nie rozlewa:

Jest to jednak kilka wad. Po pierwsze, ten nowy plan wykonania wymaga 388 MB przyznanie pamięci – pamięć, która w innym przypadku mogłaby być wykorzystana przez serwer do buforowania planów, indeksów i danych:

Po drugie, może być trudno wybrać dobrą liczbę dla podpowiedzi, która będzie dobrze działać we wszystkich przyszłych wykonaniach, bez niepotrzebnego rezerwowania pamięci.

Zauważ również, że musieliśmy wskazać wartość zmiennej, która jest o 10% wyższa niż rzeczywista wartość zmiennej, aby całkowicie wyeliminować wyciek. Nie jest to rzadkie, ponieważ ogólny algorytm sortowania jest bardziej złożony niż proste sortowanie w miejscu. Rezerwowanie pamięci równej rozmiarowi sortowanego zestawu nie zawsze (a nawet ogólnie) wystarczy, aby uniknąć rozlania w czasie wykonywania.

Osadzanie i ponowna kompilacja

Inną opcją jest skorzystanie z Optymalizacji osadzania parametrów włączonej przez dodanie wskazówki dotyczącej ponownej kompilacji na poziomie zapytania w programie SQL Server 2008 SP1 CU5 lub nowszym. Ta czynność pozwoli optymalizatorowi zobaczyć wartość zmiennej w czasie wykonywania i odpowiednio zoptymalizować:

-- 3150ms
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
--    SELECT COUNT_BIG(*) 
--    FROM dbo.obj AS O
--);
 
SELECT
    Median = AVG(0E + f.val)
FROM
( 
    SELECT TOP (2 - @Count % 2)
        t.val
    FROM 
    ( 
        SELECT TOP (@Count / 2 + 1) 
            z.val
        FROM dbo.obj AS z
        ORDER BY 
            z.val ASC
    ) AS t 
    ORDER BY 
        t.val DESC 
) AS f
OPTION (MAXDOP 1, RECOMPILE);
 
SELECT RF = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());

Skraca to czas wykonania do 3150ms – 100 ms lepiej niż przy użyciu OPTIMIZE FOR wskazówka. Powód tej dalszej niewielkiej poprawy można dostrzec w nowym planie powykonawczym:

Wyrażenie (2 – @Count % 2) – jak widzieliśmy wcześniej w drugim napędzie Top – można go teraz złożyć do jednej znanej wartości. Przepisywanie po optymalizacji może następnie połączyć Top z sortowaniem, co daje w wyniku pojedyncze sortowanie Top N. To przepisanie nie było wcześniej możliwe, ponieważ zwijanie Top + Sort do Top N Sort działa tylko ze stałą wartością literału Top (nie zmiennymi lub parametrami).

Zastąpienie Topu i sortowania sortowaniem Top N ma niewielki korzystny wpływ na wydajność (tutaj 100 ms), ale prawie całkowicie eliminuje wymagania dotyczące pamięci, ponieważ sortowanie Top N musi tylko śledzić N najwyższych (lub najniższych) rzędy, a nie cały zestaw. W wyniku tej zmiany algorytmu plan wykonania dla tego zapytania pokazuje, że minimum 1 MB pamięć była zarezerwowana dla Top N Sort w tym planie i tylko 16 KB był używany w czasie wykonywania (pamiętaj, że plan pełnego sortowania wymagał 388 MB):

Unikanie ponownej kompilacji

(Oczywistą) wadą korzystania z podpowiedzi dotyczącej kwerendy dotyczącej rekompilacji jest to, że wymaga ona pełnej kompilacji przy każdym wykonaniu. W tym przypadku obciążenie jest dość małe – około 1 ms czasu procesora i 272 KB pamięci. Mimo to istnieje sposób na ulepszenie zapytania tak, aby uzyskać korzyści z sortowania według Top N bez używania podpowiedzi i bez ponownej kompilacji.

Pomysł pochodzi z rozpoznania, że ​​maksymalna dwóch rzędów będzie wymagane do ostatecznego obliczenia mediany. Może to być jeden wiersz lub dwa w czasie wykonywania, ale nigdy więcej. Ten wgląd oznacza, że ​​możemy dodać logicznie nadmiarowy górny (2) krok pośredni do zapytania w następujący sposób:

-- 3150ms
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
--    SELECT COUNT_BIG(*) 
--    FROM dbo.obj AS O
--);
 
SELECT
    Median = AVG(0E + f.val)
FROM
( 
    SELECT TOP (2 - @Count % 2)
        t.val
    FROM 
    (
        SELECT TOP (2) -- NEW!
            z.val
        FROM 
        (
            SELECT TOP (@Count / 2 + 1) 
                z.val
            FROM dbo.obj AS z
            ORDER BY 
                z.val ASC
        ) AS z
        ORDER BY z.val DESC
    ) AS t 
    ORDER BY 
        t.val DESC 
) AS f
OPTION (MAXDOP 1);
 
SELECT RF = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());

Nowy Top (z najważniejszym literałem stałym) oznacza, że ​​ostateczny plan wykonania zawiera żądany operator Top N Sort bez ponownej kompilacji:

Wydajność tego planu wykonania pozostaje niezmieniona w stosunku do wersji ze wskazaną rekompilacją przy 3150ms a wymagania dotyczące pamięci są takie same. Należy jednak pamiętać, że brak osadzania parametrów oznacza, że ​​oszacowania kardynalności poniżej sortowania są domysłami 100-wierszowymi (chociaż nie ma to wpływu na wydajność).

Głównym wnioskiem na tym etapie jest to, że jeśli chcesz sortować z małą ilością pamięci Top N, musisz użyć stałego literału lub umożliwić optymalizatorowi zobaczenie literału poprzez optymalizację osadzania parametrów.

Skalar obliczeniowy

Eliminacja 388 MB Przyznanie pamięci (przy jednoczesnym zwiększeniu wydajności o 100 ms) jest z pewnością warte zachodu, ale istnieje znacznie większa wygrana wydajności. Nieprawdopodobnym celem tego ostatecznego ulepszenia jest skalar obliczeniowy tuż nad skanowaniem indeksu klastrowego.

Ten skalar obliczeniowy odnosi się do wyrażenia (0E + f.val) zawarte w AVG agregacja w zapytaniu. Jeśli wydaje ci się to dziwne, jest to dość powszechna sztuczka przy pisaniu zapytań (jak mnożenie przez 1,0), aby uniknąć arytmetyki liczb całkowitych w obliczeniach średniej, ale ma ona kilka bardzo ważnych skutków ubocznych.

Jest tu szczególna sekwencja wydarzeń, którą musimy śledzić krok po kroku.

Po pierwsze, zauważ, że 0E jest stałym dosłownym zerem, z zmienną zmiennoprzecinkową typ danych. Aby dodać to do wartości val kolumny integer, procesor zapytań musi przekonwertować kolumnę z liczby całkowitej na zmiennoprzecinkową (zgodnie z regułami pierwszeństwa typu danych). Podobny rodzaj konwersji byłby konieczny, gdybyśmy zdecydowali się pomnożyć kolumnę przez 1,0 (literał z niejawnym liczbowym typem danych). Ważne jest to, że ta rutynowa sztuczka wprowadza wyrażenie .

Teraz SQL Server zazwyczaj próbuje przesunąć wyrażenia w dół drzewo planu w miarę możliwości podczas kompilacji i optymalizacji. Dzieje się tak z kilku powodów, między innymi w celu ułatwienia dopasowywania wyrażeń z wyliczonymi kolumnami oraz uproszczenia przy użyciu informacji o ograniczeniach. Ta zasada przesuwania w dół wyjaśnia, dlaczego skalar obliczeniowy pojawia się znacznie bliżej poziomu liścia planu niż sugerowałaby oryginalna pozycja tekstowa wyrażenia w zapytaniu.

Ryzyko wykonania tego przesunięcia polega na tym, że wyrażenie może zostać obliczone więcej razy, niż jest to konieczne. Większość planów charakteryzuje się zmniejszaniem liczby wierszy, gdy poruszamy się w górę drzewa planów, ze względu na efekt łączeń, agregacji i filtrów. Przesuwanie wyrażeń w dół drzewa wiąże się zatem z ryzykiem oceny tych wyrażeń więcej razy (tj. w większej liczbie wierszy) niż to konieczne.

Aby to złagodzić, SQL Server 2005 wprowadził optymalizację, dzięki której skalary obliczeniowe mogą po prostu definiować wyrażenie, z pracą polegającą na faktycznej ocenie wyrażenie odroczone dopóki późniejszy operator w planie nie zażąda wyniku. Kiedy ta optymalizacja działa zgodnie z przeznaczeniem, efektem jest uzyskanie wszystkich korzyści płynących z przesuwania wyrażeń w dół drzewa, przy jednoczesnym ocenianiu wyrażenia tylko tyle razy, ile jest to rzeczywiście potrzebne.

Co to wszystko oznacza Compute Scalar

W naszym uruchomionym przykładzie 0E + val wyrażenie było pierwotnie powiązane z AVG zagregowane, więc możemy się spodziewać, że zobaczymy go w (lub nieco później) zagregowanym strumieniu. Jednak to wyrażenie zostało przesunięte drzewo, które stanie się nowym skalarem obliczeniowym zaraz po skanowaniu, z wyrażeniem oznaczonym jako [Wyr1004].

Patrząc na plan wykonania, widzimy, że [Expr1004] odwołuje się do Stream Aggregate (wyciąg z zakładki Plan Explorer Expressions Tab pokazany poniżej):

Jeśli wszystkie rzeczy są równe, ocena wyrażenia [Expr1004] byłaby odroczona dopóki agregat nie potrzebuje tych wartości do części sumy obliczenia średniej. Ponieważ agregat może napotkać tylko jeden lub dwa wiersze, powinno to spowodować, że [Expr1004] zostanie ocenione tylko raz lub dwa razy:

Niestety nie do końca tak to działa. Problemem jest blokujący operator sortowania:

To wymusza ocenę [Expr1004], więc zamiast oceniania go tylko raz lub dwa razy w Stream Aggregate, sortowanie oznacza, że ​​w końcu konwertujemy val kolumna do liczby zmiennoprzecinkowej i dodanie do niej zera 5,000,001 razy!

Obejście

Idealnie byłoby, gdyby SQL Server był nieco mądrzejszy w tym wszystkim, ale dzisiaj tak nie działa. Nie ma wskazówki dotyczącej zapytania, aby zapobiec wypychaniu wyrażeń w dół drzewa planu, a także nie możemy wymusić obliczeń skalarnych za pomocą przewodnika po planach. Nieuchronnie istnieje nieudokumentowana flaga śledzenia, ale nie jest to taka, o której mogę odpowiedzialnie mówić w obecnym kontekście.

Tak więc, na lepsze lub gorsze, to pozostawia nam to próbę znalezienia przepisanego zapytania, które akurat uniemożliwia SQL Server oddzielenie wyrażenia od agregacji i wypchnięcie go poza sortowanie, bez zmiany wyniku zapytania. Nie jest to tak proste, jak mogłoby się wydawać, ale (co prawda nieco dziwnie wyglądająca) modyfikacja poniżej osiąga to za pomocą CASE wyrażenie na niedeterministycznej funkcji wewnętrznej:

-- 2150ms
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
--    SELECT COUNT_BIG(*) 
--    FROM dbo.obj AS O
--);
 
SELECT
    -- NEW!
    Median = AVG(CASE WHEN GETDATE() >= {D '1753-01-01'} THEN 0E + f.val END)
FROM
( 
    SELECT TOP (2 - @Count % 2)
        t.val
    FROM 
    (
        SELECT TOP (2) 
            z.val
        FROM 
        (
            SELECT TOP (@Count / 2 + 1) 
                z.val
            FROM dbo.obj AS z
            ORDER BY 
                z.val ASC
        ) AS z
        ORDER BY z.val DESC
    ) AS t 
    ORDER BY 
        t.val DESC 
) AS f
OPTION (MAXDOP 1);
 
SELECT RF = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());

Plan wykonania dla tej ostatecznej formy zapytania jest pokazany poniżej:

Zauważ, że skalar obliczeniowy nie jest już wyświetlany między skanowaniem indeksu klastrowego a górą. 0E + val wyrażenie jest teraz obliczane w Stream Aggregate w maksymalnie dwóch wierszach (zamiast pięciu milionów!), a wydajność wzrasta o dalsze 32% od 3150ms do 2150ms w rezultacie.

To nadal nie jest tak dobre w porównaniu z wydajnością poniżej sekundy OFFSET i dynamiczne rozwiązania do obliczania mediany kursora, ale nadal stanowią bardzo znaczącą poprawę w stosunku do oryginalnych 5800 ms dla tej metody dla dużego zestawu problemów z pojedynczą medianą.

Oczywiście nie ma gwarancji, że sztuczka CASE zadziała w przyszłości. Na wynos nie tyle chodzi o używanie dziwnych sztuczek wyrażania wielkości liter, ile o potencjalne implikacje wydajnościowe skalarów obliczeniowych. Gdy już wiesz o tym, możesz pomyśleć dwa razy przed pomnożeniem przez 1,0 lub dodaniem zmiennoprzecinkowego zera w obliczeniach średniej.

Aktualizacja: zobacz pierwszy komentarz dotyczący fajnego obejścia autorstwa Roberta Heiniga, które nie wymaga żadnych nieudokumentowanych sztuczek. O czym należy pamiętać, gdy następnym razem będziesz kuszony, aby pomnożyć liczbę całkowitą przez dziesiętną (lub zmiennoprzecinkową) przez jeden w średniej agregacji.

Media zgrupowane

Aby uzyskać kompletność i ściślej powiązać tę analizę z oryginalnym artykułem Roba, na koniec zastosujemy te same ulepszenia do zgrupowanego obliczania mediany. Mniejsze rozmiary zestawów (na grupę) oznaczają oczywiście, że efekty będą mniejsze.

Pogrupowane mediany danych próbki (znowu w postaci pierwotnie stworzonej przez Aarona Bertranda) obejmują dziesięć tysięcy wierszy dla każdego ze stu wyimaginowanych sprzedawców:

CREATE TABLE dbo.Sales
(
    SalesPerson integer NOT NULL, 
    Amount integer NOT NULL
);
 
WITH X AS
(
    SELECT TOP (100) 
        V.number
    FROM master.dbo.spt_values AS V
    GROUP BY 
        V.number
)
INSERT dbo.Sales WITH (TABLOCKX) 
(
    SalesPerson, 
    Amount
)
SELECT 
    X.number,
    ABS(CHECKSUM(NEWID())) % 99
FROM X 
CROSS JOIN X AS X2 
CROSS JOIN X AS X3;
 
CREATE CLUSTERED INDEX cx 
ON dbo.Sales
    (SalesPerson, Amount);

Bezpośrednie zastosowanie rozwiązania Roba Farleya daje następujący kod, który wykonuje się w 560ms na moim komputerze.

-- 560ms Original
DECLARE @s datetime2 = SYSUTCDATETIME();
 
SELECT
    d.SalesPerson, 
    w.Median 
FROM 
( 
    SELECT S.SalesPerson, COUNT_BIG(*) AS y 
    FROM dbo.Sales AS S
    GROUP BY S.SalesPerson
) AS d 
CROSS APPLY 
( 
    SELECT AVG(0E + f.Amount) 
    FROM 
    ( 
        SELECT TOP (2 - d.y % 2)
            t.Amount
        FROM 
        ( 
            SELECT TOP (d.y / 2 + 1)
                z.Amount
            FROM dbo.Sales AS z
            WHERE z.SalesPerson = d.SalesPerson
            ORDER BY z.Amount ASC
        ) AS t 
        ORDER BY t.Amount DESC 
    ) AS f 
) AS w (Median)
OPTION (MAXDOP 1);
 
SELECT DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME());

Plan wykonania ma oczywiste podobieństwa do pojedynczej mediany:

Naszym pierwszym ulepszeniem jest zastąpienie sortowania sortowaniem Top N poprzez dodanie wyraźnego Top (2). To nieznacznie poprawia czas wykonania z 560 ms do 550 ms .

-- 550ms
DECLARE @s datetime2 = SYSUTCDATETIME();
 
SELECT
    d.SalesPerson, 
    w.Median 
FROM 
( 
    SELECT S.SalesPerson, COUNT_BIG(*) AS y 
    FROM dbo.Sales AS S
    GROUP BY S.SalesPerson
) AS d 
CROSS APPLY 
( 
    SELECT AVG(0E + f.Amount) 
    FROM 
    ( 
        SELECT TOP (2 - d.y % 2)
            q.Amount
        FROM 
        (
            -- NEW!
            SELECT TOP (2) t.Amount
            FROM
            ( 
                SELECT TOP (d.y / 2 + 1)
                    z.Amount
                FROM dbo.Sales AS z
                WHERE z.SalesPerson = d.SalesPerson
                ORDER BY z.Amount ASC
            ) AS t 
            ORDER BY t.Amount DESC 
        ) AS q
        ORDER BY q.Amount DESC
    ) AS f 
) AS w (Median)
OPTION (MAXDOP 1);
 
SELECT DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME());

Plan wykonania pokazuje sortowanie Top N zgodnie z oczekiwaniami:

Na koniec wdrażamy dziwnie wyglądające wyrażenie CASE, aby usunąć wypchnięte wyrażenie Compute Scalar. To dodatkowo poprawia wydajność do 450 ms (20% poprawa w stosunku do oryginału):

-- 450ms
DECLARE @s datetime2 = SYSUTCDATETIME();
 
SELECT
    d.SalesPerson, 
    w.Median 
FROM 
( 
    SELECT S.SalesPerson, COUNT_BIG(*) AS y 
    FROM dbo.Sales AS S
    GROUP BY S.SalesPerson
) AS d 
CROSS APPLY 
(
    -- NEW! 
    SELECT AVG(CASE WHEN GETDATE() >= {D '1753-01-01'} THEN 0E + Amount END)
    FROM 
    ( 
        SELECT TOP (2 - d.y % 2)
            q.Amount
        FROM 
        (
            SELECT TOP (2) t.Amount
            FROM
            ( 
                SELECT TOP (d.y / 2 + 1)
                    z.Amount
                FROM dbo.Sales AS z
                WHERE z.SalesPerson = d.SalesPerson
                ORDER BY z.Amount ASC
            ) AS t 
            ORDER BY t.Amount DESC 
        ) AS q
        ORDER BY q.Amount DESC
    ) AS f 
) AS w (Median)
OPTION (MAXDOP 1);
 
SELECT DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME());

Gotowy plan wykonania wygląda następująco:


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Tworzenie klastra Docker Swarm w usłudze Azure Container Service

  2. Eksploracja interfejsów API modułów w Javie 9

  3. Utwórz relację w SQL

  4. Hekaton z niespodzianką:In-memory TVP – część 3

  5. Jaki jest najszybszy sposób obliczenia mediany?