Come archiviare i dati delle serie storiche


22

Ho quello che credo sia un set di dati di serie temporali (per favore correggimi se sbaglio) che ha un sacco di valori associati.

Un esempio potrebbe essere la modellazione di un'auto e il monitoraggio dei vari attributi durante un viaggio. Per esempio:

timestamp | velocità | distanza percorsa | temperatura | eccetera

Quale sarebbe il modo migliore per archiviare questi dati in modo che un'applicazione Web possa interrogare efficacemente i campi per trovare max, min e tracciare ogni set di dati nel tempo?

Ho iniziato un approccio ingenuo di analisi del dump dei dati e memorizzazione nella cache dei risultati in modo che non debbano mai essere archiviati. Dopo averlo giocato un po ', tuttavia, sembra che questa soluzione non si ridimensionerebbe a lungo termine a causa di vincoli di memoria e se la cache dovesse essere cancellata, tutti i dati dovrebbero essere riesaminati e ripuliti nella cache.

Inoltre, supponendo che i dati vengano tracciati ogni secondo con la rara possibilità di set di dati di oltre 10 ore, si consiglia in genere di troncare il set di dati campionando ogni N secondi?

Risposte:


31

Non esiste davvero un "modo migliore" per archiviare i dati delle serie storiche e dipende onestamente da una serie di fattori. Tuttavia, mi concentrerò principalmente su due fattori, tra cui:

(1) Quanto è serio questo progetto che merita i tuoi sforzi per ottimizzare lo schema?

(2) Come saranno realmente i tuoi schemi di accesso alle query ?

Con queste domande in mente, discutiamo alcune opzioni di schema.

Tavolo piatto

L'opzione per usare una tabella piatta ha molto più a che fare con la domanda (1) , dove se questo non è un progetto serio o su larga scala, troverai molto più facile non pensare troppo allo schema e basta usare un tavolo piatto, come:

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

Non ci sono molti casi in cui consiglierei questo corso, solo se si tratta di un piccolo progetto che non ti garantisce gran parte del tuo tempo.

Dimensioni e fatti

Quindi, se hai eliminato l'ostacolo della domanda (1) e desideri uno schema più prestazionale, questa è una delle prime opzioni da considerare. Include alcune normalizzazioni di base, ma estrae le quantità "dimensionali" dalle quantità misurate "di fatto".

In sostanza, vorrai una tabella per registrare informazioni sui viaggi,

CREATE trips(
  trip_id integer,
  other_info text);

e una tabella per registrare i timestamp,

CREATE tstamps(
  tstamp_id integer,
  tstamp timestamptz);

e infine tutti i fatti misurati, con riferimenti di chiave esterna alle tabelle delle dimensioni (ovvero meas_facts(trip_id)riferimenti trips(trip_id)e meas_facts(tstamp_id)riferimenti tstamps(tstamp_id))

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

A prima vista, questo potrebbe non sembrare molto utile, ma se ad esempio hai migliaia di viaggi simultanei, tutti potrebbero prendere le misure una volta al secondo, al secondo. In tal caso, dovresti registrare nuovamente il timestamp ogni volta per ogni viaggio, anziché utilizzare una sola voce nella tstampstabella.

Caso d'uso: questo caso andrà bene se ci sono molti viaggi simultanei per i quali stai registrando dati e non ti dispiace accedere a tutti i tipi di misurazione tutti insieme.

Poiché Postgres legge per riga, ogni volta che si desidera, ad esempio, le speedmisurazioni in un determinato intervallo di tempo, è necessario leggere l'intera riga dalla meas_factstabella, che rallenterà sicuramente una query, anche se il set di dati con cui si sta lavorando è non troppo grande, quindi non noteresti nemmeno la differenza.

Suddivisione dei fatti misurati

Per estendere ulteriormente l'ultima sezione, è possibile suddividere le misure in tabelle separate, dove ad esempio mostrerò le tabelle per velocità e distanza:

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

e

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

Naturalmente, puoi vedere come questo potrebbe essere esteso ad altre misurazioni.

Caso d'uso: quindi questo non ti darà un'enorme velocità per una query, forse solo un aumento lineare della velocità quando si esegue una query su un tipo di misurazione. Questo perché quando si desidera cercare informazioni sulla velocità, è necessario solo leggere le righe dalla speed_factstabella, anziché tutte le informazioni extra non necessarie che sarebbero presenti in una riga della meas_factstabella.

