Grouper par heure sur un grand ensemble de données

12

À l'aide de MS SQL 2008, je sélectionne un champ moyen parmi 2,5 millions d'enregistrements. Chaque enregistrement représente une seconde. MyField est une moyenne horaire de ces enregistrements d'une seconde. Bien sûr, le CPU du serveur atteint 100% et la sélection prend trop de temps. Je dois éventuellement enregistrer ces valeurs moyennes afin que SQL n'ait pas à sélectionner tous ces enregistrements à chaque demande. Ce qui peut être fait?

  SELECT DISTINCT
         CONVERT(VARCHAR, [timestamp], 1)+' '+ CAST(DATEPART(Hh,[timestamp]) as VARCHAR) AS TimeStampHour,
         MIN([timestamp]) as TimeStamp,
         AVG(MyField) As AvgField
    FROM MyData
   WHERE TimeStamp > '4/10/2011'
GROUP BY CONVERT(VARCHAR, [timestamp], 1)+' '+ CAST(DATEPART(Hh,[timestamp]) as VARCHAR)
ORDER BY TimeStamp

la source
6
TimeStamp fait-il partie d'un index clusterisé? Ça devrait être ...
@antisanity - pourquoi? il utilise au maximum le processeur et non le disque io
Jack dit d'essayer topanswers.xyz

Réponses:

5

La partie de la requête maximise le CPU pendant de longues périodes est les fonctions de la clause GROUP BY et le fait que le regroupement va toujours nécessiter un tri non indexé dans cette instance. Bien qu'un index sur le champ d'horodatage aidera le filtre initial, cette opération doit être effectuée sur chaque ligne à laquelle le filtre correspond. Accélérer cela utilise un itinéraire plus efficace pour faire le même travail que suggéré par Alex, mais vous avez toujours une énorme inefficacité car la combinaison de fonctions que vous utilisez le planificateur de requêtes ne sera pas en mesure de trouver quelque chose qui sera aidé par n'importe quel index, il devra donc parcourir chaque ligne en exécutant d'abord les fonctions pour calculer les valeurs de regroupement, alors seulement il pourra ordonner les données et calculer les agrégats sur les regroupements résultants.

La solution consiste donc à créer le groupe de processus d'une manière ou d'une autre pour laquelle il peut utiliser un index, ou à supprimer la nécessité de prendre en compte toutes les lignes correspondantes à la fois.

Vous pouvez conserver une colonne supplémentaire pour chaque ligne contenant l'heure arrondie à l'heure et indexer cette colonne pour une utilisation dans de telles requêtes. Cela dénormalise vos données et peut donc sembler "sale", mais cela fonctionnerait et serait plus propre que la mise en cache de tous les agrégats pour une utilisation future (et la mise à jour de ce cache lorsque les données de base sont modifiées). La colonne supplémentaire doit être maintenue par déclencheur ou être une colonne calculée persistante, plutôt que maintenue par la logique ailleurs, car cela garantira que tous les emplacements actuels et futurs qui pourraient insérer des données ou mettre à jour les colonnes d'horodatage ou les lignes existantes se traduisent par des données cohérentes dans le nouveau colonne. Vous pouvez toujours extraire le MIN (horodatage). Ce que la requête entraînera de cette façon est toujours une marche dans toutes les lignes (cela ne peut pas être évité, évidemment) mais il peut le faire dans l'ordre des index, sortie d'une ligne pour chaque groupe au fur et à mesure qu'elle atteint la valeur suivante dans l'index plutôt que d'avoir à se souvenir de l'ensemble des lignes pour une opération de tri non indexé avant que le regroupement / l'agrégation puisse être effectué. Il utilisera également beaucoup moins de mémoire, car il n'aura pas besoin de se souvenir des lignes des valeurs de regroupement précédentes afin de traiter celle qu'il regarde maintenant ou les autres.

Cette méthode supprime le besoin de trouver quelque part en mémoire pour l'ensemble des résultats et effectue le tri non indexé pour l'opération de groupe et supprime le calcul des valeurs de groupe de la grande requête (déplacement de ce travail vers les INSERT / UPDATE individuels qui produisent le données) et devrait permettre à ces requêtes de s'exécuter de manière acceptable sans avoir besoin de conserver un magasin séparé des résultats agrégés.

