So verbessern Sie die Schätzung von 1 Zeile in einer Ansicht, die durch DateAdd () für einen Index eingeschränkt ist

8

Verwenden von Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Gegeben eine Tabelle und einen Index:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

Die tatsächlichen Zeilen für jede der folgenden Abfragen betragen 3,1 Millionen. Die geschätzten Zeilen werden als Kommentare angezeigt.

Wenn diese Abfragen eine andere Abfrage in einer Ansicht füttern , wählt der Optimierer aufgrund der Schätzungen für 1 Zeile einen Schleifenverknüpfungspunkt. Wie kann die Schätzung auf dieser Ebene verbessert werden, um zu vermeiden, dass der Join-Hinweis für übergeordnete Abfragen überschrieben wird oder auf einen SP zurückgegriffen wird?

Die Verwendung eines fest codierten Datums funktioniert hervorragend:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

Diese äquivalenten Abfragen sind mit der Ansicht kompatibel, aber alle schätzen 1 Zeile:

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

Versuchen Sie einige Hinweise (aber N / A zum Anzeigen):

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

Versuchen Sie es mit Parameter / Hints (aber N / A zum Anzeigen):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

Schätzung vs Ist

Die Statistiken sind aktuell.

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

Die letzten Zeilen des Histogramms (insgesamt 189 Zeilen) werden angezeigt:

Geben Sie hier die Bildbeschreibung ein

crokusek
quelle

Antworten:

6

Eine weniger umfassende Antwort als die von Aaron, aber DATEADDdas Hauptproblem ist ein Fehler bei der Kardinalitätsschätzung, wenn der Typ datetime2 verwendet wird:

Verbinden: Falsche Schätzung, wenn sysdatetime in einem dateadd () - Ausdruck angezeigt wird

Eine Problemumgehung ist die Verwendung GETUTCDATE(die datetime zurückgibt):

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

Beachten Sie, dass die Konvertierung in datetime2 außerhalb von erfolgen muss DATEADD, um den Fehler zu vermeiden.

Das Problem der 1-zeiligen Kardinalitätsschätzung wird für mich in allen Versionen von SQL Server bis einschließlich 2016 RC0 reproduziert, in denen der Kardinalitätsschätzer mit 70 Modellen verwendet wird.

Aaron Bertrand hat einen Artikel darüber für SQLPerformance.com geschrieben:

Paul White 9
quelle
6

In einigen Szenarien kann SQL Server wirklich wilde Schätzungen für DATEADD/ haben DATEDIFF, abhängig von den Argumenten und dem Aussehen Ihrer tatsächlichen Daten. Ich habe darüber geschrieben, DATEDIFFwenn ich mich mit Anfang des Monats und einigen Problemumgehungen befasse:

Mein typischer Rat ist jedoch, die Verwendung von DATEADD/ DATEDIFFin where / join-Klauseln einfach einzustellen.

Der folgende Ansatz ist zwar nicht sehr genau, wenn ein Schaltjahr im gefilterten Bereich liegt (in diesem Fall wird er einen zusätzlichen Tag enthalten), und wenn er auf den Tag gerundet wird, werden die Schätzungen besser (aber immer noch nicht großartig!) Sie sind nicht DATEDIFFgegen den Spaltenansatz und können dennoch eine Suche verwenden:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

Sie können die Eingaben manipulieren DATEFROMPARTS, um Probleme am Schalttag zu vermeiden DATETIMEFROMPARTS, um mehr Präzision zu erzielen, anstatt auf den Tag zu runden usw. Dies soll nur zeigen, dass Sie eine Variable mit einem Datum in der Vergangenheit füllen können, ohne sie zu verwenden DATEADD(es ist nur ein wenig mehr Arbeit) und vermeiden Sie so den lähmenderen Teil des Schätzungsfehlers (der in 2014+ behoben wurde).

Um Fehler am Schalttag zu vermeiden, können Sie dies stattdessen ab dem 28. Februar letzten Jahres anstelle des 29. Februar tun:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

Sie können auch sagen, fügen Sie einen Tag hinzu, indem Sie überprüfen, ob wir dieses Jahr einen Schalttag hinter uns haben, und wenn ja, fügen Sie einen Tag zum Anfang hinzu (interessanterweise ermöglicht die Verwendung DATEADD hier immer noch genaue Schätzungen):

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

