web-dev-qa-db-fra.com

Mauvaise performance de la table temporelle sur les valeurs plus anciennes

Je rencontre un problème étrange lors de l'accès aux enregistrements historiques dans une table temporelle. Les requêtes qui accèdent aux anciennes entrées de la table temporelle via la sous-clause AS OF prennent plus de temps que les requêtes sur les entrées d'historique récentes.

La table historique a été générée par SQL Server (inclut un index clusterisé sur les colonnes de date et utilise la compression de page), j'ai ajouté 50 millions de lignes à la table historique et mes requêtes récupéraient environ 25 000 lignes.

J'ai essayé de déterminer la cause première du problème, mais je n'ai pas pu l'identifier. Jusqu'à présent, j'ai testé:

  • Création d'une table de test avec 50 millions de lignes avec un index clusterisé pour voir si le ralentissement était simplement dû au volume. J'ai pu récupérer 25 000 lignes à temps constant (~ 400 ms).
  • Suppression de la compression de page du tableau historique. Cela n'a eu aucun effet sur le temps de récupération, mais a considérablement augmenté la taille de la table.
  • J'ai essayé d'accéder aux lignes de la table d'historique directement en utilisant une colonne ID par rapport aux colonnes de date. C'est là que les choses étaient un peu plus intéressantes. Je pouvais accéder aux anciennes lignes du tableau à ~ 400 ms, comme pour la sous-clause AS OF, cela prendrait ~ 1200 ms. J'ai essayé de filtrer sur ma table de test sur la colonne date et j'ai remarqué un ralentissement similaire par rapport au filtrage sur la colonne ID. Cela m'amène à croire que les comparaisons de dates sont à l'origine d'une partie du ralentissement.

Je veux regarder cela plus en détail, mais je veux aussi m'assurer que je n'aboie pas dans le mauvais arbre. Premièrement, quelqu'un d'autre a-t-il connu ce même comportement lors de l'accès à des données historiques plus anciennes dans une table temporelle (nous avons seulement remarqué que les ralentissements dépassaient 10 millions de lignes)? Deuxièmement, quelles sont les stratégies que je peux utiliser pour isoler davantage la cause première du problème de performances (je viens de commencer à étudier les plans d'exécution, mais c'est toujours un peu cryptique pour moi)?

Plans d'exécution

Il s'agit de requêtes de récupération simples: la première accède aux anciennes lignes, la seconde accède aux nouvelles lignes.

Rangées plus anciennes ~ 1200ms de temps d'exécution

Lignes récentes ~ 350ms de temps d'exécution

Détails du tableau

