Dlaczego istnieją różnice w planie wykonania między OFFSET… FETCH a starym schematem ROW_NUMBER?

15

Nowy OFFSET ... FETCHmodel wprowadzony wraz z SQL Server 2012 oferuje proste i szybsze stronicowanie. Dlaczego w ogóle istnieją jakiekolwiek różnice, biorąc pod uwagę, że te dwie formy są semantycznie identyczne i bardzo powszechne?

Zakłada się, że optymalizator rozpoznaje oba i optymalizuje je (trywialnie) w pełni.

Oto bardzo prosty przypadek, w którym OFFSET ... FETCHwedług szacunków kosztów jest on ~ 2x szybszy.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

Ten przypadek testowy można zmieniać, tworząc CI object_idlub dodając filtry, ale nie można usunąć wszystkich różnic w planie. OFFSET ... FETCHjest zawsze szybszy, ponieważ wykonuje mniej pracy w czasie wykonywania.

usr
źródło
Nie jestem pewien, więc umieszczam to jako komentarz, ale myślę, że to dlatego, że masz taką samą kolejność według warunków dla numeracji wierszy i końcowego zestawu wyników. Ponieważ w drugim warunku optymalizator wie o tym, nie trzeba ponownie sortować wyników. W pierwszym przypadku należy jednak upewnić się, że wyniki z zewnętrznego wyboru są posortowane, a także numerację wierszy w wyniku wewnętrznym. Utworzenie odpowiedniego indeksu dla #objects powinno rozwiązać problem
Akash

Odpowiedzi:

13

Przykłady w pytaniu nie dają takich samych wyników (w OFFSETprzykładzie występuje błąd „jeden po drugim”). Poniższe zaktualizowane formularze rozwiązują ten problem, usuwają dodatkowe sortowanie ROW_NUMBERsprawy i wykorzystują zmienne, aby rozwiązanie było bardziej ogólne:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

ROW_NUMBERPlan ma szacunkowy koszt 0.0197935 :

Plan numerów wierszy

OFFSETPlan ma szacunkowy koszt 0.0196955 :

Plan offsetowy

Jest to oszczędność 0,000098 szacowanych jednostek kosztów (chociaż OFFSETplan wymagałby dodatkowych operatorów, jeśli chcesz zwrócić numer wiersza dla każdego wiersza). OFFSETPlan nadal będą nieco tańsze, ogólnie rzecz biorąc, ale pamiętam, że szacowane koszty są dokładnie tym - nadal wymagane jest prawdziwe testy. Większość kosztów w obu planach to koszt pełnego rodzaju zestawu danych wejściowych, więc pomocne indeksy byłyby korzystne dla obu rozwiązań.

Tam, gdzie stosowane są stałe wartości literalne (np. OFFSET 30W oryginalnym przykładzie), optymalizator może użyć sortowania TopN zamiast pełnego sortowania, po którym następuje Top. Gdy wiersze potrzebne w sortowaniu TopN są stałym literałem, a <= 100 (suma OFFSETi FETCH) silnik wykonawczy może użyć innego algorytmu sortowania, który może działać szybciej niż uogólnione sortowanie TopN. Wszystkie trzy przypadki mają ogólnie różne charakterystyki wydajności.

Powód, dla którego optymalizator nie przekształca automatycznie ROW_NUMBERwzorca składni OFFSET, jest kilka powodów:

  1. Niemal niemożliwe jest napisanie transformacji, która pasowałaby do wszystkich istniejących zastosowań
  2. Automatyczne przekształcanie niektórych zapytań stronicujących i brak innych może być mylące
  3. OFFSETPlan nie gwarantuje się lepiej we wszystkich przypadkach

