Comment améliorer l'estimation d'une ligne dans une vue contrainte par DateAdd () par rapport à un index

8

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

Étant donné une table et un 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)

Les lignes réelles pour chacune des requêtes suivantes sont de 3,1 M, les lignes estimées sont affichées sous forme de commentaires.

Lorsque ces requêtes alimentent une autre requête dans une vue , l'optimiseur choisit une jointure en boucle en raison des estimations sur 1 ligne. Comment améliorer l'estimation à ce niveau du sol pour éviter de remplacer l'indicateur de jointure de requête parent ou de recourir à un SP?

L'utilisation d'une date codée en dur fonctionne très bien:

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

Ces requêtes équivalentes sont compatibles avec les vues, mais toutes estiment 1 ligne:

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)

Essayez quelques conseils (mais N / A pour afficher):

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

Essayez d'utiliser Parameter / Hints (mais N / A pour afficher):

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

Estimation vs réel

Les statistiques sont à jour.

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

Les dernières lignes de l'histogramme (189 lignes au total) sont affichées:

entrez la description de l'image ici

crokusek
la source

Réponses:

6

Une réponse moins complète que celle d'Aaron mais le problème principal est un bogue d'estimation de cardinalité avec l' DATEADDutilisation du type datetime2 :

Connect: estimation incorrecte lorsque sysdatetime apparaît dans une expression dateadd ()

Une solution de contournement consiste à utiliser GETUTCDATE(qui renvoie datetime):

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

Notez que la conversion en datetime2 doit être en dehors de DATEADDpour éviter le bogue.

Le problème de l'estimation de cardinalité à 1 ligne se reproduit pour moi dans toutes les versions de SQL Server jusqu'à 2016 RC0 inclus, où l'estimateur de cardinalité à 70 modèles est utilisé.

Aaron Bertrand a écrit un article à ce sujet pour SQLPerformance.com:

Paul White 9
la source
6

Dans certains scénarios, SQL Server peut avoir des estimations très sauvages pour DATEADD/ DATEDIFF, selon les arguments et l'apparence de vos données réelles. J'ai écrit à ce sujet pour le DATEDIFFdébut du mois et quelques solutions de contournement ici:

Mais, mon conseil typique est de ne plus utiliser les clauses DATEADD/ DATEDIFFin where / join.

L'approche suivante, bien qu'elle ne soit pas très précise lorsqu'une année bissextile est dans la plage filtrée (elle inclura un jour supplémentaire dans ce cas), et bien qu'elle soit arrondie au jour, obtiendra de meilleures estimations (mais toujours pas géniales!), Tout comme votre non-sargable DATEDIFFcontre l'approche de la colonne, et autorisez toujours une recherche à utiliser:

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

SELECT ... WHERE CreatedUtc >= @start;

Vous pouvez manipuler les entrées pour DATEFROMPARTSéviter les problèmes le jour bissextile, utiliser DATETIMEFROMPARTSpour obtenir plus de précision au lieu d'arrondir au jour, etc.Ceci est juste pour démontrer que vous pouvez remplir une variable avec une date dans le passé sans utiliser DATEADD(c'est juste un un peu plus de travail), et ainsi éviter la partie la plus paralysante du bug d'estimation (qui est corrigé en 2014+).

Pour éviter les erreurs le jour bissextile, vous pouvez le faire à la place, à partir du 28 février de l'année dernière au lieu de 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
);

Vous pouvez également dire ajouter un jour en vérifiant si nous avons dépassé un jour bissextile cette année, et si oui, ajoutez un jour au début (il est intéressant de noter que l'utilisation DATEADD ici permet toujours des estimations précises):

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 vous devez être plus précis qu'au jour à minuit, vous pouvez simplement ajouter plus de manipulation avant la sélection:

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;

Maintenant, vous pouvez bloquer tout cela dans une vue, et il utilisera toujours une recherche et l'estimation de 30% sans nécessiter d'indices ou d'indicateurs de trace, mais ce n'est pas joli. Les CTE imbriqués sont juste pour que je n'ai pas à taper SYSUTCDATETIME()cent fois ou répéter des expressions réutilisées - ils peuvent toujours être évalués plusieurs fois.

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

C'est beaucoup plus verbeux que votre DATEDIFFcontre la colonne, mais comme je l'ai mentionné dans un commentaire , cette approche n'est pas discutable et fonctionnera probablement de manière compétitive alors que la majeure partie du tableau doit être lue de toute façon, mais je soupçonne que cela deviendra un fardeau car «l'année dernière» devient un pourcentage inférieur du tableau.

Aussi, juste pour référence, voici quelques-unes des mesures que j'ai obtenues lorsque j'ai essayé de reproduire:

entrez la description de l'image ici

Je n'ai pas pu obtenir d'estimations sur une ligne, et j'ai essayé très fort de faire correspondre votre distribution (3,13 millions de lignes, 2,89 millions par rapport à l'année dernière). Mais vous pouvez voir:

  • nos deux solutions effectuent des lectures à peu près équivalentes.
  • votre solution est légèrement moins précise car elle ne tient compte que des limites de jour (et cela pourrait être bien, mon avis pourrait être rendu moins précis pour correspondre).
  • La recompilation 4199 + n'a pas vraiment changé les estimations (ou les plans).

Ne tirez pas trop sur les chiffres de la durée - ils sont proches maintenant, mais peuvent ne pas rester proches à mesure que le tableau se développe (encore une fois, je crois parce que même la recherche doit encore lire la majeure partie du tableau).

Voici les plans pour v4 (votre datiff contre colonne) et v5 (ma version):

entrez la description de l'image ici

entrez la description de l'image ici

Aaron Bertrand
la source
En résumé, comme indiqué dans votre blog . cette réponse fournit une estimation utilisable et un plan basé sur la recherche. La réponse de @PaulWhite donne la meilleure estimation. Peut-être que les estimations d'une ligne que j'obtenais (vs 1500) pourraient être dues au fait que le tableau n'a pas de lignes au cours des dernières 24 heures.
crokusek
@crokusek Si vous dites que >= DATEADD(DAY, -365, SYSDATETIME())le bug est que l'estimation est basée sur >= SYSDATETIME(). Donc, techniquement, l'estimation est basée sur le nombre de lignes du tableau qui en auront CreatedUtcà l'avenir. Il s'agit probablement de 0, mais SQL Server arrondit toujours 0 à 1 pour les lignes estimées.
Aaron Bertrand
1

Remplacez dateadd () par dateiff () pour obtenir une approximation adéquate (30% ish).

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

Cela semble être un bogue similaire à MS Connect 630583 .

La recompilation des options ne fait aucune différence.

Planifier les statistiques

crokusek
la source
2
Notez que l'application de datiff à la colonne rend l'expression non sargable, vous devrez donc scanner. Ce qui est probablement correct lorsque 90 +% du tableau doivent être lus de toute façon, mais à mesure que le tableau s'agrandit, cela s'avérera plus coûteux.
Aaron Bertrand
Bon point. Je pensais qu'il pourrait le convertir en interne. Vérifie qu'il effectue une analyse.
crokusek