web-dev-qa-db-fra.com

Comment stocker des données de séries chronologiques

J'ai ce que je crois être un ensemble de données de séries chronologiques (veuillez me corriger si je me trompe) qui a un tas de valeurs associées.

Un exemple serait de modéliser une voiture et de suivre ses divers attributs pendant un voyage. Par exemple:

horodatage | vitesse | distance parcourue | température | etc

Quelle serait la meilleure façon de stocker ces données afin qu'une application Web puisse interroger efficacement les champs pour trouver le maximum, les minutes et tracer chaque ensemble de données au fil du temps?

J'ai commencé une approche naïve de l'analyse du vidage des données et de la mise en cache des résultats afin qu'ils n'aient jamais à être stockés. Après avoir joué un peu avec, cependant, il semble que cette solution ne serait pas mise à l'échelle à long terme en raison de contraintes de mémoire et si le cache devait être effacé, alors toutes les données devraient être analysées et remises en cache.

En outre, en supposant que les données soient suivies toutes les secondes avec la rare possibilité d'ensembles de données de plus de 10 heures, est-il généralement conseillé de tronquer l'ensemble de données en échantillonnant toutes les N secondes?

22
guest82

Il n'y a vraiment pas de "meilleure façon" de stocker des données de séries chronologiques, et cela dépend honnêtement d'un certain nombre de facteurs. Cependant, je vais me concentrer principalement sur deux facteurs, à savoir:

(1) Quelle est la gravité de ce projet qu'il mérite vos efforts pour optimiser le schéma?

(2) À quoi ressembleront vos modèles d'accès aux requêtes ?

Avec ces questions à l'esprit, discutons quelques options de schéma.

Table plate

L'option d'utiliser une table plate a beaucoup plus à voir avec la question (1) , où si ce n'est pas un projet sérieux ou à grande échelle , vous trouverez beaucoup plus facile de ne pas trop penser au schéma et d'utiliser simplement une table plate, comme:

CREATE flat_table(
  trip_id integer,
  tstamp timestamptz,
  speed float,
  distance float,
  temperature float,
  ,...);

Il n'y a pas beaucoup de cas où je recommanderais ce cours, seulement si c'est un petit projet qui ne mérite pas beaucoup de votre temps.

Dimensions et faits

Donc, si vous avez surmonté l'obstacle de la question (1) , et que vous voulez un schéma plus performant, c'est l'une des premières options à considérer . Il comprend une normalisation de base, mais en extrayant les quantités "dimensionnelles" des quantités "factuelles" mesurées.

Essentiellement, vous voudrez un tableau pour enregistrer des informations sur les voyages,

CREATE trips(
  trip_id integer,
  other_info text);

et une table pour enregistrer les horodatages,

CREATE tstamps(
  tstamp_id integer,
  tstamp timestamptz);

et enfin tous vos faits mesurés, avec des références de clé étrangère aux tables de dimension (c'est-à-dire meas_facts(trip_id) références trips(trip_id) & meas_facts(tstamp_id) références tstamps(tstamp_id) )

CREATE meas_facts(
  trip_id integer,
  tstamp_id integer,
  speed float,
  distance float,
  temperature float,
  ,...);

Cela peut ne pas sembler très utile au début, mais si vous avez par exemple des milliers de voyages simultanés, ils peuvent tous prendre des mesures une fois par seconde, le deuxième. Dans ce cas, vous devrez réenregistrer l'horodatage à chaque fois pour chaque trajet, plutôt que d'utiliser simplement une seule entrée dans la table tstamps.

Cas d'utilisation: Ce cas sera bon s'il y a de nombreux trajets simultanés pour lesquels vous enregistrez des données, et cela ne vous dérange pas d'accéder à toutes les mesures types tous ensemble.

Étant donné que Postgres lit par lignes, chaque fois que vous voulez, par exemple, les mesures speed sur une plage de temps donnée, vous devez lire la ligne entière de la table meas_facts, Ce qui ralentira définitivement un si le jeu de données avec lequel vous travaillez n'est pas trop volumineux, vous ne remarquerez même pas la différence.

Diviser vos faits mesurés

Pour étendre un peu plus la dernière section, vous pouvez diviser vos mesures en tableaux séparés, où par exemple je montrerai les tableaux de vitesse et de distance:

CREATE speed_facts(
  trip_id integer,
  tstamp_id integer,
  speed float);

et

CREATE distance_facts(
  trip_id integer,
  tstamp_id integer,
  distance float);

Bien sûr, vous pouvez voir comment cela pourrait être étendu aux autres mesures.

Cas d'utilisation: Donc, cela ne vous donnera pas une vitesse énorme pour une requête, peut-être seulement une augmentation linéaire de la vitesse lorsque vous interrogez sur une mesure type. En effet, lorsque vous souhaitez rechercher des informations sur la vitesse, il vous suffit de lire les lignes de la table speed_facts, Plutôt que toutes les informations supplémentaires inutiles qui seraient présentes dans une ligne du meas_facts table.

Donc, vous devez lire d'énormes quantités de données sur un seul type de mesure, vous pourriez en retirer des avantages. Avec votre cas proposé de 10 heures de données à une seconde d'intervalle, vous ne liriez que 36 000 lignes, vous ne verriez donc jamais vraiment de bénéfice significatif à le faire. Cependant, si vous deviez consulter les données de mesure de la vitesse pour 5000 trajets qui duraient environ 10 heures, vous envisagez maintenant de lire 180 millions de lignes. Une augmentation linéaire de la vitesse pour une telle requête pourrait apporter certains avantages, tant que vous n'avez besoin d'accéder qu'à un ou deux des types de mesure à la fois.

Matrices/HStore/& TOAST

Vous n'avez probablement pas à vous soucier de cette partie, mais je connais des cas où cela importe. Si vous devez accéder à [~ # ~] énormes [~ # ~] quantités de séries chronologiques données, et vous savez que vous devez accéder à tout cela dans un bloc énorme, vous pouvez utiliser une structure qui utilisera les TOAST Tables , qui stockent essentiellement vos données dans des segments compressés plus grands. Cela conduit à un accès plus rapide aux données, tant que votre objectif est d'accéder à toutes les données.

Un exemple de mise en œuvre pourrait être

CREATE uber_table(
  trip_id integer,
  tstart timestamptz,
  speed float[],
  distance float[],
  temperature float[],
  ,...);

Dans ce tableau, tstart stockera l'horodatage de la première entrée du tableau et chaque entrée suivante sera la valeur d'une lecture pour la seconde suivante. Cela vous oblige à gérer l'horodatage pertinent pour chaque valeur de tableau dans un logiciel d'application.

Une autre possibilité est

CREATE uber_table(
  trip_id integer,
  speed hstore,
  distance hstore,
  temperature hstore,
  ,...);

où vous ajoutez vos valeurs de mesure sous forme de paires (clé, valeur) de (horodatage, mesure).

Cas d'utilisation: Il s'agit d'une implémentation probablement mieux laissée à quelqu'un qui est plus à l'aise avec PostgreSQL, et seulement si vous êtes sûr que vos modèles d'accès doivent être modèles d'accès en vrac.

Conclusions?

Wow, cela a pris beaucoup plus de temps que prévu, désolé. :)

