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

Leistungsüberraschungen und Annahmen:DATEADD

Im Jahr 2013 schrieb ich über einen Fehler im Optimierer, wo das 2. und 3. Argument zu DATEDIFF() können ausgetauscht werden – was zu falschen Schätzungen der Zeilenanzahl und damit zu einer schlechten Auswahl des Ausführungsplans führen kann:

  • Leistungsüberraschungen und Annahmen:DATEDIFF

Am vergangenen Wochenende erfuhr ich von einer ähnlichen Situation und ging sofort davon aus, dass es sich um dasselbe Problem handelte. Immerhin schienen die Symptome nahezu identisch zu sein:

  1. Es gab eine Datums-/Zeitfunktion im WHERE Klausel.
    • Diesmal war es DATEADD() statt DATEDIFF() .
  2. Es gab eine offensichtlich falsche Schätzung der Zeilenzahl von 1 im Vergleich zu einer tatsächlichen Zeilenzahl von über 3 Millionen.
    • Das war eigentlich eine Schätzung von 0, aber SQL Server rundet solche Schätzungen immer auf 1 auf.
  3. Aufgrund der niedrigen Schätzung wurde eine schlechte Planauswahl getroffen (in diesem Fall wurde ein Loop Join gewählt).

Das fehlerhafte Muster sah folgendermaßen aus:

WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());

Der Benutzer hat mehrere Varianten ausprobiert, aber nichts hat sich geändert; Sie haben es schließlich geschafft, das Problem zu umgehen, indem sie das Prädikat geändert haben in:

WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;

Dies hat eine bessere Schätzung (die typische 30% Ungleichheitsschätzung); also nicht ganz richtig. Und obwohl es den Schleifenjoin eliminiert hat, gibt es zwei Hauptprobleme mit diesem Prädikat:

  1. Ist es nicht die gleiche Abfrage, da jetzt nach 365-Tage-Grenzen gesucht wird, die überschritten wurden, im Gegensatz dazu, dass sie größer als ein bestimmter Zeitpunkt vor 365 Tagen sind. Statistisch signifikant? Vielleicht nicht. Aber immer noch, technisch gesehen, nicht dasselbe.
  2. Das Anwenden der Funktion auf die Spalte macht den gesamten Ausdruck nicht-sargbar – was zu einem vollständigen Scan führt. Wenn die Tabelle nur etwas mehr als ein Jahr an Daten enthält, ist dies keine große Sache, aber wenn die Tabelle größer oder das Prädikat schmaler wird, wird dies zu einem Problem.

Wieder bin ich zu dem Schluss gekommen, dass DATEADD() Die Operation war das Problem und empfahl einen Ansatz, der sich nicht auf DATEADD() stützte – Erstellen einer datetime von allen Teilen der aktuellen Zeit, wodurch ich ein Jahr subtrahieren kann, ohne DATEADD() zu verwenden :

WHERE [column] >= DATETIMEFROMPARTS(
      DATEPART(YEAR,   SYSUTCDATETIME())-1, 
      DATEPART(MONTH,  SYSUTCDATETIME()),
      DATEPART(DAY,    SYSUTCDATETIME()),
      DATEPART(HOUR,   SYSUTCDATETIME()), 
      DATEPART(MINUTE, SYSUTCDATETIME()),
      DATEPART(SECOND, SYSUTCDATETIME()), 0);

Dies war nicht nur sperrig, sondern hatte auch einige Probleme, nämlich dass ein Haufen Logik hinzugefügt werden musste, um Schaltjahre richtig zu berücksichtigen. Erstens, damit es nicht scheitert, wenn es zufällig am 29. Februar läuft, und zweitens, um immer genau 365 Tage zu umfassen (statt 366 im Jahr nach einem Schalttag). Einfache Korrekturen natürlich, aber sie machen die Logik viel hässlicher – insbesondere, weil die Abfrage innerhalb einer Ansicht existieren musste, wo Zwischenvariablen und mehrere Schritte nicht möglich sind.

In der Zwischenzeit reichte das OP ein Connect-Element ein, bestürzt über die 1-Zeilen-Schätzung:

  • Connect #2567628 :Einschränkung mit DateAdd() liefert keine guten Schätzungen

Dann kam Paul White (@SQL_Kiwi) und brachte, wie so oft zuvor, zusätzliches Licht in das Problem. Er hat einen ähnlichen Connect-Artikel geteilt, der 2011 von Erland Sommarskog eingereicht wurde:

  • Connect #685903 :Falsche Schätzung, wenn sysdatetime in einem dateadd()-Ausdruck erscheint

