Warum gibt es Ausführungsplanunterschiede zwischen OFFSET… FETCH und dem alten ROW_NUMBER-Schema?


15

Das OFFSET ... FETCHmit SQL Server 2012 eingeführte neue Modell bietet einfaches und schnelleres Paging. Warum gibt es überhaupt Unterschiede, wenn man bedenkt, dass die beiden Formen semantisch identisch und sehr häufig sind?

Man würde annehmen, dass der Optimierer beide erkennt und sie (trivial) vollständig optimiert.

Hier ist ein sehr einfacher Fall, in OFFSET ... FETCHdem der Kostenvoranschlag ~ 2x schneller ist.

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

Sie können diesen Testfall variieren, indem Sie ein CI erstellen object_idoder Filter hinzufügen. Es ist jedoch unmöglich, alle Planunterschiede zu entfernen. OFFSET ... FETCHist immer schneller, weil es zur Ausführungszeit weniger Arbeit macht.


Nicht sehr sicher, also als Kommentar, aber ich denke, es liegt daran, dass Sie die gleiche Reihenfolge nach Bedingung für die Zeilennummerierung und die endgültige Ergebnismenge haben. Da dies im 2. Zustand dem Optimierer bekannt ist, müssen die Ergebnisse nicht erneut sortiert werden. Im ersten Fall muss jedoch sichergestellt werden, dass die Ergebnisse der äußeren Auswahl sowie die Zeilennummerierung im inneren Ergebnis sortiert sind. Das Erstellen eines richtigen Index für #Objekte sollte das Problem lösen
Akash

Antworten:


13

Die Beispiele in der Frage führen nicht zu den gleichen Ergebnissen (das OFFSETBeispiel weist einen Fehler von eins auf). Die aktualisierten Formulare unten beheben dieses Problem, entfernen die zusätzliche Sortierung für den ROW_NUMBERFall und verwenden Variablen, um die Lösung allgemeiner zu gestalten:

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;

Der ROW_NUMBERPlan hat geschätzte Kosten von 0.0197935 :

Zeilennummernplan

Der OFFSETPlan hat geschätzte Kosten von 0.0196955 :

Versatzplan

Dies entspricht einer Einsparung von 0,000098 geschätzten Kosteneinheiten (obwohl der OFFSETPlan zusätzliche Operatoren erfordern würde, wenn Sie eine Zeilennummer für jede Zeile zurückgeben möchten). Der OFFSETPlan wird im Allgemeinen immer noch etwas billiger sein, aber denken Sie daran, dass die geschätzten Kosten genau so hoch sind - echte Tests sind immer noch erforderlich. Der Großteil der Kosten in beiden Plänen sind die Kosten für die gesamte Art der Eingabemenge, sodass hilfreiche Indizes beiden Lösungen zugute kommen würden.

Wenn konstante OFFSET 30Literalwerte verwendet werden (z. B. im ursprünglichen Beispiel), kann der Optimierer eine TopN-Sortierung anstelle einer vollständigen Sortierung gefolgt von einer Top-Sortierung verwenden. Wenn die für die TopN-Sortierung benötigten Zeilen ein konstantes Literal sind und <= 100 (die Summe von OFFSETund FETCH), kann die Ausführungsengine einen anderen Sortieralgorithmus verwenden, der eine schnellere Leistung als die verallgemeinerte TopN-Sortierung liefert. Alle drei Fälle weisen insgesamt unterschiedliche Leistungsmerkmale auf.

Wie, warum sich der Optimierer nicht automatisch die Transformation ROW_NUMBERSyntax Muster zu verwenden OFFSET, gibt es eine Reihe von Gründen:

  1. Es ist fast unmöglich, eine Transformation zu schreiben, die allen vorhandenen Verwendungen entspricht
  2. Es kann verwirrend sein, wenn einige Paging-Abfragen automatisch umgewandelt werden und andere nicht
  3. Es OFFSETwird nicht garantiert, dass der Plan in allen Fällen besser ist