Wenn Sie genauer sein müssen als auf den Tag um Mitternacht, können Sie vor der Auswahl einfach weitere Manipulationen hinzufügen:

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

Jetzt können Sie all dies in einer Ansicht blockieren, und es wird weiterhin eine Suche und die 30% -Schätzung verwendet, ohne dass Hinweise oder Ablaufverfolgungsflags erforderlich sind, aber es ist nicht schön. Verschachtelte CTEs dienen nur dazu, dass ich nicht SYSUTCDATETIME()hundertmal tippen oder wiederverwendete Ausdrücke wiederholen muss - sie können immer noch mehrmals ausgewertet werden.

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

Dies ist viel ausführlicher als Ihre DATEDIFFgegen die Kolumne, aber wie ich in einem Kommentar erwähnt habe , ist dieser Ansatz nicht sargable und wird wahrscheinlich wettbewerbsfähig sein, während der größte Teil der Tabelle sowieso gelesen werden muss, aber ich vermute, dass es eine Belastung wird als "das letzte Jahr" wird ein niedrigerer Prozentsatz der Tabelle.

Nur als Referenz, hier sind einige der Metriken, die ich erhalten habe, als ich versucht habe zu reproduzieren:

Geben Sie hier die Bildbeschreibung ein

Ich konnte keine Schätzungen für eine Zeile erhalten und habe mich sehr bemüht, Ihre Verteilung anzupassen (3,13 Millionen Zeilen, 2,89 Millionen aus dem letzten Jahr). Aber Sie können sehen:

  • Unsere beiden Lösungen führen ungefähr gleichwertige Lesevorgänge durch.
  • Ihre Lösung ist etwas ungenauer, da sie nur Tagesgrenzen berücksichtigt (und das könnte in Ordnung sein, meine Ansicht könnte weniger genau angepasst werden).
  • Die Neukompilierung von 4199 + hat die Schätzungen (oder die Pläne) nicht wirklich geändert.

Ziehen Sie nicht zu viel aus den Durationszahlen - sie sind jetzt nahe beieinander, bleiben aber möglicherweise nicht nahe beieinander, wenn die Tabelle wächst (wieder glaube ich, weil selbst die Suche noch den größten Teil der Tabelle lesen muss).

Hier sind die Pläne für v4 (Ihr Datum gegen Spalte) und v5 (meine Version):

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Aaron Bertrand
quelle
Zusammenfassend, wie in Ihrem Blog angegeben . Diese Antwort liefert eine brauchbare Schätzung und einen suchbasierten Plan. Die Antwort von @PaulWhite gibt die beste Schätzung. Vielleicht könnten die Schätzungen für 1 Zeile, die ich erhalten habe (gegenüber 1500), darauf zurückzuführen sein, dass die Tabelle in den letzten ~ 24 Stunden keine Zeilen hatte.
Crokusek
@crokusek Wenn Sie sagen, dass >= DATEADD(DAY, -365, SYSDATETIME())der Fehler darin besteht, dass die Schätzung auf basiert >= SYSDATETIME(). Technisch gesehen basiert die Schätzung also darauf, wie viele Zeilen in der Tabelle CreatedUtcin Zukunft eine haben. Dies ist wahrscheinlich 0, aber SQL Server rundet 0 für geschätzte Zeilen immer auf 1 auf.
Aaron Bertrand
1

Ersetzen Sie dateadd () durch dateiff (), um einen angemessenen ungefähren Wert (30% ish) zu erhalten.

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

Dies scheint ein Fehler zu sein, der MS Connect 630583 ähnelt .

Die Neukompilierung von Optionen macht keinen Unterschied.

Planen Sie Statistiken

crokusek
quelle
2
Beachten Sie, dass das Anwenden von dateiff auf die Spalte den Ausdruck nicht sargbar macht, sodass Sie scannen müssen. Was wahrscheinlich in Ordnung ist, wenn 90 +% der Tabelle ohnehin gelesen werden müssen, aber wenn die Tabelle größer wird, wird dies teurer.
Aaron Bertrand
Toller Punkt. Ich dachte, es könnte es intern konvertieren. Es wurde überprüft, ob ein Scan durchgeführt wird.
Crokusek