Database
 sql >> Datenbank >  >> RDS >> Database

Leistungsüberraschungen und Annahmen:DATEDIFF

Es ist sehr einfach zu beweisen, dass die folgenden zwei Ausdrücke genau das gleiche Ergebnis liefern:den ersten Tag des aktuellen Monats.

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
       CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));

Und ihre Berechnung dauert ungefähr gleich lang:

SELECT SYSDATETIME();
GO
DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
GO 1000000
GO
SELECT SYSDATETIME();
GO
DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE());
GO 1000000
SELECT SYSDATETIME();

Auf meinem System dauerte es etwa 175 Sekunden, bis beide Stapel abgeschlossen waren.

Also, warum würden Sie eine Methode der anderen vorziehen? Wenn einer von ihnen wirklich mit Kardinalitätsschätzungen herumspielt .

Lassen Sie uns als kurze Einführung diese beiden Werte vergleichen:

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), -- today: 2013-09-01
       DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); -- today: 1786-05-01
--------------------------------------^^^^^^^^^^^^ notice how these are swapped

(Beachten Sie, dass sich die hier dargestellten tatsächlichen Werte ändern, je nachdem, wann Sie diesen Beitrag lesen – „heute“, auf das im Kommentar verwiesen wird, ist der 5. September 2013, der Tag, an dem dieser Beitrag geschrieben wurde. Im Oktober 2013 wird die Ausgabe beispielsweise 2013-10-01 sein und 1786-04-01 .)

Lassen Sie mich Ihnen zeigen, was ich meine …

Ein Nachbau

Lassen Sie uns eine sehr einfache Tabelle mit nur einem geclusterten DATE erstellen Spalte und laden Sie 15.000 Zeilen mit dem Wert 1786-05-01 und 50 Zeilen mit dem Wert 2013-09-01 :

CREATE TABLE dbo.DateTest
(
  CreateDate DATE
);
 
CREATE CLUSTERED INDEX x ON dbo.DateTest(CreateDate);
 
INSERT dbo.DateTest(CreateDate) 
SELECT TOP (15000) DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0)
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
UNION ALL
SELECT TOP (50) DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)
FROM sys.all_objects;

Sehen wir uns dann die tatsächlichen Pläne für diese beiden Abfragen an:

SELECT /* Query 1 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
 
SELECT /* Query 2 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);

Die grafischen Pläne sehen richtig aus:


Grafischer Plan für DATEDIFF(MONTH, 0, GETDATE()) Abfrage


Grafischer Plan für DATEDIFF(MONTH, GETDATE(), 0) Abfrage

Aber die geschätzten Kosten sind aus dem Gleichgewicht – beachten Sie, wie viel höher die geschätzten Kosten für die erste Abfrage sind, die nur 50 Zeilen zurückgibt, im Vergleich zur zweiten Abfrage, die 15.000 Zeilen zurückgibt!


Auszugsraster mit geschätzten Kosten

Und die Registerkarte Top Operations zeigt, dass die erste Abfrage (Suche nach 2013-09-01 ) schätzte, dass es 15.000 Zeilen finden würde, obwohl es in Wirklichkeit nur 50 fand; Die zweite Abfrage zeigt das Gegenteil:Es wurden 50 Zeilen erwartet, die mit 1786-05-01 übereinstimmen , aber 15.000 gefunden. Basierend auf solchen falschen Kardinalitätsschätzungen können Sie sich sicher vorstellen, welche drastischen Auswirkungen dies auf komplexere Abfragen mit viel größeren Datensätzen haben könnte.


Registerkarte "Top-Operationen" für erste Abfrage [DATEDIFF(MONTH, 0, GETDATE())]


Registerkarte "Top-Operationen" für zweite Abfrage [DATEDIFF(MONTH, 0, GETDATE())]

Eine etwas andere Variation der Abfrage, die einen anderen Ausdruck verwendet, um den Anfang des Monats zu berechnen (auf den am Anfang des Posts angespielt wurde), zeigt dieses Symptom nicht:

SELECT /* Query 3 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));

Der Plan ist der obigen Abfrage 1 sehr ähnlich, und wenn Sie nicht genauer hinsehen, würden Sie denken, dass diese Pläne gleichwertig sind:


Grafischer Plan für Nicht-DATEDIFF-Abfragen

Wenn Sie sich hier jedoch die Registerkarte "Top Operations" ansehen, sehen Sie, dass die Schätzung genau richtig ist:


Registerkarte "Top-Vorgänge" mit genauen Schätzungen

Bei dieser bestimmten Datengröße und Abfrage ist die Auswirkung auf die Nettoleistung (insbesondere Dauer und Lesevorgänge) weitgehend irrelevant. Und es ist wichtig zu beachten, dass die Abfragen selbst immer noch korrekte Daten zurückgeben; Es ist nur so, dass die Schätzungen falsch sind (und zu einem schlechteren Plan führen könnten, als ich hier gezeigt habe). Das heißt, wenn Sie Konstanten mit DATEDIFF ableiten innerhalb Ihrer Abfragen auf diese Weise sollten Sie diese Auswirkung wirklich in Ihrer Umgebung testen.

Warum passiert das?

Um es einfach auszudrücken, SQL Server hat ein DATEDIFF Fehler, bei dem das zweite und dritte Argument vertauscht werden, wenn der Ausdruck für die Kardinalitätsschätzung ausgewertet wird. Dies scheint zumindest peripher eine konstante Faltung zu beinhalten; In diesem Books-Online-Artikel finden Sie viele weitere Details zum Thema Constant Folding, aber leider enthält der Artikel keine Informationen zu diesem speziellen Fehler.

Es gibt eine Lösung – oder doch?

Es gibt einen Knowledge Base-Artikel (KB #2481274), der behauptet, das Problem zu lösen, aber er hat ein paar eigene Probleme:

  1. Der KB-Artikel behauptet, dass das Problem in verschiedenen Service Packs oder kumulativen Updates für SQL Server 2005, 2008 und 2008 R2 behoben wurde. Das Symptom ist jedoch immer noch in Zweigen vorhanden, die dort nicht explizit erwähnt werden, obwohl sie seit der Veröffentlichung des Artikels viele zusätzliche CUs gesehen haben. Ich kann dieses Problem immer noch auf SQL Server 2008 SP3 CU Nr. 8 (10.0.5828) und SQL Server 2012 SP1 CU Nr. 5 (11.0.3373) reproduzieren.
  2. Es wird nicht erwähnt, dass Sie, um von der Korrektur zu profitieren, das Trace-Flag 4199 aktivieren müssen (und von all den anderen Möglichkeiten „profitieren“ müssen, auf die ein bestimmtes Trace-Flag den Optimierer beeinflussen kann). Die Tatsache, dass dieses Ablaufverfolgungsflag für den Fix erforderlich ist, wird in einem verwandten Connect-Element, Nr. 630583, erwähnt, aber diese Informationen haben es nicht zurück in den KB-Artikel geschafft. Weder der KB-Artikel noch das Connect-Element geben einen Einblick in die Ursache (dass die Argumente für DATEDIFF wurden während der Evaluierung vertauscht). Positiv ist, dass Sie die obigen Abfragen mit aktiviertem Trace-Flag ausführen (mithilfe von OPTION (QUERYTRACEON 4199)). ) ergibt Pläne, bei denen das Problem der falschen Schätzung nicht auftritt.
  3. Es wird empfohlen, dynamisches SQL zu verwenden, um das Problem zu umgehen. Verwenden Sie in meinen Tests einen anderen Ausdruck (wie den obigen, der DATEDIFF nicht verwendet). ) hat das Problem in modernen Builds von SQL Server 2008 und SQL Server 2012 überwunden. Die Empfehlung von dynamischem SQL hier ist unnötig komplex und wahrscheinlich übertrieben, da ein anderer Ausdruck das Problem lösen könnte. Aber wenn Sie dynamisches SQL verwenden würden, würde ich es auf diese Weise anstelle der im KB-Artikel empfohlenen Weise tun, vor allem, um das Risiko einer SQL-Einschleusung zu minimieren:

    DECLARE 
      @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
      @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.DateTest 
        WHERE CreateDate = @date;';
     
    EXEC sp_executesql @sql, N'@date DATE', @date;

    (Und Sie können OPTION (RECOMPILE) hinzufügen dort, je nachdem, wie SQL Server das Parameter-Sniffing handhaben soll.)

    Dies führt zu demselben Plan wie die frühere Abfrage, die DATEDIFF nicht verwendet , mit korrekten Schätzungen und 99,1 % der Kosten im Clustered-Index suchen.

    Ein anderer Ansatz, der Sie in Versuchung führen könnte (und mit Ihnen meine ich mich, als ich anfing, nachzuforschen), besteht darin, eine Variable zu verwenden, um den Wert im Voraus zu berechnen:

    DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
     
    SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;

    Das Problem bei diesem Ansatz besteht darin, dass Sie mit einer Variablen einen stabilen Plan erhalten, die Kardinalität jedoch auf einer Vermutung basiert (und die Art der Vermutung vom Vorhandensein oder Fehlen von Statistiken abhängt). . In diesem Fall sind hier die geschätzten und tatsächlichen Werte:


    Registerkarte "Top-Operationen" für Abfragen, die eine Variable verwenden

    Das ist eindeutig nicht richtig; Anscheinend hat SQL Server angenommen, dass die Variable mit 50 % der Zeilen in der Tabelle übereinstimmen würde.

SQL-Server 2014

Ich habe ein etwas anderes Problem in SQL Server 2014 gefunden. Die ersten beiden Abfragen wurden behoben (durch Änderungen am Kardinalitätsschätzer oder andere Korrekturen), was bedeutet, dass der DATEDIFF Argumente werden nicht mehr umgeschaltet. Yay!

Allerdings scheint eine Regression in die Problemumgehung zur Verwendung eines anderen Ausdrucks eingeführt worden zu sein – jetzt leidet sie unter einer ungenauen Schätzung (basierend auf der gleichen 50-%-Schätzung wie bei der Verwendung einer Variablen). Dies sind die Abfragen, die ich ausgeführt habe:

SELECT /* 0, GETDATE() (2013) */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
 