Jeden przykład dla trzeciego punktu powyżej występuje, gdy zestaw stronicowania jest dość szeroki. Znacznie bardziej wydajne może być wyszukiwanie potrzebnych kluczy za pomocą indeksu nieklastrowanego i ręczne wyszukiwanie indeksu klastrowanego w porównaniu ze skanowaniem indeksu za pomocą OFFSETlub ROW_NUMBER. Są dodatkowe kwestie do rozważenia, jeśli aplikacja stronicująca musi wiedzieć, ile jest w sumie wierszy lub stron. Nie ma innego dobra dyskusja o zaletach „klucza szukać” i „przesunięcie” metody tutaj .

Ogólnie rzecz biorąc, prawdopodobnie lepiej jest, aby ludzie podjęli świadomą decyzję o zmianie zapytań stronicowania, aby w OFFSETrazie potrzeby użyć ich po dokładnych testach.

Paul White 9
źródło
1
Dlatego przyczyną nie przeprowadzania transformacji w typowych przypadkach jest prawdopodobnie zbyt trudny do znalezienia akceptowalny kompromis inżynieryjny. Podałeś dobre powody, dla których mogło tak być; Muszę powiedzieć, że to dobra odpowiedź. Wiele spostrzeżeń i nowych przemyśleń. Pozostawię pytanie otwarte, a następnie wybiorę najlepszą odpowiedź.
usr
5

Po lekkim skrzypieniu zapytania otrzymuję jednakowy kosztorys (50/50) i równe statystyki IO:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Pozwala to uniknąć dodatkowego sortowania, które pojawia się w Twojej wersji, sortując dalej rzamiast object_id.

Mark Storey-Smith
źródło
Dziękuję za ten wgląd. Teraz, gdy o tym myślę, widziałem, że optymalizator nie rozumie wcześniej posortowanej natury wyniku ROW_NUMBER. Uważa, że ​​zestaw jest nieuporządkowany przez object_id. Lub przynajmniej nie posortowane zarówno według r, jak i object_id.
usr
2
@usr ORDER BY, którego używa ROW_NUMBER (), określa sposób przypisywania liczb. Nic nie obiecuje kolejności wyjściowej - to osobne. Zdarza się tak, że często się pokrywa, ale nie jest to gwarantowane.
Aaron Bertrand
@AaronBertrand Rozumiem, że ROW_NUMBER nie porządkuje danych wyjściowych. Ale jeśli ROW_NUMBER jest uporządkowany według tych samych kolumn co dane wyjściowe, to zagwarantowane jest to samo zamówienie , prawda? Optymalizator zapytań mógłby więc wykorzystać ten fakt. Dlatego w tym zapytaniu zawsze nie są potrzebne dwie operacje sortowania .
usr
1
@usr natrafiłeś na typowy przypadek użycia, którego nie uwzględnia optymalizator, ale nie jest to jedyny przypadek użycia. Rozważ przypadki, w których kolejność w ROW_NUMBER () to ta kolumna i coś jeszcze. Lub gdy zewnętrzny porządek wykonuje wtórne sortowanie w innej kolumnie. Lub kiedy chcesz zamówić malejąco. Lub przez coś zupełnie innego. Lubię porządkować według wyrażenia rzamiast kolumny podstawowej, choćby dlatego, że pasuje do tego, co bym zrobił w nie zagnieżdżonym zapytaniu i porządkowania według wyrażenia - używałbym aliasu przypisanego do wyrażenia zamiast powtarzania wyrażenia.
Aaron Bertrand
4
@usr I do rzeczy Paula, będą przypadki, w których można znaleźć luki w funkcjonalności w optymalizatorze. Jeśli nie zostaną naprawione, a znasz lepszy sposób na napisanie zapytania, użyj lepszego. Pacjent: „Doktorze, boli mnie, gdy wykonuję x”. Lekarz: „Nie rób x”. :-)
Aaron Bertrand
-3

Zmodyfikowali optymalizator zapytań, aby dodać tę funkcję. Oznacza to, że wdrożyli mechanizmy specjalnie do obsługi polecenia offset ... fetch. Innymi słowy, dla najpopularniejszego zapytania SQL Server musi wykonać znacznie więcej pracy. Zatem różnica w planach zapytań.

Wymywanie Brandona
źródło