Cómo mejorar la estimación de 1 fila en una Vista restringida por DateAdd () contra un índice

8

Uso de Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Dada una tabla e índice:

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)

Las filas reales para cada una de las siguientes consultas son 3.1M, las filas estimadas se muestran como comentarios.

Cuando estas consultas alimentan otra consulta en una Vista , el optimizador elige una unión en bucle debido a las estimaciones de 1 fila. ¿Cómo mejorar la estimación a este nivel del suelo para evitar anular la sugerencia de combinación de consulta principal o recurrir a un SP?

Usar una fecha codificada funciona muy bien:

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

Estas consultas equivalentes son compatibles con la vista, pero todas estiman 1 fila:

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)

Pruebe algunos consejos (pero N / A para ver):

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

Intente usar Parámetro / Sugerencias (pero N / A para Ver):

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

Estimación vs real

Las estadísticas están actualizadas.

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

Se muestran las últimas filas del histograma (189 filas en total):

ingrese la descripción de la imagen aquí

crokusek
fuente

Respuestas:

6

Una respuesta menos completa que la de Aaron, pero el problema principal es un error de estimación de cardinalidad DATEADDcuando se usa el tipo datetime2 :

Conectar: Estimación incorrecta cuando sysdatetime aparece en una expresión dateadd ()

Una solución alternativa es usar GETUTCDATE(que devuelve datetime):

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

Tenga en cuenta que la conversión a datetime2 debe estar fuera de DATEADDpara evitar el error.

El problema de la estimación de cardinalidad de 1 fila se reproduce para mí en todas las versiones de SQL Server hasta 2016 RC0 inclusive, donde se usa el estimador de cardinalidad modelo 70.

Aaron Bertrand ha escrito un artículo sobre esto para SQLPerformance.com:

Paul White 9
fuente
6

En algunos escenarios, SQL Server puede tener estimaciones realmente descabelladas para DATEADD/ DATEDIFF, dependiendo de cuáles son los argumentos y cómo se ven sus datos reales. Escribí sobre esto para DATEDIFFcuando se trata de principios de mes, y algunas soluciones, aquí:

Pero mi consejo típico es dejar de usar las cláusulas DATEADD/ DATEDIFFin where / join.

El siguiente enfoque, aunque no es súper preciso cuando un año bisiesto está en el rango filtrado (incluirá un día adicional en ese caso), y aunque se redondea al día, obtendrá mejores estimaciones (¡pero aún no excelentes!), Al igual que su enfoque no sargable DATEDIFFcontra la columna, y todavía permite que se use una búsqueda:

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

SELECT ... WHERE CreatedUtc >= @start;

Puede manipular las entradas para DATEFROMPARTSevitar problemas en el día bisiesto, usar DATETIMEFROMPARTSpara obtener más precisión en lugar de redondear al día, etc. Esto es solo para demostrar que puede llenar una variable con una fecha en el pasado sin usar DATEADD(es solo un poco más de trabajo), y así evitar la parte más paralizante del error de estimación (que se corrigió en 2014+).

Para evitar errores en el día bisiesto, puede hacerlo, comenzando desde el 28 de febrero del año pasado en lugar del 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
);

También podría decir agregar un día verificando si hemos pasado un día bisiesto este año, y si es así, agregue un día al comienzo (curiosamente, el uso DATEADD aquí todavía permite estimaciones precisas):

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;

Si necesita ser más preciso que el día a la medianoche, simplemente puede agregar más manipulación antes de seleccionar:

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;

Ahora, podría bloquear todo esto en una vista, y todavía usará una búsqueda y la estimación del 30% sin requerir ninguna pista o rastro, pero no es bonito. Los CTE anidados son solo para que no tenga que escribir SYSUTCDATETIME()cien veces o repetir expresiones reutilizadas; aún pueden evaluarse varias veces.

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

Esto es mucho más detallado que su DATEDIFFcontra la columna, pero como mencioné en un comentario , ese enfoque no es sargable, y probablemente tendrá un rendimiento competitivo mientras que la mayor parte de la tabla debe leerse de todos modos, pero sospecho que se convertirá en una carga como "el último año" se convierte en un porcentaje más bajo de la tabla.

Además, solo como referencia, estas son algunas de las métricas que obtuve cuando intenté reproducir:

ingrese la descripción de la imagen aquí

No pude obtener estimaciones de 1 fila, y me esforcé mucho por igualar su distribución (3,13 millones de filas, 2,89 millones del año pasado). Pero puedes ver:

  • nuestras dos soluciones realizan lecturas más o menos equivalentes.
  • su solución es un poco menos precisa porque solo tiene en cuenta los límites del día (y eso podría estar bien, mi opinión podría ser menos precisa para que coincida).
  • 4199 + recompilación realmente no cambió las estimaciones (o los planes).

No extraiga demasiado de las cifras de duración: ahora están cerca, pero es posible que no se mantengan cerca a medida que la tabla crece (nuevamente, creo que incluso la búsqueda aún tiene que leer la mayor parte de la tabla).

Estos son los planes para v4 (su fecha contra columna) y v5 (mi versión):

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Aaron Bertrand
fuente
En resumen, como se indica en su blog . Esta respuesta proporciona una estimación utilizable y un plan basado en la búsqueda. La respuesta de @PaulWhite da la mejor estimación. Quizás las estimaciones de 1 fila que estaba obteniendo (frente a 1500) podrían deberse a que la tabla no tuvo filas en las últimas ~ 24 horas.
crokusek
@crokusek Si dice que >= DATEADD(DAY, -365, SYSDATETIME())el error es en que se basa la estimación >= SYSDATETIME(). Entonces, técnicamente, la estimación se basa en cuántas filas de la tabla tienen un CreatedUtcen el futuro. Probablemente sea 0, pero SQL Server siempre redondea 0 hasta 1 para las filas estimadas.
Aaron Bertrand
1

Reemplace dateadd () con dateiff () para obtener una aproximación adecuada (30% ish).

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

Esto parece ser un error similar a MS Connect 630583 .

La opción de recompilación no hace ninguna diferencia.

Planificar estadísticas

crokusek
fuente
2
Tenga en cuenta que la aplicación de dateiff a la columna hace que la expresión no sea sargable, por lo que tendrá que escanear. Lo que probablemente está bien cuando más del 90% de la tabla necesita leerse de todos modos, pero a medida que la tabla se agrande, esto resultará más costoso.
Aaron Bertrand
Gran punto Estaba pensando que podría convertirlo internamente. Verificado que está realizando un escaneo.
crokusek