Come migliorare la stima di 1 riga in una vista vincolata da DateAdd () rispetto a un indice


8

Utilizzo di Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Data una tabella e un indice:

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)

Le righe effettive per ciascuna delle seguenti query sono 3,1 M, le righe stimate vengono visualizzate come commenti.

Quando queste query alimentano un'altra query in una vista , l'ottimizzatore sceglie un join loop a causa delle stime a 1 riga. Come migliorare la stima a questo livello base per evitare l'override del suggerimento di join della query principale o il ricorso a un SP?

L'uso di una data codificata funziona alla grande:

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

Queste query equivalenti sono compatibili con la visualizzazione ma tutte stimano 1 riga:

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)

Prova alcuni suggerimenti (ma N / A per visualizzare):

 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);

Prova a utilizzare Parametro / Suggerimenti (ma N / A per visualizzare):

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);

Stima vs reale

Le statistiche sono aggiornate.

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

Vengono visualizzate le ultime righe dell'istogramma (189 righe in totale):

inserisci qui la descrizione dell'immagine

Risposte:


6

Una risposta meno completa di quella di Aaron ma il problema principale è un bug di stima della cardinalità con DATEADDquando si utilizza il tipo datetime2 :

Connetti: stima errata quando sysdatetime appare in un'espressione dateadd ()

Una soluzione alternativa è utilizzare GETUTCDATE(che restituisce datetime):

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

Si noti che la conversione in datetime2 deve essere esterna a DATEADDper evitare l'errore.

Il problema della stima della cardinalità a 1 riga si riproduce per me in tutte le versioni di SQL Server fino al 2016 RC0 incluso in cui viene utilizzato lo stimatore della cardinalità del modello 70.

Aaron Bertrand ha scritto un articolo su questo per SQLPerformance.com:


6

In alcuni scenari, SQL Server può avere stime davvero pazzesche per DATEADD/ DATEDIFF, a seconda di quali siano gli argomenti e quali siano i dati effettivi. Ho scritto di questo per DATEDIFFquando ho a che fare con l'inizio del mese e alcune soluzioni alternative, qui:

Ma il mio consiglio tipico è di smettere di usare DATEADD/ DATEDIFFnelle clausole where / join.

L'approccio seguente, sebbene non molto accurato quando un anno bisestile rientra nell'intervallo filtrato (includerà un giorno in più in quel caso), e mentre arrotondato al giorno, otterrà stime migliori (ma comunque non eccezionali!), Proprio come il tuo non-sargable DATEDIFFcontro l'approccio colonna, e ancora consentire una ricerca da utilizzare:

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

SELECT ... WHERE CreatedUtc >= @start;

È possibile manipolare gli input per DATEFROMPARTSevitare problemi nel giorno bisestile, utilizzare DATETIMEFROMPARTSper ottenere più precisione invece di arrotondare al giorno, ecc. Questo è solo per dimostrare che è possibile popolare una variabile con una data nel passato senza usare DATEADD(è solo un un po 'più di lavoro), evitando così la parte più paralizzante del bug di stima (che è stato corretto nel 2014+).

Per evitare errori nel giorno bisestile, puoi farlo invece, a partire dal 28 febbraio dell'anno scorso anziché dal 29:

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

Potresti anche dire di aggiungere un giorno controllando per vedere se siamo passati un giorno bisestile quest'anno, e in tal caso, aggiungi un giorno all'inizio (è interessante notare che l'utilizzo DATEADD qui consente ancora stime accurate):

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;

Se devi essere più preciso rispetto al giorno a mezzanotte, puoi semplicemente aggiungere più manipolazione prima della selezione:

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;

Ora, potresti inceppare tutto ciò in una vista e utilizzerà comunque una ricerca e la stima del 30% senza richiedere suggerimenti o tracce di tracciamento, ma non è carino. I CTE nidificati sono solo per non dover scrivere SYSUTCDATETIME()cento volte o ripetere espressioni riutilizzate: possono ancora essere valutati più volte.

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);

Questo è molto più dettagliato del tuo DATEDIFFcontro la colonna, ma come ho già detto in un commento , tale approccio non è ampio e probabilmente funzionerà in modo competitivo mentre la maggior parte della tabella deve essere letta comunque, ma sospetto che diventerà un peso poiché "l'ultimo anno" diventa una percentuale inferiore della tabella.

Inoltre, solo per riferimento, ecco alcune delle metriche che ho ottenuto quando ho provato a riprodurre:

inserisci qui la descrizione dell'immagine

Non sono riuscito a ottenere stime a 1 riga e ho cercato di abbinare la tua distribuzione (3,13 milioni di righe, 2,89 milioni dall'ultimo anno). Ma puoi vedere:

  • entrambe le nostre soluzioni eseguono letture approssimativamente equivalenti.
  • la tua soluzione è leggermente meno accurata perché tiene conto solo dei limiti del giorno (e ciò potrebbe andare bene, la mia vista potrebbe essere resa meno precisa per corrispondere).
  • 4199 + ricompilazione non ha davvero modificato le stime (o i piani).

Non trarre troppo dai dati sulla durata: ora sono vicini, ma potrebbero non rimanere vicini man mano che la tabella cresce (di nuovo, credo perché anche la ricerca deve ancora leggere la maggior parte della tabella).

Ecco i piani per v4 (il tuo dateiff contro la colonna) e v5 (la mia versione):

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine


In sintesi, come indicato nel tuo blog . questa risposta fornisce una stima utilizzabile e cerca un piano basato. La risposta di @PaulWhite fornisce la migliore stima. Forse le stime di 1 riga che stavo ottenendo (contro 1500) potrebbero essere dovute alla tabella che non ha alcuna riga nelle ultime 24 ore.
crokusek,

@crokusek Se dici che >= DATEADD(DAY, -365, SYSDATETIME())il bug è che la stima si basa >= SYSDATETIME(). Quindi tecnicamente la stima si basa su quante righe nella tabella hanno un CreatedUtcfuturo. Probabilmente è 0, ma SQL Server arrotonda sempre da 0 a 1 per le righe stimate.
Aaron Bertrand

1

Sostituisci dateadd () con datiff () per ottenere un'adeguata approssimazione (30% ish).

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

Questo sembra essere un bug simile a MS Connect 630583 .

La ricompilazione delle opzioni non fa differenza.

Statistiche del piano


2
Nota che l'applicazione di dateiff alla colonna rende l'espressione non eseguibile, quindi dovrai scansionare. Il che probabilmente va bene quando il 90 +% della tabella deve essere letto comunque, ma quando la tabella diventa più grande questo si rivelerà più costoso.
Aaron Bertrand

Ottimo punto Pensavo che potesse convertirlo internamente. Verificato che sta eseguendo una scansione.
crokusek,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.