So speichern Sie Zeitreihendaten


22

Ich glaube, es handelt sich um einen Zeitreihendatensatz (bitte korrigieren Sie mich, wenn ich mich irre), der eine Reihe von zugehörigen Werten enthält.

Ein Beispiel wäre, ein Auto zu modellieren und seine verschiedenen Attribute während einer Reise zu verfolgen. Beispielsweise:

Zeitstempel | Geschwindigkeit | zurückgelegte Strecke | Temperatur | etc

Wie können diese Daten am besten gespeichert werden, damit eine Webanwendung die Felder effizient abfragen kann, um Max, Min und Plot für jeden Datensatz im Zeitverlauf zu ermitteln?

Ich begann einen naiven Ansatz, bei dem ich den Datendump analysierte und die Ergebnisse zwischenspeicherte, damit sie niemals gespeichert werden mussten. Nachdem Sie ein wenig damit gespielt haben, scheint es jedoch, dass diese Lösung aufgrund von Speicherbeschränkungen nicht langfristig skaliert werden kann. Wenn der Cache geleert werden soll, müssen alle Daten erneut analysiert und zwischengespeichert werden.

Unter der Annahme, dass Daten jede Sekunde mit der seltenen Möglichkeit von Datensätzen von mehr als 10 Stunden aufgezeichnet werden, wird im Allgemeinen empfohlen, den Datensatz alle N Sekunden durch Abtasten zu kürzen.

Antworten:


31

Es gibt wirklich keinen "besten Weg", Zeitreihendaten zu speichern, und das hängt ehrlich gesagt von einer Reihe von Faktoren ab. Ich werde mich jedoch in erster Linie auf zwei Faktoren konzentrieren:

(1) Wie ernst ist dieses Projekt, das Ihre Bemühungen zur Optimierung des Schemas verdient?

(2) Wie werden Ihre Abfragezugriffsmuster wirklich aussehen?

Lassen Sie uns unter Berücksichtigung dieser Fragen einige Schemaoptionen diskutieren.

Flacher Tisch

Die Option, einen flachen Tisch zu verwenden, hat viel mehr mit Frage (1) zu tun. Wenn dies kein ernstes oder umfangreiches Projekt ist, fällt es Ihnen viel leichter, nicht zu viel über das Schema nachzudenken benutze einfach einen flachen Tisch als:

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

Es gibt nicht viele Fälle, in denen ich diesen Kurs empfehlen würde, nur wenn es sich um ein kleines Projekt handelt, das nicht viel Zeit in Anspruch nimmt.

Maße und Fakten

Wenn Sie also die Hürde von Frage (1) genommen haben und ein leistungsfähigeres Schema wünschen, ist dies eine der ersten zu berücksichtigenden Optionen. Es beinhaltet einige grundlegende Normailisierungen, aber das Extrahieren der 'dimensionalen' Größen aus den gemessenen 'Tatsachen'-Größen.

Im Wesentlichen möchten Sie eine Tabelle, um Informationen über die Reisen aufzuzeichnen,

CREATE trips(
  trip_id integer,
  other_info text);

und eine Tabelle zum Aufzeichnen von Zeitstempeln,

CREATE tstamps(
  tstamp_id integer,
  tstamp timestamptz);

und schließlich alle Ihre gemessenen Fakten mit Fremdschlüsselverweisen auf die Dimensionstabellen (dh meas_facts(trip_id)Referenzen trips(trip_id)und meas_facts(tstamp_id)Referenzen tstamps(tstamp_id))

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

Dies scheint auf den ersten Blick nicht sehr hilfreich zu sein, aber wenn Sie beispielsweise Tausende von Fahrten gleichzeitig durchführen, werden die Messungen möglicherweise alle einmal pro Sekunde und in der Sekunde durchgeführt. In diesem Fall müssten Sie den Zeitstempel jedes Mal neu für jede Reise aufzeichnen, anstatt nur einen einzelnen Eintrag in der tstampsTabelle zu verwenden.

Anwendungsfall: Dieser Fall ist hilfreich, wenn Sie bei vielen gleichzeitigen Fahrten Daten aufzeichnen und es Ihnen nichts ausmacht, auf alle Messtypen zusammen zuzugreifen.

Da Postgres zeilenweise liest, müssen Sie zu jedem Zeitpunkt, zu dem Sie beispielsweise die speedMessungen über einen bestimmten Zeitraum durchführen möchten, die gesamte Zeile aus der meas_factsTabelle auslesen , was eine Abfrage auf jeden Fall verlangsamt, auch wenn der Datensatz, mit dem Sie arbeiten, derselbe ist nicht zu groß, dann merkt man den Unterschied gar nicht.

Aufteilen Ihrer gemessenen Fakten

Um den letzten Abschnitt noch ein wenig zu erweitern, können Sie Ihre Messungen in separate Tabellen aufteilen, in denen ich zum Beispiel die Tabellen für Geschwindigkeit und Entfernung zeige:

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

und

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

Natürlich können Sie sehen, wie dies auf die anderen Messungen ausgeweitet werden kann.

Anwendungsfall: Dies bedeutet also keine enorme Geschwindigkeitssteigerung für eine Abfrage, möglicherweise nur eine lineare Geschwindigkeitssteigerung, wenn Sie nach einem Messtyp abfragen. Dies liegt daran, dass Sie zum Nachschlagen von Informationen zur Geschwindigkeit nur Zeilen aus der speed_factsTabelle lesen müssen und nicht alle zusätzlichen, nicht benötigten Informationen, die in einer meas_factsTabellenzeile vorhanden wären .

