Comment stocker des données de séries chronologiques


22

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?

Réponses:


31

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 sur deux facteurs principalement, à savoir:

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

(2) À quoi ressembleront réellement 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 utilisez 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 souhaitez 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és étrangères aux tables de dimension (c'est-à-dire des meas_facts(trip_id)références trips(trip_id)et des 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 de simplement utiliser une seule entrée dans le tstampstableau.

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 à tous les types de mesure tous ensemble.

Étant donné que Postgres lit par lignes, chaque fois que vous voulez, par exemple, les speedmesures sur une plage de temps donnée, vous devez lire la ligne entière de la meas_factstable, ce qui ralentira certainement une requête, bien que si l'ensemble de données avec lequel vous travaillez est pas trop grand, vous ne remarquerez même pas la différence.

Diviser vos faits mesurés

Pour étendre la dernière section un peu plus loin, vous pouvez diviser vos mesures en tableaux séparés, où par exemple je vais montrer 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: cela ne vous donnera donc pas une vitesse énorme pour une requête, peut-être seulement une augmentation linéaire de la vitesse lorsque vous interrogez sur un type de mesure. En effet, lorsque vous souhaitez rechercher des informations sur la vitesse, il vous suffit de lire les lignes de la speed_factstable, plutôt que toutes les informations supplémentaires inutiles qui seraient présentes dans une ligne de la meas_factstable.

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, de sorte que vous ne trouveriez jamais vraiment un avantage 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 avez besoin d'accéder à d' énormes quantités de données de séries chronologiques et que vous savez que vous devez accéder à toutes ces données dans un bloc énorme, vous pouvez utiliser une structure qui utilisera les tables TOAST , qui stockent essentiellement vos données dans des fichiers plus grands et compressés. segments. 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, tstartstockerait l'horodatage pour la première entrée dans le tableau, et chaque entrée suivante serait 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 qu'il vaut probablement mieux laisser à 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 des modèles d'accès en masse.

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.

PS: 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. ;)


Wow, merci pour la réponse détaillée, Chris! Je vais examiner l'utilisation de l'option 2 ou 3.
guest82

Bonne chance à toi!
Chris

Wow, je voterais cette réponse 1000 fois si je le pouvais. Merci pour l'explication détaillée.
kikocorreoso

1

Son 2019 et cette question méritent 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 l'échelle de temps.
  • 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 10 h 00 à 10 h 59, ils seront ajoutés à 1 bloc, mais 11 h 00 seront insérés dans un nouveau bloc et cela se poursuivra indéfiniment.

  • Si vous ne souhaitez pas stocker les données à l'infini, vous pouvez également DROP des morceaux plus anciens que 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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.