Das Problem besteht im Wesentlichen darin, dass nicht einfach bei SYSDATETIME() eine schlechte Schätzung vorgenommen werden kann (oder SYSUTCDATETIME() ) erscheint, wie Erland ursprünglich berichtete, aber wenn irgendein datetime2 Ausdruck am Prädikat beteiligt ist (und vielleicht nur dann, wenn DATEADD() wird auch verwendet). Und es kann in beide Richtungen gehen – wenn wir >= vertauschen für <= , wird die Schätzung zur gesamten Tabelle, also scheint der Optimierer auf SYSDATETIME() zu schauen value als Konstante und ignoriert vollständig alle Operationen wie DATEADD() die dagegen durchgeführt werden.

Paul teilte mit, dass die Problemumgehung einfach darin besteht, ein datetime zu verwenden Äquivalent bei der Berechnung des Datums, bevor es in den richtigen Datentyp konvertiert wird. In diesem Fall können wir SYSUTCDATETIME() austauschen und ändern Sie es in GETUTCDATE() :

WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));

Ja, das führt zu einem kleinen Präzisionsverlust, aber das könnte auch ein Staubpartikel sein, das Ihren Finger auf dem Weg zum Drücken von F5 verlangsamt Schlüssel. Wichtig ist, dass ein seek immer noch and verwendet werden kann die Schätzungen waren richtig – fast perfekt sogar:

Die Lesevorgänge sind ähnlich, da die Tabelle fast ausschließlich Daten aus dem vergangenen Jahr enthält, sodass selbst eine Suche zu einem Bereichsscan des größten Teils der Tabelle wird. Die Anzahl der Zeilen ist nicht identisch, weil (a) die zweite Abfrage um Mitternacht endet und (b) die dritte Abfrage aufgrund des Schalttages früher in diesem Jahr einen zusätzlichen Tag mit Daten enthält. Auf jeden Fall demonstriert dies immer noch, wie wir näher an richtige Schätzungen herankommen können, indem wir DATEADD() eliminieren , aber die richtige Lösung besteht darin, die direkte Kombination zu entfernen von DATEADD() und datetime2 .

Um weiter zu veranschaulichen, wie die Schätzungen falsch liegen, können Sie sehen, dass, wenn wir unterschiedliche Argumente und Anweisungen an die ursprüngliche Abfrage und Pauls Neufassung übergeben, die Anzahl der geschätzten Zeilen für die erstere immer auf der aktuellen Zeit basiert – sie don ändern sich nicht mit der Anzahl der verstrichenen Tage (während die von Paul jedes Mal relativ genau ist):

Die tatsächlichen Zeilen für die erste Abfrage sind etwas niedriger, da diese nach einem langen Nickerchen ausgeführt wurde

Die Schätzungen werden nicht immer so gut sein; meine Tabelle hat nur eine relativ stabile Verteilung. Ich habe es mit der folgenden Abfrage gefüllt und dann die Statistiken mit fullscan aktualisiert, falls Sie dies selbst ausprobieren möchten:

-- OP's table definition:
CREATE TABLE dbo.DateaddRepro 
(
  SessionId  int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
  CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
 
CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc]
ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId);
GO
 
INSERT dbo.DateaddRepro(CreatedUtc)
SELECT dt FROM 
(
  SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER()
    OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE())
  FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2
) AS x;
 
UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN;
 
SELECT DISTINCT SessionId FROM dbo.DateaddRepro 
WHERE /* pick your WHERE clause to test */;

Ich habe das neue Connect-Element kommentiert und werde wahrscheinlich zurückgehen und meine Stack Exchange-Antwort verbessern.

Die Moral der Geschichte

Vermeiden Sie die Kombination von DATEADD() mit Ausdrücken, die datetime2 ergeben , insbesondere auf älteren Versionen von SQL Server (dies war auf SQL Server 2012). Es kann auch ein Problem sein, selbst auf SQL Server 2016, wenn das ältere Kardinalitätsschätzungsmodell verwendet wird (aufgrund eines niedrigeren Kompatibilitätsgrads oder einer expliziten Verwendung des Ablaufverfolgungsflags 9481). Probleme wie dieses sind subtil und nicht immer sofort offensichtlich, also dient dies hoffentlich als Erinnerung (vielleicht sogar für mich, wenn ich das nächste Mal auf ein ähnliches Szenario stoße). Wie ich im letzten Beitrag vorgeschlagen habe, überprüfen Sie bei Abfragemustern wie diesem, ob Sie korrekte Schätzungen erhalten, und machen Sie sich irgendwo eine Notiz, um sie erneut zu überprüfen, wenn sich größere Änderungen im System (z. B. ein Upgrade oder ein Service Pack) ergeben.