Wenn Sie also große Datenmengen nur zu einem Messtyp lesen müssen, können Sie einige Vorteile erzielen. Bei Ihrem vorgeschlagenen Fall von 10 Stunden Daten in Abständen von einer Sekunde würden Sie nur 36.000 Zeilen lesen, so dass Sie nie wirklich einen nennenswerten Nutzen daraus ziehen würden. Wenn Sie sich jedoch Geschwindigkeitsmessdaten für 5.000 Fahrten mit einer Gesamtdauer von 10 Stunden ansehen, sollten Sie jetzt 180 Millionen Zeilen lesen. Eine lineare Erhöhung der Geschwindigkeit für eine solche Abfrage kann sich als vorteilhaft erweisen, sofern Sie nur auf einen oder zwei der Messtypen gleichzeitig zugreifen müssen.

Arrays / HStore / & TOAST

Sie müssen sich wahrscheinlich nicht um diesen Teil kümmern, aber ich kenne Fälle, in denen es wichtig ist. Wenn Sie auf große Mengen von Zeitreihendaten zugreifen müssen und wissen, dass Sie auf alle Daten in einem großen Block zugreifen müssen, können Sie eine Struktur verwenden, die die TOAST-Tabellen verwendet , in denen Ihre Daten im Wesentlichen in größeren, komprimierten Formaten gespeichert werden Segmente. Dies führt zu einem schnelleren Zugriff auf die Daten, solange Ihr Ziel darin besteht, auf alle Daten zuzugreifen.

Eine beispielhafte Implementierung könnte sein

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

In dieser Tabelle tstartwürde der Zeitstempel für den ersten Eintrag im Array gespeichert, und jeder nachfolgende Eintrag wäre der Wert eines Messwerts für die nächste Sekunde. Dazu müssen Sie den relevanten Zeitstempel für jeden Array-Wert in einer Anwendungssoftware verwalten.

Eine andere Möglichkeit ist

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

wo Sie Ihre Messwerte als (Schlüssel, Wert) Paare von (Zeitstempel, Messung) hinzufügen.

Anwendungsfall: Dies ist eine Implementierung, die wahrscheinlich besser jemandem überlassen wird, der mit PostgreSQL vertraut ist, und nur dann, wenn Sie sicher sind, dass Ihre Zugriffsmuster Bulk-Zugriffsmuster sein müssen.

Schlussfolgerungen

Wow, das hat viel länger gedauert als ich erwartet hatte, sorry. :)

Grundsätzlich gibt es eine Reihe von Optionen, aber Sie werden wahrscheinlich den größten Gewinn für Ihr Geld erzielen, wenn Sie die zweite oder dritte Option verwenden, da sie für den allgemeineren Fall geeignet sind.

PS: Ihre anfängliche Frage implizierte, dass Sie Ihre Daten in großen Mengen laden, nachdem sie alle gesammelt wurden. Wenn Sie die Daten in Ihre PostgreSQL-Instanz streamen, müssen Sie einige weitere Arbeiten ausführen, um sowohl die Datenerfassung als auch die Abfragearbeit zu bewältigen, aber das belassen wir für eine andere Zeit. ;)


Wow, danke für die ausführliche Antwort, Chris! Ich werde Option 2 oder 3 verwenden.
guest82

Viel Glück!
Chris

Wow, ich würde diese Antwort 1000 Mal abstimmen, wenn ich könnte. Danke für die ausführliche Erklärung.
kikocorreoso

1

Seine 2019 und diese Frage verdient eine aktualisierte Antwort.

  • Ob der Ansatz der beste ist oder nicht, überlasse ich Ihnen das Benchmarking und Testen, aber hier ist ein Ansatz.
  • Verwenden Sie eine Datenbankerweiterung namens timescaledb
  • Dies ist eine Erweiterung, die auf Standard-PostgreSQL installiert ist und einige Probleme beim Speichern von Zeitreihen relativ gut behandelt

Erstellen Sie zunächst eine einfache Tabelle in PostgreSQL

Schritt 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
) 

Schritt 2

  • Machen Sie daraus einen so genannten Hypertable in der Welt der Zeitskalierung.
  • Mit einfachen Worten, es handelt sich um eine große Tabelle, die kontinuierlich in kleinere Tabellen mit bestimmten Zeitintervallen unterteilt wird, z. B. an einem Tag, an dem jede Minitabelle als Block bezeichnet wird
  • Diese Minitabelle ist nicht offensichtlich, wenn Sie Abfragen ausführen, obwohl Sie sie in Ihre Abfragen einschließen oder ausschließen können

    SELECT create_hypertable ('trip', 'ts', chunk_time_interval => Intervall '1 Stunde', if_not_exists => TRUE);

  • Was wir oben gemacht haben, ist, dass wir unseren Trip-Tisch stündlich anhand der Spalte 'ts' in Mini-Chunk-Tische aufteilen. Wenn Sie einen Zeitstempel von 10:00 bis 10:59 hinzufügen, werden sie zu einem Block hinzugefügt, aber 11:00 wird in einen neuen Block eingefügt und dies wird unendlich fortgesetzt.

  • Wenn Sie keine unbegrenzten Datenmengen speichern möchten, können Sie auch DROP-Blöcke verwenden, die älter als 3 Monate sind

    SELECT drop_chunks (Intervall '3 Monate', 'Reise');

  • Sie können auch eine Liste aller bis zu diesem Datum erstellten Chunks mit einer Abfrage wie erhalten

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

  • Auf diese Weise erhalten Sie eine Liste aller bis dahin erstellten Minitabellen, und Sie können eine Abfrage für die letzte Minitabelle ausführen, wenn Sie diese Liste verwenden möchten

  • Sie können Ihre Abfragen optimieren, um Blöcke einzuschließen, auszuschließen oder nur mit den letzten N Blöcken zu arbeiten und so weiter

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.