Warum verschlechtert das Hinzufügen eines TOP 1 die Leistung dramatisch?

39

Ich habe eine ziemlich einfache Frage

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Das ist eine schreckliche Leistung für mich (als hätte ich nie darauf gewartet, dass es zu Ende geht). Der Abfrageplan sieht folgendermaßen aus:

Bildbeschreibung hier eingeben

Wenn ich jedoch das entferne, TOP 1erhalte ich einen Plan, der so aussieht und in 1-2 Sekunden ausgeführt wird:

Bildbeschreibung hier eingeben

Korrigieren Sie PK & Indizierung unten.

Die Tatsache, dass TOP 1sich der Abfrageplan geändert hat, überrascht mich nicht. Ich bin nur ein bisschen überrascht, dass es so viel schlimmer ist.

Hinweis: Ich habe die Ergebnisse dieses Beitrags gelesen und das Konzept eines Row Goalusw. verstanden. Ich bin gespannt, wie ich die Abfrage so ändern kann, dass sie den besseren Plan verwendet. Momentan speichere ich die Daten in einer temporären Tabelle und ziehe dann die erste Zeile heraus. Ich frage mich, ob es eine bessere Methode gibt.

Bearbeiten Für Leute, die dies nachträglich lesen, gibt es hier ein paar zusätzliche Informationen.

  • Document_Queue - PK / CI ist D_ID und hat ~ 5k Zeilen.
  • Correspondence_Journal - PK / CI ist FILE_NUMBER, CORRESPONDENCE_ID und hat ~ 1,4 Millionen Zeilen.

Als ich anfing, gab es keine anderen Indizes. Ich landete mit einem auf Correspondence_Journal (Document_Id, File_Number)

Kenneth Fisher
quelle
1
Haben Sie eine Fremdschlüsseleinschränkung, die die DOCUMENT_IDBeziehung zwischen den beiden Tabellen erzwingt (oder hat jeder Datensatz in CORRESPONDENCE_JOURNALeinen übereinstimmenden Datensatz in DOCUMENT_QUEUE)?
Daniel Hutmacher

Antworten:

28

Versuchen Sie, einen Hash- Join zu erzwingen *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Der Optimierer dachte wahrscheinlich, dass eine Schleife mit Top 1 besser werden würde und das macht Sinn, aber in Wirklichkeit hat es hier nicht funktioniert. Nur eine Vermutung hier, aber vielleicht waren die geschätzten Kosten für diesen Spool weg - er verwendet TEMPDB - Sie haben möglicherweise eine TEMPDB mit schlechter Leistung.


* Seien Sie vorsichtig mit Verknüpfungshinweisen , da diese die Zugriffsreihenfolge für Plantabellen erzwingen, damit sie mit der schriftlichen Reihenfolge der Tabellen in der Abfrage übereinstimmen (so als ob OPTION (FORCE ORDER)sie angegeben worden wären ). Über den Dokumentationslink:

BOL-Extrakt

Dies kann im Beispiel keine unerwünschten Wirkungen hervorrufen, kann aber im Allgemeinen sehr gut sein. FORCE ORDER(implizit oder explizit) ist ein sehr mächtiger Hinweis, der über die Durchsetzung der Ordnung hinausgeht. Dadurch wird verhindert, dass eine breite Palette von Optimierungsverfahren angewendet wird, einschließlich Teilaggregationen und Neuanordnungen.

Eine OPTION (HASH JOIN) Abfrage Hinweis darauf sein kann , weniger aufdringlich in geeigneten Fällen, da dies bedeutet nicht FORCE ORDER. Dies gilt jedoch für alle Joins in der Abfrage. Andere Lösungen sind verfügbar.

Paparazzo
quelle
1
Sieht aus wie die richtige Antwort und der einzige Unterschied zum einfacheren Plan war eine zusätzliche Sortierung vorne.
Kenneth Fisher
3
Ich bin mir nicht sicher, ob mir diese Antwort gefällt. Join-Hinweise sind sehr invasiv. Einige einfache Indexänderungen sollten zuerst versucht werden, z. B. der Index für die Datumsspalte.
usr
@usr Es ist ein einfacher PK-Join, der in weniger als einer Sekunde ausgeführt wird. Ziemlich sicher hier.
Paparazzo
4
Wenn Sie einen Hash-Join erzwingen, erzwingen Sie einen Scan der großen Tabelle. Es gibt bessere Möglichkeiten.
Rob Farley
30

Da Sie den richtigen Plan mit dem bekommen ORDER BY, könnten Sie vielleicht einfach Ihren eigenen TOPOperator rollen ?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