Quindi, devi leggere enormi quantità di dati su un solo tipo di misurazione, potresti ottenere alcuni vantaggi. Con il caso proposto di 10 ore di dati a intervalli di un secondo, leggeresti solo 36.000 righe, quindi non otterrai mai un vantaggio significativo nel farlo. Tuttavia, se dovessi guardare i dati di misurazione della velocità per 5.000 viaggi che sono durati circa 10 ore, ora stai leggendo 180 milioni di righe. Un aumento lineare della velocità per una query di questo tipo potrebbe comportare alcuni vantaggi, purché sia ​​necessario accedere solo a uno o due tipi di misurazione alla volta.

Array / HStore / & TOAST

Probabilmente non dovrai preoccuparti di questa parte, ma conosco i casi in cui è importante. Se devi accedere a ENORME quantità di dati di serie temporali e sai che devi accedervi tutti in un unico blocco enorme, puoi utilizzare una struttura che utilizzerà le Tabelle TOAST , che essenzialmente memorizza i tuoi dati in dimensioni più grandi e compresse segmenti. Questo porta ad un accesso più rapido ai dati, purché il tuo obiettivo sia quello di accedere a tutti i dati.

Un esempio di implementazione potrebbe essere

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

In questa tabella, tstartmemorizzerebbe il timestamp per la prima voce nella matrice e ogni voce successiva sarebbe il valore di una lettura per il secondo successivo. Ciò richiede di gestire il timestamp rilevante per ciascun valore di array in un software applicativo.

Un'altra possibilità è

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

dove aggiungi i valori di misurazione come coppie (chiave, valore) di (data / ora, misurazione).

Caso d'uso: questa è un'implementazione probabilmente meglio lasciata a qualcuno che è più a suo agio con PostgreSQL, e solo se sei sicuro che i tuoi schemi di accesso debbano essere schemi di accesso in blocco.

Conclusioni?

Wow, questo è molto più lungo di quanto mi aspettassi, scusa. :)

In sostanza, ci sono una serie di opzioni, ma probabilmente otterrai il massimo profitto dal tuo dollaro usando il secondo o il terzo, poiché si adattano al caso più generale.

PS: la tua domanda iniziale implicava che caricherai in massa i tuoi dati dopo che sono stati raccolti. Se stai trasmettendo i dati in streaming all'istanza di PostgreSQL, dovrai fare qualche ulteriore lavoro per gestire sia il tuo ingestione di dati che il carico di lavoro delle query, ma lo lasceremo per un'altra volta. ;)


Caspita, grazie per la risposta dettagliata, Chris!
Esaminerò l'

Buona fortuna a te!
Chris,

Caspita, voterei questa risposta 1000 volte se potessi. Grazie per la spiegazione dettagliata.
kikocorreoso,

1

È il 2019 e questa domanda merita una risposta aggiornata.

  • Se l'approccio è il migliore o no è qualcosa che ti lascerò fare un benchmark e testare, ma ecco un approccio.
  • Utilizzare un'estensione del database denominata timescaledb
  • Questa è un'estensione installata su PostgreSQL standard e gestisce diversi problemi riscontrati durante la memorizzazione di serie temporali ragionevolmente bene

Prendendo il tuo esempio, crea prima una tabella semplice in PostgreSQL

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

Passo 2

  • Trasformalo in ciò che viene chiamato hypertable nel mondo di timescaledb .
  • In parole semplici, è una tabella di grandi dimensioni che viene continuamente suddivisa in tabelle più piccole di un certo intervallo di tempo, ad esempio un giorno in cui ogni mini tabella viene definita un blocco
  • Questa mini tabella non è ovvia quando si eseguono query anche se è possibile includerla o escluderla nelle query

    SELEZIONA create_hypertable ('viaggio', 'ts', chunk_time_interval => intervallo '1 ora', if_not_exists => TRUE);

  • Quello che abbiamo fatto sopra è prendere la nostra tabella di viaggio, dividerla in mini tavoli ogni ora sulla base della colonna 'ts'. Se aggiungi un timestamp da 10:00 a 10:59, verranno aggiunti a 1 blocco, ma 11:00 verrà inserito in un nuovo blocco e questo andrà avanti all'infinito.

  • Se non si desidera archiviare i dati all'infinito, è possibile anche DROP blocchi più vecchi di 3 mesi usando

    SELECT drop_chunks (intervallo '3 mesi', 'viaggio');

  • Puoi anche ottenere un elenco di tutti i blocchi creati fino ad oggi usando una query simile

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

  • Questo ti darà un elenco di tutte le mini tabelle create fino alla data e puoi eseguire una query sull'ultima mini tabella se lo desideri da questo elenco

  • È possibile ottimizzare le query per includere, escludere blocchi o operare solo sugli ultimi N blocchi e così via

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.