Essentiellement, il existe un certain nombre d'options, mais vous obtiendrez probablement le meilleur rapport qualité-prix en utilisant la deuxième ou la troisième, car elles conviennent au cas plus général.

P.S.: Votre question initiale impliquait que vous chargeriez vos données en bloc une fois qu'elles auront toutes été collectées. Si vous diffusez les données dans votre instance PostgreSQL, vous devrez effectuer un travail supplémentaire pour gérer à la fois l'ingestion de données et la charge de travail des requêtes, mais nous laisserons cela pour une autre fois. ;)

31
Chris

Son 2019 et cette question mérite une réponse mise à jour.

  • Que l'approche soit la meilleure ou non est quelque chose que je vous laisse évaluer et tester, mais voici une approche.
  • Utilisez une extension de base de données appelée timescaledb
  • Il s'agit d'une extension installée sur PostgreSQL standard et gère plusieurs problèmes rencontrés lors du stockage de séries temporelles raisonnablement bien

En prenant votre exemple, créez d'abord une table simple dans PostgreSQL

Étape 1

CREATE TABLE IF NOT EXISTS trip (
    ts TIMESTAMPTZ NOT NULL PRIMARY KEY,
    speed REAL NOT NULL,
    distance REAL NOT NULL,
    temperature REAL NOT NULL
) 

Étape 2

  • Transformez cela en ce qu'on appelle un hypertable dans le monde de timescaledb.
  • En termes simples, c'est une grande table qui est continuellement divisée en petites tables d'un certain intervalle de temps, disons un jour où chaque mini-table est appelée un morceau
  • Cette mini-table n'est pas évidente lorsque vous exécutez des requêtes bien que vous puissiez l'inclure ou l'exclure dans vos requêtes

    SELECT create_hypertable ('trip', 'ts', chunk_time_interval => intervalle '1 hour', if_not_exists => TRUE);

  • Ce que nous avons fait ci-dessus est de prendre notre table de voyage, de la diviser en mini-tables de morceaux toutes les heures sur la base de la colonne 'ts'. Si vous ajoutez un horodatage de 10h00 à 10h59, ils seront ajoutés à 1 bloc, mais 11h00 seront insérés dans un nouveau bloc et cela continuera indéfiniment.

  • Si vous ne souhaitez pas stocker les données à l'infini, vous pouvez également DROP des morceaux de plus de 3 mois en utilisant

    SELECT drop_chunks (intervalle '3 mois', 'trip');

  • Vous pouvez également obtenir une liste de tous les morceaux créés jusqu'à la date à l'aide d'une requête comme

    SELECT chunk_table, table_bytes, index_bytes, total_bytes FROM chunk_relation_size ('trip');

  • Cela vous donnera une liste de toutes les mini-tables créées jusqu'à la date et vous pouvez exécuter une requête sur la dernière mini-table si vous le souhaitez à partir de cette liste

  • Vous pouvez optimiser vos requêtes pour inclure, exclure des morceaux ou opérer uniquement sur les N derniers morceaux et ainsi de suite

1
PirateApp