Meiner Meinung nach sollte der Abfrageplan für das ROW_NUMBER()oben Genannte derselbe sein, als ob Sie einen hatten ORDER BY. Der Abfrageplan sollte jetzt ein Segment, ein Sequenzprojekt und schließlich einen Filteroperator enthalten. Der Rest sollte genauso aussehen wie Ihr guter Plan.

Daniel Hutmacher
quelle
3
Tatsächlich lief es, während es dem Top-Operator (und ein paar anderen Dingen (einem Sequenzprojekt, einem Segment und einer Sortierung) gab, immer noch eine Untersekunde. Ich werde @frisbee allerdings die richtige Antwort geben, da er der erste war und es einfacher ist. Tolle Antwort.
Kenneth Fisher
10
@KennethFisher, die Antwort von Frisbee ist einfacher, aber so, wie ein Vorschlaghammer einen Finish-Nagel einfacher antreibt als ein Standard-Rahmenhammer. Es ist auch mit einem hohen Risiko verbunden, insbesondere wenn es auf lange Sicht an Ort und Stelle bleibt. Ich würde solche Hinweise nur beim Testen oder vielleicht als Randausnahme verwenden.
Steve Mangiameli
@SteveMangiameli In diesem speziellen Fall gibt es nur einen Join, sodass einige Bedenken verschwinden. Ich bin mir der Risiken bewusst, die mit der Verwendung eines Verknüpfungshinweises (oder Abfragehinweises) verbunden sind. Ich denke nur, dass dies in diesem Fall gerechtfertigt ist.
Kenneth Fisher
5
@KennethFisher Imo, das Hauptrisiko des Abfragehinweise dass Ihre Daten oder Änderungen wächst, können Sie die Abfrage - Plan werden erzwingen kann schlimmer als das, was das System gefunden haben , auf eigene wäre. Sie haben bereits gesehen, wie ein kleiner Fehler im Plan die Leistung erheblich beeinträchtigen kann. Mit einem Hinweis in der Produktion erklärt : „Ich weiß dieser Plan wird immer, immer , die beste , weil ich den Planer so vollständig verstehen und wie meine Daten werden die Lebensdauer dieser Abfrage in der Produktion verhalten über.“ Ich war noch nie so zuversichtlich in Bezug auf eine Anfrage.
jpmc26
29

Bearbeiten: +1 funktioniert in dieser Situation, da sich herausstellt, dass FILE_NUMBERes sich um eine mit Nullen aufgefüllte Zeichenfolgenversion einer Ganzzahl handelt. Eine bessere Lösung für Zeichenfolgen ist das Anhängen ''(der leere String), da das Anhängen eines Werts die Reihenfolge beeinflussen kann, oder das Hinzufügen einer Konstante, die jedoch eine nicht deterministische Funktion enthält, wie z sign(rand()+1). Die Idee, die Sorte zu durchbrechen, ist hier immer noch gültig, nur dass meine Methode nicht ideal war.

+1

Nein, ich meine nicht, dass ich mit irgendetwas einverstanden bin, ich meine das als Lösung. Wenn Sie Ihre Abfrage ändern , um ORDER BY cj.FILE_NUMBER + 1dann das TOP 1wird sich anders verhalten.

Wie Sie sehen, versucht das System mit dem kleinen Zeilenziel für eine geordnete Abfrage, die Daten der Reihe nach zu verarbeiten, um einen Sortieroperator zu vermeiden. Es wird auch vermieden, eine Hash-Tabelle zu erstellen, da es wahrscheinlich nicht zu viel Arbeit erfordert, um diese erste Zeile zu finden. In Ihrem Fall ist dies falsch - von der Dicke dieser Pfeile aus sieht es so aus, als müsste eine Menge Daten verbraucht werden, um eine einzelne Übereinstimmung zu finden.

Die Dicke dieser Pfeile deutet darauf hin, dass Ihre DOCUMENT_QUEUE(DQ) -Tabelle viel kleiner als Ihre CORRESPONDENCE_JOURNAL(CJ) -Tabelle ist. Und dass der beste Plan tatsächlich darin besteht, die DQ-Zeilen zu durchsuchen, bis eine CJ-Zeile gefunden wird. Genau das würde der Query Optimizer (QO) tun, wenn er nicht so nervig wäre. Dies ORDER BYwird durch einen CJ-Covering-Index unterstützt.

Wenn Sie die Datei also ORDER BYvollständig löschen, erhalten Sie wahrscheinlich einen Plan, der eine verschachtelte Schleife umfasst, die die Zeilen in DQ durchläuft und nach CJ sucht, um sicherzustellen, dass die Zeile vorhanden ist. Und mit TOP 1würde dies aufhören, nachdem eine einzelne Reihe gezogen wurde.

Wenn Sie jedoch tatsächlich die erste Zeile in der richtigen FILE_NUMBERReihenfolge benötigen , können Sie das System dazu verleiten, den Index zu ignorieren, der (fälschlicherweise) so hilfreich zu sein scheint. ORDER BY CJ.FILE_NUMBER+1Wir wissen, dass er die gleiche Reihenfolge wie zuvor, aber vor allem die QO beibehält nicht. Die Qualitätssicherung wird sich darauf konzentrieren, das Ganze darzulegen, damit ein Top-N-Sortieroperator zufrieden sein kann. Diese Methode sollte einen Plan erstellen, der einen Compute Scalar-Operator zum Ermitteln des Bestellwerts und einen Top N Sort-Operator zum Abrufen der ersten Zeile enthält. Aber auf der rechten Seite sollten Sie eine schöne verschachtelte Schleife sehen, die viele Suchvorgänge auf CJ ausführt. Und eine bessere Leistung als das Durchsuchen einer großen Zeilentabelle, die mit nichts in DQ übereinstimmt.

Das Hash-Match ist nicht unbedingt furchtbar, aber wenn die Menge der Zeilen, die Sie von DQ zurückgeben, viel kleiner als CJ ist (wie ich es erwarten würde), wird das Hash-Match viel mehr CJ scannen als es braucht.

Hinweis: Ich habe +1 anstelle von +0 verwendet, da das Abfrageoptimierungsprogramm wahrscheinlich erkennt, dass +0 nichts ändert. Natürlich könnte das Gleiche für die +1 gelten, wenn nicht jetzt, dann irgendwann in der Zukunft.

Rob Farley
quelle
7

Ich habe die Ergebnisse dieses Beitrags gelesen und das Konzept eines Zeilenziels usw. verstanden. Ich bin gespannt, wie ich die Abfrage so ändern kann, dass sie den besseren Plan verwendet

Durch OPTION (QUERYTRACEON 4138)das Hinzufügen wird die Wirkung von Zeilenzielen nur für diese Abfrage deaktiviert, ohne dass der endgültige Plan zu genau festgelegt wird. Dies ist wahrscheinlich der einfachste / direkteste Weg.

Wenn Sie durch Hinzufügen dieses Hinweises einen Berechtigungsfehler erhalten (erforderlich für DBCC TRACEON), können Sie ihn mithilfe eines Planungshandbuchs anwenden:

Verwendung QUERYTRACEONin Planführern von Spaghettidba

... oder verwenden Sie einfach eine gespeicherte Prozedur:

Welche Berechtigungen sind erforderlich QUERYTRACEON? von Kendra Little

Martin Smith
quelle
3

Neuere Versionen von SQL Server bieten verschiedene (und möglicherweise bessere) Optionen für die Verarbeitung von Abfragen, die eine suboptimale Leistung erzielen, wenn das Optimierungsprogramm Zeilenzieloptimierungen anwenden kann. In SQL Server 2016 SP1 wurde das eingeführt, DISABLE_OPTIMIZER_ROWGOAL USE HINTdas den gleichen Effekt wie das Ablaufverfolgungsflag 4138 hat. Wenn Sie nicht in dieser Version sind, können Sie auch den OPTIMIZE FORAbfragehinweis verwenden, um einen Abfrageplan zu erhalten, der alle Zeilen anstelle von nur 1 zurückgibt gibt die gleichen Ergebnisse wie das in der Frage angegebene zurück, wird jedoch nicht mit dem Ziel erstellt, nur eine Zeile zu erhalten.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
Joe Obbish
quelle
2

Da du a machst TOP(1), empfehle ich, das ORDER BYDeterministische für den Anfang zu machen. Zumindest wird so sichergestellt, dass die Ergebnisse funktionell vorhersehbar sind (immer nützlich für Regressionstests). Es sieht so aus, als müssten Sie hinzufügen DC.D_IDund CJ.CORRESPONDENCE_IDdafür.

Bei der Betrachtung von Abfrageplänen finde ich es manchmal aufschlussreich, die Abfrage zu vereinfachen: Wählen Sie möglicherweise alle relevanten DC-Zeilen im Voraus in einer temporären Tabelle aus, um Probleme mit der Kardinalitätsschätzung für QUEUE_DATEund zu beseitigen PRINT_LOCATION. Dies sollte angesichts der geringen Zeilenanzahl schnell gehen. Sie können dann bei Bedarf Indizes zu dieser temporären Tabelle hinzufügen, ohne die permanente Tabelle zu ändern.

Simon Birch
quelle