Parameter Sniffing vs VARIABLES vs Recompile vs OPTIMIZE FOR UNKNOWN

40

Daher hatten wir heute Morgen einen langen laufenden Prozess, der Probleme verursachte (30 Sek. + Laufzeit). Wir haben uns entschlossen zu prüfen, ob das Parameter-Sniffing die Schuld trägt. Also haben wir den proc umgeschrieben und die eingehenden Parameter auf Variablen gesetzt, um das Parameter-Sniffing zu umgehen. Ein bewährter Ansatz. Bam, Abfragezeit verbessert (weniger als 1 Sekunde). Beim Betrachten des Abfrageplans wurden die Verbesserungen in einem Index gefunden, den das Original nicht verwendete.

Um zu überprüfen, dass wir kein falsches Positiv erhalten haben, haben wir einen Dbcc-Freeproccache für den ursprünglichen Proc durchgeführt und erneut ausgeführt, um zu prüfen, ob die verbesserten Ergebnisse dieselben wären. Aber zu unserer Überraschung lief der ursprüngliche Prozess immer noch langsam. Wir haben es erneut mit WITH RECOMPILE versucht, immer noch langsam (wir haben versucht, es beim Aufruf des Proc und innerhalb des Proc selbst neu zu kompilieren). Wir haben sogar den Server neu gestartet (Dev Box offensichtlich).

Meine Frage ist also: Wie kann das Parameter-Sniffing schuld sein, wenn wir die gleiche langsame Abfrage für einen leeren Plan-Cache erhalten? Es sollten keine Parameter zum Sniffing vorhanden sein?

Sind wir stattdessen von Tabellenstatistiken betroffen, die nicht mit dem Plan-Cache zusammenhängen? Und wenn ja, warum würde es helfen, die eingehenden Parameter auf Variablen zu setzen?

In weiteren Tests haben wir auch festgestellt, dass das Einfügen von OPTION (OPTIMIZE FOR UNKNOWN) in die Interna des Prozesses DID den erwarteten verbesserten Plan ergibt .

Also, einige von Ihnen sind schlauer als ich, können Sie uns einige Hinweise geben, was hinter den Kulissen vor sich geht, um diese Art von Ergebnissen zu erzielen?

Andererseits wird der langsame Plan auch mit einem Grund vorzeitig abgebrochen, GoodEnoughPlanFoundwährend der schnelle Plan im aktuellen Plan keinen Grund für einen vorzeitigen Abbruch enthält.

in Summe

  • Variablen aus eingehenden Parametern erstellen (1 Sek.)
  • mit Neukompilierung (30+ Sek.)
  • dbcc freeproccache (30+ sec)
  • OPTION (FÜR UKNOWN OPTIMIEREN) (1 Sek.)

AKTUALISIEREN:

Den Zeitlupenplan finden Sie hier: https://www.dropbox.com/s/cmx2lrsea8q8mr6/plan_slow.xml

Den Plan für die schnelle Ausführung finden Sie hier: https://www.dropbox.com/s/b28x6a01w7dxsed/plan_fast.xml

Anmerkung: Tabelle, Schema und Objektnamen wurden aus Sicherheitsgründen geändert.

RThomas
quelle

Antworten:

43

Die Abfrage lautet

SELECT SUM(Amount) AS SummaryTotal
FROM   PDetail WITH(NOLOCK)
WHERE  ClientID = @merchid
       AND PostedDate BETWEEN @datebegin AND @dateend 

Die Tabelle enthält 103.129.000 Zeilen.

Der schnelle Plan wird von ClientId mit einem verbleibenden Prädikat für das Datum nachgeschlagen, es müssen jedoch 96 Suchvorgänge ausgeführt werden, um den abzurufen Amount. Der <ParameterList>Abschnitt im Plan ist wie folgt.

        <ParameterList>
          <ColumnReference Column="@dateend" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@datebegin" 
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@merchid" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

Der langsame Plan sucht nach Datum und verfügt über Suchvorgänge, um das verbleibende Vergleichselement für ClientId auszuwerten und den Betrag abzurufen (Estimated 1 vs Actual 7.388.383). Der <ParameterList>Abschnitt ist

        <ParameterList>
          <ColumnReference Column="@EndDate" 
                           ParameterCompiledValue="'2013-02-01 23:59:00.000'" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@BeginDate" 
                           ParameterCompiledValue="'2013-01-01 00:00:00.000'"               
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@ClientID" 
                           ParameterCompiledValue="(78155)" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

In diesem zweiten Fall ParameterCompiledValueist der nicht leer. SQL Server hat die in der Abfrage verwendeten Werte erfolgreich abgerufen.

Das Buch "Praktische Fehlerbehebung in SQL Server 2005" enthält Informationen zur Verwendung lokaler Variablen

Die Verwendung lokaler Variablen, um das Parameter-Sniffing zu unterbinden, ist ein ziemlich häufiger Trick, aber die OPTION (RECOMPILE)und OPTION (OPTIMIZE FOR)-Hinweise ... sind im Allgemeinen eleganter und etwas weniger riskant


