Database
 sql >> Base de données >  >> RDS >> Database

Surprises et hypothèses de performance :DATEADD

En 2013, j'ai écrit à propos d'un bogue dans l'optimiseur où les 2e et 3e arguments de DATEDIFF() peuvent être permutées, ce qui peut entraîner des estimations incorrectes du nombre de lignes et, par conséquent, une mauvaise sélection du plan d'exécution :

  • Surprises et hypothèses de performance :DATEDIFF

Le week-end dernier, j'ai appris une situation similaire et j'ai immédiatement supposé qu'il s'agissait du même problème. Après tout, les symptômes semblaient presque identiques :

  1. Il y avait une fonction date/heure dans WHERE clause.
    • Cette fois, c'était DATEADD() au lieu de DATEDIFF() .
  2. Il y avait une estimation du nombre de lignes manifestement incorrecte de 1, par rapport à un nombre réel de lignes de plus de 3 millions.
    • C'était en fait une estimation de 0, mais SQL Server arrondit toujours ces estimations à 1.
  3. Une mauvaise sélection de plan a été effectuée (dans ce cas, une jointure en boucle a été choisie) en raison de l'estimation basse.

Le schéma incriminé ressemblait à ceci :

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

L'utilisateur a essayé plusieurs variantes, mais rien n'a changé; ils ont finalement réussi à contourner le problème en changeant le prédicat en :

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

Cela a obtenu une meilleure estimation (l'estimation typique d'inégalité de 30 % ); donc pas tout à fait raison. Et bien qu'il ait éliminé la jointure de boucle, il y a deux problèmes majeurs avec ce prédicat :

  1. Ce n'est pas la même requête, car il recherche maintenant des limites de 365 jours passées, au lieu d'être supérieures à un point précis dans le temps il y a 365 jours. Statistiquement significatif? Peut être pas. Mais techniquement, ce n'est pas la même chose.
  2. L'application de la fonction sur la colonne rend l'expression entière non sargable, ce qui conduit à une analyse complète. Lorsque le tableau ne contient qu'un peu plus d'un an de données, ce n'est pas grave, mais à mesure que le tableau s'agrandit ou que le prédicat se rétrécit, cela deviendra un problème.

Encore une fois, j'ai sauté à la conclusion que le DATEADD() l'opération était le problème et a recommandé une approche qui ne reposait pas sur DATEADD() – construire un datetime de toutes les parties de l'heure actuelle, me permettant de soustraire une année sans utiliser DATEADD() :

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

En plus d'être volumineux, cela avait ses propres problèmes, à savoir qu'un tas de logique devrait être ajouté pour tenir compte correctement des années bissextiles. Premièrement, pour qu'il n'échoue pas s'il se produit le 29 février, et deuxièmement, pour inclure exactement 365 jours dans tous les cas (au lieu de 366 pendant l'année suivant un jour bissextile). Des correctifs faciles, bien sûr, mais ils rendent la logique beaucoup plus laide, en particulier parce que la requête devait exister à l'intérieur d'une vue, où les variables intermédiaires et les étapes multiples ne sont pas possibles.

Dans l'intervalle, l'OP a déposé un article Connect, consterné par l'estimation à 1 ligne :

  • Connect #2567628 :la contrainte avec DateAdd() ne fournit pas de bonnes estimations

Puis Paul White (@SQL_Kiwi) est arrivé et, comme de nombreuses fois auparavant, a apporté un éclairage supplémentaire sur le problème. Il a partagé un élément Connect connexe déposé par Erland Sommarskog en 2011 :

  • Connect #685903 :Estimation incorrecte lorsque sysdatetime apparaît dans une expression dateadd()

Essentiellement, le problème est qu'une mauvaise estimation peut être faite non seulement lorsque SYSDATETIME() (ou SYSUTCDATETIME() ) apparaît, comme Erland l'a signalé à l'origine, mais quand n'importe quel datetime2 l'expression est impliquée dans le prédicat (et peut-être seulement lorsque DATEADD() est également utilisé). Et cela peut aller dans les deux sens - si nous échangeons >= pour <= , l'estimation devient la table entière, il semble donc que l'optimiseur regarde le SYSDATETIME() value en tant que constante et en ignorant complètement toutes les opérations telles que DATEADD() qui sont exécutés contre lui.

Paul a partagé que la solution de contournement consiste simplement à utiliser un datetime équivalent lors du calcul de la date, avant de la convertir dans le type de données approprié. Dans ce cas, nous pouvons échanger SYSUTCDATETIME() et changez-le en GETUTCDATE() :

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

Oui, cela entraîne une petite perte de précision, mais une particule de poussière pourrait également ralentir votre doigt en appuyant sur F5 clé. L'important est qu'une recherche puisse toujours être utilisée et les estimations étaient correctes - presque parfaites, en fait :

Les lectures sont similaires car la table contient presque exclusivement des données de l'année écoulée, de sorte que même une recherche devient une analyse de plage de la majeure partie de la table. Le nombre de lignes n'est pas identique car (a) la deuxième requête se termine à minuit et (b) la troisième requête inclut un jour supplémentaire de données en raison du jour bissextile plus tôt cette année. Dans tous les cas, cela montre encore comment nous pouvons nous rapprocher des estimations appropriées en éliminant DATEADD() , mais la solution appropriée consiste à supprimer la combinaison directe de DATEADD() et datetime2 .

Pour illustrer davantage comment les estimations se trompent, vous pouvez voir que si nous passons différents arguments et directions à la requête d'origine et à la réécriture de Paul, le nombre de lignes estimées pour la première est toujours basé sur l'heure actuelle - ils ne ne change pas avec le nombre de jours passés (alors que celui de Paul est relativement précis à chaque fois) :

Les lignes réelles pour la première requête sont légèrement inférieures car elle a été exécutée après une longue sieste

Les estimations ne seront pas toujours aussi bonnes ; ma table a juste une distribution relativement stable. Je l'ai rempli avec la requête suivante, puis j'ai mis à jour les statistiques avec une analyse complète, au cas où vous voudriez l'essayer par vous-même :

-- 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 */;

J'ai commenté le nouvel élément Connect et je vais probablement revenir en arrière et retoucher ma réponse Stack Exchange.

La morale de l'histoire

Essayez d'éviter de combiner DATEADD() avec des expressions qui donnent datetime2 , en particulier sur les anciennes versions de SQL Server (c'était sur SQL Server 2012). Cela peut également être un problème, même sur SQL Server 2016, lors de l'utilisation de l'ancien modèle d'estimation de cardinalité (en raison d'un niveau de compatibilité inférieur ou de l'utilisation explicite de l'indicateur de trace 9481). Des problèmes comme celui-ci sont subtils et pas toujours immédiatement évidents, alors j'espère que cela servira de rappel (peut-être même pour moi la prochaine fois que je rencontrerai un scénario similaire). Comme je l'ai suggéré dans le dernier message, si vous avez des modèles de requête comme celui-ci, vérifiez que vous obtenez des estimations correctes et notez quelque part pour les vérifier à nouveau chaque fois que quelque chose de majeur change dans le système (comme une mise à niveau ou un service pack).