Une méthode qui ne fonctionne pasdénormaliser vos données, mais nécessite encore une structure supplémentaire, consiste à utiliser un «calendrier», dans ce cas, celui contenant une ligne par heure pendant tout le temps que vous êtes susceptible de considérer. Ce tableau ne consommerait pas une quantité d'espace importante dans une base de données ou une taille appréciable - pour couvrir une période de 100 ans, un tableau contenant une ligne de deux dates (le début et la fin de l'heure, comme '2011-01-01 @ 00: 00: 00.0000 ',' 2011-01-01 @ 00: 00: 59.9997 ', le "9997" étant le plus petit nombre de millisecondes un champ DATETIME ne sera pas arrondi à la seconde suivante), qui font tous deux partie de la la clé primaire en cluster prendra environ 14 Mo d'espace (8 + 8 octets par ligne * 24 heures / jour * 365,25 jours / an * 100, plus un peu pour la surcharge de la structure arborescente de l'index en cluster, mais cette surcharge ne sera pas significative) .

SELECT CONVERT(VARCHAR, [timestamp], 1)+' '+ CAST(DATEPART(Hh,[timestamp]) as VARCHAR) AS TimeStampHour
     , MIN([timestamp]) as TimeStamp
     , AVG(MyField) As AvgField
FROM TimeRangeByHours tt
INNER JOIN MyData md ON md.TimeStamp BETWEEN tt.StartTime AND tt.EndTime
WHERE tt.StartTime > '4/10/2011'
GROUP BY tt.StartTime
ORDER BY tt.StartTime