Hinweis

In SQL Server 2005 ermöglicht die Kompilierung auf Anweisungsebene die Kompilierung einer einzelnen Anweisung in einer gespeicherten Prozedur, die bis kurz vor der ersten Ausführung der Abfrage zurückgestellt wird. Bis dahin wäre der Wert der lokalen Variablen bekannt. Theoretisch könnte SQL Server dies nutzen, um lokale Variablenwerte auf die gleiche Weise zu erfassen, wie Parameter erfasst werden. Da jedoch in SQL Server 7.0 und SQL Server 2000+ häufig lokale Variablen verwendet wurden, um das Parameter-Sniffing zu umgehen, wurde das Sniffing lokaler Variablen in SQL Server 2005 nicht aktiviert. Möglicherweise wird es in einer zukünftigen SQL Server-Version aktiviert, was jedoch von Vorteil ist Grund, eine der anderen in diesem Kapitel beschriebenen Optionen zu verwenden, wenn Sie die Wahl haben.


Nach einem kurzen Test ist das oben beschriebene Verhalten in 2008 und 2012 immer noch dasselbe und Variablen werden nicht für ein verzögertes Kompilieren abgehört, sondern nur, wenn ein expliziter OPTION RECOMPILEHinweis verwendet wird.

DECLARE @N INT = 0

CREATE TABLE #T ( I INT );