Ce sont les colonnes du tableau temporel. La table d'historique a les mêmes colonnes mais n'a pas de clé primaire (selon les exigences de la table d'historique): Temporal table column

Voici les indices du tableau d'historique: Indices on the history table

8
Ebrahim Behbahani

Dans un commentaire de Zane sur votre question, il a déclaré:

... Il semble qu'une partie de votre problème est que vous lisez 50 millions de lignes afin de retourner 20K dans le plan.

Tel est en effet le problème. Aucun index n'est disponible pour pousser certains ou tous les prédicats vers le moteur de stockage. Microsoft recommande cette stratégie d'indexation de base pour les tables temporelles dans l'article Docs Temporal Table Considerations and Limitations :

Une stratégie d'indexation optimale comprendra un index de stockage de colonnes en cluster et/ou un index de magasin de lignes d'arborescence B sur la table actuelle et un index de magasin de colonnes en cluster sur la table d'historique pour une taille et des performances de stockage optimales. Si vous créez/utilisez votre propre table d'historique, nous vous recommandons fortement de créer ce type d'index composé de colonnes de période commençant par la colonne de fin de période pour accélérer les requêtes temporelles ainsi que pour accélérer les requêtes qui font partie de la cohérence des données. vérifier. La table d'historique par défaut a un index de magasin de lignes en cluster créé pour vous en fonction des colonnes de période (fin, début). Au minimum, un index de magasin de lignes non cluster est recommandé

La formulation de cela est un peu déroutante (pour moi, en tout cas). Mais la leçon à retenir est que vous pouvez créer ces index pour améliorer les performances, sinon beaucoup:

Index NC sur la table en cours, commençant par SysEndTime:

CREATE NONCLUSTERED INDEX IX_SysEndTime_SysStartTime 
ON dbo.Benefits (SysEndTime, SysStartTime)
/*INCLUDE (ideally, include your other important fields here)*/;

Cela vous permettra d'éviter de lire certaines des lignes du tableau actuel en recherchant l'heure de fin appropriée.

CCI sur la table de l'histoire

CREATE CLUSTERED COLUMNSTORE INDEX ix_BenefitsHistory
ON dbo.BenefitsHistory
WITH (DROP_EXISTING = ON);

Cela vous permettra d'obtenir le mode batch sur la table d'historique, ce qui devrait rendre les analyses beaucoup plus rapides.

Index NC sur la table en cours, commençant par SysStartTime:

Voir la réponse de Paul à la question Méthode la plus efficace pour récupérer des plages de dates pour plus de détails sur la raison pour laquelle l'indexation des requêtes de plage de dates est difficile. Sur la base de la logique, il est logique d'ajouter un autre index CN sur la table actuelle qui mène à SysStartTime, afin que l'optimiseur puisse choisir celui à utiliser en fonction des statistiques et des paramètres spécifiques de votre requête:

CREATE NONCLUSTERED INDEX IX_SysStartTime_SysEndTime
ON dbo.Benefits (SysStartTime, SysEndTime)
/*INCLUDE (ideally, include your other important fields here)*/;

La création des 3 index décrits ci-dessus a fait une différence significative dans l'utilisation des ressources dans mes cas de test. J'ai mis en place un scénario de test qui exécute deux requêtes qui retournent 1,5 million de lignes au total. L'historique et les tables actuelles contiennent 50 millions de lignes).

Remarque: Pour réduire la surcharge SSMS, j'ai exécuté le test avec l'option "Supprimer les résultats après l'exécution" activée

Plan d'exécution - Index par défaut

Lectures logiques: 1 330 612
Temps processeur: 00: 00: 14.718
Temps écoulé: 00: 00: 06.198

Plan d'exécution - avec les index décrits ci-dessus

Lectures logiques: 27 656 (magasin de 8 111 lignes + magasin de colonnes 19 545)
Temps processeur: 00: 00: 01.828
Temps écoulé: 00: 00: 01.150

Comme vous pouvez le voir, les 3 mesures ont chuté de manière significative - y compris le temps total écoulé, de 6 secondes à 1 seconde.


L'autre option présentée par l'article Docs consiste à renoncer aux deux index NC de la table actuelle au profit d'un index columnstore en cluster. Dans mon test, les performances étaient très similaires à la solution d'indexation décrite ci-dessus.

6
Josh Darnell

La clause FOR SYSTEM TIME AS OF Essaie de renvoyer l'ensemble de données tel qu'il existait à l'heure indiquée. Cela signifie que les mises à jour doivent être annulées en interne, les suppressions doivent être "non supprimées" et les insertions doivent être ignorées, en fonction de l'heure système de la demande.

Plus le temps AS OF est éloigné dans le passé, plus le travail doit être validé pour garantir que la table temporelle est telle qu'elle existait à l'heure système spécifiée, et donc plus la requête prendra de temps.

SI la table de données n'est qu'une table de journalisation et qu'aucune modification n'est apportée aux données, l'utilisation de la date enregistrée et d'un index renvoie les données plus rapidement et de manière plus cohérente. Il n'est pas nécessaire d'utiliser les caractéristiques temporelles dans ce cas. Cependant, si des modifications sont apportées aux lignes (autres que les insertions), l'utilisation de la fonction de table temporelle est le seul moyen de renvoyer les données exactes demandées (l'état de la table tel qu'il existait à ce moment précis), et vous il suffit d'accepter la surcharge supplémentaire des requêtes temporelles.

Remarque: Les "annulations" ne sont pas des annulations réelles. Les tables temporelles utilisent deux tables: une table actuelle et une table historique. Lorsqu'une ligne est modifiée, une copie de la version précédente est insérée dans la table Historique avec la plage de temps pendant laquelle la ligne était valide. Si vous insérez une ligne au 20/10/2018 10: 20: 20.18, mettez à jour une valeur au 25/10/2018 10: 25: 20.18 et mettez-la à nouveau à jour au 12/01/2018 12: 01: 20.18, vous avez la dernière version de la ligne du tableau actuel avec une date de début du 12/01/2018 12: 01: 20.18 et deux lignes du tableau d'historique avec des plages valides de 10/20 au 25/10/2018 et 10/25 au 12/01/2018

2
Laughing Vergil