Cela signifie que le planificateur de requêtes peut organiser l'utilisation de l'index sur MyData.TimeStamp. Le planificateur de requêtes doit être suffisamment brillant pour comprendre qu'il peut parcourir la table apprivoisée en fonction de l'index sur MyData.TimeStamp, en produisant à nouveau une ligne par regroupement et en supprimant chaque ensemble ou lignes lorsqu'il atteint la valeur de regroupement suivante. Pas de stockage de toutes les lignes intermédiaires quelque part dans la RAM, puis effectuez un tri non indexé sur elles. Bien sûr, cette méthode nécessite que vous créiez la table de temps et que vous vous assuriez qu'elle s'étend suffisamment loin en arrière et en avant, mais vous pouvez utiliser la table de temps pour les requêtes sur de nombreux champs de date dans différentes requêtes, alors que l'option "colonne supplémentaire" nécessiterait une colonne calculée supplémentaire pour chaque champ de date dont vous aviez besoin pour filtrer / grouper de cette manière, et la petite taille de la table (sauf si vous en avez besoin pour s'étendre sur 10,

La méthode du calendrier a une différence supplémentaire (qui pourrait être assez avantageuse) par rapport à votre situation actuelle et à la solution de colonne calculée: elle peut renvoyer des lignes pour des périodes pour lesquelles il n'y a pas de données, simplement en changeant le INNER JOIN dans l'exemple de requête ci-dessus être GAUCHE EXTERNE.

Certaines personnes suggèrent de ne pas avoir de table de temps physique, mais de la renvoyer toujours à partir d'une fonction de retour de table. Cela signifie que le contenu de la table de temps n'est jamais stocké sur (ou doit être lu à partir du) disque et si la fonction est bien écrite, vous n'avez jamais à vous soucier de la durée pendant laquelle la table de temps doit s'étendre d'avant en arrière dans le temps, mais je doutez du coût du processeur de produire une table en mémoire pour certaines lignes, chaque requête vaut la petite économie de la création (et de la maintenance, si sa durée doit dépasser la limite de votre version initiale) de la table de temps physique.

Remarque: vous n'avez pas non plus besoin de cette clause DISTINCT sur votre requête d'origine. Le regroupement garantira que ces requêtes ne renvoient qu'une ligne par période considérée, de sorte que le DISTINCT ne fera rien d'autre que de faire tourner le processeur un peu plus (sauf si le planificateur de requêtes remarque que le distinct serait un no-op dans ce cas, il ignorez-le et n'utilisez pas de temps CPU supplémentaire).

David Spillett
la source
3

Voir cette question ( fixer une date ) Aussi, pourquoi s'embêter à tout convertir en chaîne - vous pouvez le faire plus tard (si vous en avez besoin).

  SELECT DISTINCT
         dateadd(hour,datediff(hour,0,[timestamp]),0) AS TimeStampHour,
         MIN([timestamp]) as TimeStamp,
         AVG(MyField) As AvgField
    FROM MyData
   WHERE TimeStamp > '4/10/2011'
GROUP BY dateadd(hour,datediff(hour,0,[timestamp],0);
ORDER BY TimeStamp
Hogan
la source
1

Souhaitez-vous accélérer la requête ou vous demandez comment créer un instantané de données et l'enregistrer?

Si vous voulez le rendre plus rapide, vous avez certainement besoin d'un index sur le champ TimeStamp. Aussi, je suggère d'utiliser ceci pour convertir en heure:

select convert(varchar(13), getdate(), 121)

Si vous devez créer un instantané et le réutiliser ultérieurement, utilisez-le insert intopour créer un nouveau tableau avec les résultats de votre requête. Table d'indexation selon et utilisez-la. D'après ce que je comprends, vous aurez besoin d'un index sur TimeStampHour.

Vous pouvez également configurer un travail qui agrège les données quotidiennes dans votre nouvelle table d'agrégation.

Alex Aza
la source
-1

En convertissant votre clause group by en une chaîne comme celle-ci, vous en faites essentiellement un hit non indexé sur chaque ligne de la base de données. C'est ce qui tue vos performances. Tout serveur à mi-chemin décent sera capable de gérer un agrégat simple comme celui sur un million d'enregistrements très bien si les index sont utilisés correctement. Je modifierais votre requête et mettrais un index clusterisé sur vos horodatages. Cela va résoudre votre problème de performances alors que le calcul des données toutes les heures ne fait que retarder le problème.


la source
1
-1 - non, vous ne "faites pas un hit non indexé à chaque ligne de la base de données" - tout index sur TimeStampsera toujours utilisé pour filtrer les lignes
Jack dit d'essayer topanswers.xyz
-3

J'envisagerais d'abandonner l'idée d'implémenter ce type de calcul à l'aide d'un modèle de base de données relationnelle. Surtout si vous disposez de nombreux points de données pour lesquels vous collectez des valeurs à chaque seconde.

Si vous avez l'argent, vous pouvez envisager d'acheter un historien dédié aux données de processus comme:

  1. Honeywell Uniformance PHD
  2. Osisoft PI
  3. Aspentech IP21
  4. etc.

Ces produits peuvent stocker d'énormes quantités de données chronologiques incroyablement denses (dans des formats propriétaires) tout en permettant simultanément un traitement rapide des requêtes d'extraction de données. Les requêtes peuvent spécifier de nombreux points de données (également appelés balises), de longs intervalles de temps (mois / années) et peuvent en outre effectuer une grande variété de calculs de données récapitulatives (y compris des moyennes).

.. et sur une note générale: j'essaie toujours d'éviter d'utiliser le DISTINCTmot - clé lors de l'écriture de SQL. Ce n'est presque jamais une bonne idée. Dans votre cas, vous devriez pouvoir supprimer DISTINCTet obtenir les mêmes résultats en ajoutant MIN([timestamp])à votre GROUP BYclause.


la source
1
Ce n'est pas vraiment exact. Une base de données relationnelle convient parfaitement pour 2,5 millions d'enregistrements. Et il ne fait même pas de jointures sur beaucoup de tables. La première indication que vous devez dénormaliser vos données ou passer à un système non relationnel est lorsque vous effectuez de grandes jointures complexes sur plusieurs tables. L'ensemble de données de l'affiche ressemble en fait à une utilisation parfaitement acceptable d'un système de base de données relationnelle.