Ein Beispiel für den dritten Punkt oben ist, dass der Paging-Satz ziemlich breit ist. Es kann wesentlich effizienter sein , die benötigten Schlüssel mit einem nicht gruppierten Index zu suchen und manuell nach dem gruppierten Index zu suchen , als den Index mit OFFSEToder zu durchsuchen ROW_NUMBER. Es sind weitere Probleme zu berücksichtigen, wenn die Paging-Anwendung wissen muss, wie viele Zeilen oder Seiten insgesamt vorhanden sind. Es ist auch eine gute Diskussion über die Vorzüge des ‚Schlüssel suchen‘ und ‚Offset‘ Methoden hier .

Insgesamt ist es wahrscheinlich besser OFFSET, wenn die Benutzer eine fundierte Entscheidung treffen, ihre Paging-Abfragen gegebenenfalls nach gründlichen Tests zu ändern .


1
Der Grund dafür, dass die Transformation in den meisten Fällen nicht durchgeführt wurde, ist wahrscheinlich, dass es zu schwierig war, einen akzeptablen technischen Kompromiss zu finden. Sie haben gute Gründe angegeben, warum dies der Fall sein könnte .; Ich muss sagen, dass dies eine gute Antwort ist. Viele Einsichten und neue Gedanken. Ich lasse die Frage ein wenig offen und wähle dann die beste Antwort.
USR

5

Mit ein wenig Fummeln an Ihrer Anfrage bekomme ich die gleichen Kosten Kostenschätzung (50/50) und gleich IO - Statistik:

; 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

Dies vermeidet die zusätzliche Art , das erscheint in Ihrer Version , indem Sie auf das Sortieren rstatt object_id.


Vielen Dank für diesen Einblick. Nun, da ich darüber nachdenke, habe ich gesehen, dass der Optimierer die sortierte Natur der ROW_NUMBER-Ausgabe vorher nicht verstanden hat. Die Menge wird von object_id als ungeordnet betrachtet. Oder zumindest nicht nach r und object_id sortiert.
USR

2
@usr die ORDER BY, die ROW_NUMBER () verwendet, definiert, wie die Nummern zugewiesen werden. Es macht nichts, um die Ausgabereihenfolge zu versprechen - das ist separat. Es passiert einfach so, dass es oft zusammenfällt, aber es ist nicht garantiert.
Aaron Bertrand

@ AaronBertrand Ich verstehe, dass ROW_NUMBER die Ausgabe nicht anordnet. Aber wenn ROW_NUMBER mit den gleichen Spalten angeordnet wird als der Ausgang ist, dann ist die gleiche Reihenfolge ist garantiert, nicht wahr? Das Abfrageoptimierungsprogramm könnte diese Tatsache nutzen. Daher sind in dieser Abfrage zwei Sortiervorgänge immer nicht erforderlich .
usr

1
@usr Sie haben einen allgemeinen Anwendungsfall gefunden, den das Optimierungsprogramm nicht berücksichtigt, aber es ist nicht der einzige Anwendungsfall. Betrachten Sie Fälle, in denen die Reihenfolge innerhalb von ROW_NUMBER () diese Spalte und etwas anderes ist. Oder wenn die äußere Reihenfolge eine sekundäre Sortierung für eine andere Spalte durchführt. Oder wenn Sie absteigend bestellen möchten. Oder von etwas anderem. Ich mag es, nach dem Ausdruck ranstatt nach der Basisspalte zu sortieren, wenn auch nur, weil es mit dem übereinstimmt, was ich in einer nicht verschachtelten Abfrage und nach einem Ausdruck tun würde. Ich würde den dem Ausdruck zugewiesenen Alias ​​verwenden, anstatt den Ausdruck zu wiederholen.
Aaron Bertrand

4
@usr Und bis auf den Punkt von Paul wird es Fälle geben, in denen Sie Lücken in der Funktionalität des Optimierers finden. Wenn sie nicht behoben werden und Sie eine bessere Methode zum Schreiben der Abfrage kennen, verwenden Sie die bessere Methode. Patient: "Doktor, es tut weh, wenn ich x mache." Doktor: "Tu nicht x." :-)
Aaron Bertrand

-3

Sie haben das Abfrageoptimierungsprogramm geändert, um diese Funktion hinzuzufügen. Das heißt, sie haben Mechanismen implementiert, die speziell den Befehl offset ... fetch unterstützen. Mit anderen Worten, für die Top-Abfrage muss SQL Server viel mehr Arbeit leisten. Also der Unterschied in den Abfrageplänen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.