SELECT /* GETDATE(), 0 (1786) */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);
 
SELECT /* Non-DATEDIFF */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
 
DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE());
 
SELECT /* Variable */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;
 
DECLARE 
  @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
  @sql NVARCHAR(MAX) = N'SELECT /* Dynamic SQL */ COUNT(*) FROM dbo.DateTest 
    WHERE CreateDate = @date;';
 
EXEC sp_executesql @sql, N'@date DATE', @date;

Hier ist das Anweisungsraster, in dem die geschätzten Kosten und die tatsächlichen Laufzeitmetriken verglichen werden:


Geschätzte Kosten für die 5 Beispielabfragen auf SQL Server 2014

Und dies sind ihre geschätzten und tatsächlichen Zeilenzahlen (mit Photoshop zusammengestellt):


Geschätzte und tatsächliche Zeilenanzahl für die 5 Abfragen auf SQL Server 2014

Aus dieser Ausgabe geht hervor, dass der Ausdruck, der das Problem zuvor gelöst hat, nun einen anderen eingeführt hat. Ich bin mir nicht sicher, ob dies ein Symptom für einen CTP ist (z. B. etwas, das behoben wird) oder ob es sich wirklich um eine Regression handelt.

In diesem Fall hat das Ablaufverfolgungsflag 4199 (allein) keine Auswirkung; Der neue Kardinalitätsschätzer macht Vermutungen und ist einfach nicht korrekt. Ob dies zu einem tatsächlichen Leistungsproblem führt, hängt stark von vielen anderen Faktoren ab, die über den Rahmen dieses Beitrags hinausgehen.

Wenn Sie auf dieses Problem stoßen, können Sie – zumindest in aktuellen CTPs – das alte Verhalten mit OPTION (QUERYTRACEON 9481, QUERYTRACEON 4199) wiederherstellen . Das Ablaufverfolgungsflag 9481 deaktiviert den neuen Kardinalitätsschätzer, wie in diesen Versionshinweisen beschrieben (der sicherlich irgendwann verschwinden oder zumindest verschoben wird). Dadurch werden wiederum die korrekten Schätzungen für das Nicht-DATEDIFF wiederhergestellt Version der Abfrage, löst aber leider immer noch nicht das Problem, bei dem eine Vermutung basierend auf einer Variablen gemacht wird (und die Verwendung von TF9481 allein, ohne TF4199, die ersten beiden Abfragen dazu zwingt, zum alten Argumentaustauschverhalten zurückzukehren).

Schlussfolgerung

Ich gebe zu, das war eine große Überraschung für mich. Kudos an Martin Smith und t-clausen.dk dafür, dass sie durchgehalten und mich davon überzeugt haben, dass dies ein echtes und kein eingebildetes Problem war. Ein großes Dankeschön geht auch an Paul White (@SQL_Kiwi), der mir geholfen hat, bei Verstand zu bleiben, und mich an die Dinge erinnert hat, die ich nicht sagen sollte. :-)

Da ich mir dieses Fehlers nicht bewusst war, war ich fest davon überzeugt, dass der bessere Abfrageplan einfach dadurch generiert wurde, dass der Abfragetext überhaupt geändert wurde, und nicht aufgrund der spezifischen Änderung. Wie sich herausstellt, manchmal eine Änderung an einer Abfrage, die Sie annehmen würden wird keinen Unterschied machen, wird es tatsächlich. Daher empfehle ich, dass Sie, wenn Sie ähnliche Abfragemuster in Ihrer Umgebung haben, diese testen und sicherstellen, dass die Kardinalitätsschätzungen richtig ausfallen. Und notieren Sie sich, sie beim Upgrade erneut zu testen.