/*Reference to #T means this statement is subject to deferred compile*/
SELECT *
FROM   master..spt_values
WHERE  number = @N
       AND EXISTS(SELECT COUNT(*) FROM #T)

SELECT *
FROM   master..spt_values
WHERE  number = @N
OPTION (RECOMPILE)

DROP TABLE #T 

Trotz verzögerter Kompilierung wird die Variable nicht abgehört und die geschätzte Zeilenanzahl ist ungenau

Schätzungen vs.

Ich gehe also davon aus, dass sich der langsame Plan auf eine parametrisierte Version der Abfrage bezieht.

Das ParameterCompiledValueist gleich ParameterRuntimeValuefür alle Parameter , so ist dies nicht typisch Parameter Schnüffeln (wo der Plan für einen Satz von Werten zusammengestellt wurde dann für einen weiteren Satz von Werten ausgeführt werden ).

Das Problem ist, dass der Plan, der für die richtigen Parameterwerte kompiliert wurde, nicht geeignet ist.

Wahrscheinlich haben Sie das Problem mit aufsteigenden Daten, die hier und hier beschrieben werden . Für eine Tabelle mit 100 Millionen Zeilen müssen Sie 20 Millionen Zeilen einfügen (oder anderweitig ändern), bevor SQL Server die Statistiken automatisch für Sie aktualisiert. Es scheint, dass bei der letzten Aktualisierung keine Zeilen mit dem Datumsbereich in der Abfrage übereinstimmen, jetzt jedoch 7 Millionen.

Sie können häufiger Statistikaktualisierungen einplanen, Ablaufverfolgungsflags in Betracht ziehen 2389 - 90oder verwenden, OPTIMIZE FOR UKNOWNum nur auf Vermutungen zurückzugreifen, anstatt die derzeit irreführenden Statistiken für die datetimeSpalte verwenden zu können.

Dies ist in der nächsten Version von SQL Server (nach 2012) möglicherweise nicht erforderlich. Ein verwandtes Connect-Objekt enthält die faszinierende Antwort

Gepostet von Microsoft am 28.08.2012 um 13:35 Uhr
Wir haben eine Verbesserung der Kardinalitätsschätzung für die nächste Hauptversion vorgenommen, die dies im Wesentlichen behebt. Wenn unsere Vorschaubilder erscheinen, bleiben Sie auf dem Laufenden. Eric

Diese Verbesserung von 2014 wird von Benjamin Nevarez gegen Ende des Artikels betrachtet:

Ein erster Blick auf den New SQL Server Cardinality Estimator .

Es scheint, dass der neue Kardinalitätsschätzer zurückgreift und in diesem Fall die durchschnittliche Dichte verwendet, anstatt die 1-Zeilen-Schätzung anzugeben.

Einige zusätzliche Details zum Kardinalitätsschätzer 2014 und dem aufsteigenden Schlüsselproblem hier:

Neue Funktionen in SQL Server 2014 - Teil 2 - Neue Kardinalitätsschätzung

Martin Smith
quelle
29

Meine Frage ist also: Wie kann das Parameter-Sniffing schuld sein, wenn wir die gleiche langsame Abfrage für einen leeren Plan-Cache erhalten? Es sollten keine Parameter zum Sniffing vorhanden sein.

Wenn SQL - Server eine Abfrage enthält Parameterwerte erstellt, es sniffs die spezifischen Werte dieser Parameter für die Kardinalität (Zeilenzahl) Schätzung. In Ihrem Fall die besonderen Werte @BeginDate, @EndDateund @ClientIDverwendet werden , wenn ein Ausführungsplan wählen. Weitere Details zum Parameter-Sniffing finden Sie hier und hier . Ich stelle diese Hintergrundlinks zur Verfügung, da ich aufgrund der oben gestellten Frage der Meinung bin, dass das Konzept derzeit nicht richtig verstanden wird. Es gibt immer Parameterwerte, die beim Erstellen eines Plans überprüft werden müssen.

Wie auch immer, das ist alles nebensächlich, denn Parameter-Sniffing ist hier nicht das Problem, wie Martin Smith betont hat. Zum Zeitpunkt der Kompilierung der langsamen Abfrage ergab die Statistik, dass für die erfassten Werte von @BeginDateund keine Zeilen vorhanden waren @EndDate:

Langsamer Plan schnüffelte nach Werten

Die gemerkten Werte sind sehr neu und lassen auf das aufsteigende Schlüsselproblem schließen, das Martin erwähnt. Da die Indexsuche an den Daten schätzungsweise nur eine einzelne Zeile zurückgibt, wählt das Optimierungsprogramm einen Plan aus, der das Prädikat ClientIDals Residuum an den Key Lookup-Operator weiterleitet.

Die einzeilige Schätzung ist auch der Grund, warum das Optimierungsprogramm keine besseren Pläne mehr sucht und die Meldung "Good Enough Plan Found" zurückgibt. Die geschätzten Gesamtkosten des langsamen Plans mit der Einzelzeilenschätzung betragen nur 0,013136 Kosteneinheiten. Es macht also keinen Sinn, nach etwas Besserem zu suchen. Mit der Ausnahme, dass die Suche tatsächlich 7.388.383 Zeilen und nicht nur eine zurückgibt, wodurch die gleiche Anzahl von Schlüsselsuchen verursacht wird.

Es kann schwierig sein, Statistiken auf dem neuesten Stand zu halten und bei großen Tabellen nützlich zu sein, und die Partitionierung stellt diesbezüglich eigene Herausforderungen . Ich hatte selbst keinen besonderen Erfolg mit den Trace-Flags 2389 und 2390, aber Sie können sie gerne testen. In neueren Versionen von SQL Server (R2 SP1 und höher) sind dynamische Statistikaktualisierungen verfügbar. Diese partitionsspezifischen Statistikaktualisierungen sind jedoch immer noch nicht implementiert. In der Zwischenzeit möchten Sie möglicherweise eine manuelle Statistikaktualisierung planen, wenn Sie wesentliche Änderungen an dieser Tabelle vornehmen.

Für diese spezielle Abfrage würde ich überlegen, den vom Optimierer vorgeschlagenen Index während der Kompilierung des schnellen Abfrageplans zu implementieren:

/*
The Query Processor estimates that implementing the following index could improve
the query cost by 98.8091%.

WARNING: This is only an estimate, and the Query Processor is making this 
recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide impact,
including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index>]
ON [dbo].[PDetail] ([ClientID],[PostedDate])
INCLUDE ([Amount]);

Der Index sollte partitioniert sein, mit einer ON PartitionSchemeName (PostedDate)Klausel, aber der Punkt ist, dass die Bereitstellung eines offensichtlich besten Datenzugriffspfads dem Optimierer hilft, schlechte Planentscheidungen zu vermeiden, ohne auf OPTIMIZE FOR UNKNOWNHinweise oder altmodische Problemumgehungen wie die Verwendung lokaler Variablen zurückzugreifen.

Mit dem verbesserten Index wird die Schlüsselsuche zum Abrufen der AmountSpalte beseitigt, der Abfrageprozessor kann weiterhin die dynamische Partitionsbeseitigung durchführen und den bestimmten Bereich ClientIDund den Datumsbereich mithilfe einer Suche ermitteln .

Paul White
quelle
Ich wünschte, ich könnte zwei Antworten als richtig markieren, aber nochmals danke für die zusätzlichen Informationen - sehr lehrreich.
RThomas
1
Es ist schon ein paar Jahre her, seit ich das gepostet habe ... aber ich wollte dich nur wissen lassen. Ich benutze immer noch den Begriff "unvollkommen verstanden" und denke dabei immer an Paul White. Bringt mich jedes Mal zum Lachen.
RThomas
0

Ich hatte genau das gleiche Problem, bei dem eine gespeicherte Prozedur langsam wurde OPTIMIZE FOR UNKNOWNund RECOMPILEAbfragetipps die Langsamkeit lösten und die Ausführungszeit beschleunigten. Die folgenden beiden Methoden hatten jedoch keinen Einfluss auf die Langsamkeit der gespeicherten Prozedur: (i) Löschen des Caches (ii) Verwenden von WITH RECOMPILE. Also, wie Sie sagten, war es nicht wirklich Parameter schnüffeln.

Die Trace-Flags 2389 und 2390 haben auch nicht geholfen. Nur die Aktualisierung der Statistiken ( EXEC sp_updatestats) hat es für mich getan.

aali
quelle