Cómo almacenar datos de series temporales


22

Tengo lo que creo que es un conjunto de datos de series temporales (corríjame si me equivoco) que tiene un montón de valores asociados.

Un ejemplo sería modelar un automóvil y rastrear sus diversos atributos durante un viaje. Por ejemplo:

marca de tiempo | velocidad | distancia recorrida | temperatura | etc.

¿Cuál sería la mejor manera de almacenar estos datos para que una aplicación web pueda consultar eficientemente los campos para encontrar max, mins y trazar cada conjunto de datos a lo largo del tiempo?

Comencé un enfoque ingenuo de analizar el volcado de datos y el almacenamiento en caché de los resultados para que nunca tengan que almacenarse. Sin embargo, después de jugar un poco con él, parece que esta solución no se escalará a largo plazo debido a restricciones de memoria y si la memoria caché se borrara, entonces todos los datos tendrían que volver a analizarse y almacenarse en caché.

Además, suponiendo que los datos se rastrean cada segundo con la rara posibilidad de conjuntos de datos de más de 10 horas, ¿se recomienda generalmente truncar el conjunto de datos muestreando cada N segundos?

Respuestas:


31

Realmente no hay una 'mejor manera' para almacenar datos de series de tiempo, y honestamente depende de una serie de factores. Sin embargo, me enfocaré principalmente en dos factores, siendo ellos:

(1) ¿Qué tan serio es este proyecto que merece su esfuerzo para optimizar el esquema?

(2) ¿Cómo serán realmente sus patrones de acceso a consultas ?

Con esas preguntas en mente, analicemos algunas opciones de esquema.

Mesa plana

La opción de usar una tabla plana tiene mucho más que ver con la pregunta (1) , donde si este no es un proyecto serio o de gran escala, será mucho más fácil no pensar demasiado en el esquema, y solo use una mesa plana, como:

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

No hay muchos casos en los que recomiende este curso, solo si se trata de un pequeño proyecto que no garantiza gran parte de su tiempo.

Dimensiones y hechos

Entonces, si ha superado el obstáculo de la pregunta (1) y desea un esquema de rendimiento más, esta es una de las primeras opciones a considerar. Incluye algo de normailización básica, pero extrae las cantidades 'dimensionales' de las cantidades 'factuales' medidas.

Esencialmente, querrás una tabla para registrar información sobre los viajes,

CREATE trips(
  trip_id integer,
  other_info text);

y una tabla para registrar marcas de tiempo,

CREATE tstamps(
  tstamp_id integer,
  tstamp timestamptz);

y finalmente todos sus hechos medidos, con referencias de claves externas a las tablas de dimensiones (es decir, meas_facts(trip_id)referencias trips(trip_id)y meas_facts(tstamp_id)referencias tstamps(tstamp_id))

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

Puede que esto no parezca tan útil al principio, pero si tiene, por ejemplo, miles de viajes simultáneos, entonces todos pueden estar tomando mediciones una vez por segundo, en el segundo. En ese caso, tendría que volver a grabar la marca de tiempo cada vez para cada viaje, en lugar de usar una sola entrada en la tstampstabla.

Caso de uso: este caso será bueno si hay muchos viajes simultáneos para los que está grabando datos, y no le importa acceder a todos los tipos de medición todos juntos.

Dado que Postgres lee por filas, cada vez que desee, por ejemplo, las speedmediciones en un rango de tiempo determinado, debe leer toda la fila de la meas_factstabla, lo que definitivamente ralentizará una consulta, aunque si el conjunto de datos con el que está trabajando es no demasiado grande, entonces ni siquiera notarías la diferencia.

Dividiendo sus hechos medidos

Para extender la última sección un poco más, puede dividir sus medidas en tablas separadas, donde, por ejemplo, mostraré las tablas de velocidad y distancia:

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

y

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

Por supuesto, puede ver cómo esto podría extenderse a las otras mediciones.

Caso de uso: por lo tanto, esto no le dará una velocidad tremenda para una consulta, tal vez solo un aumento lineal de la velocidad cuando realice consultas sobre un tipo de medición. Esto se debe a que cuando desea buscar información sobre la velocidad, solo necesita leer las filas de la speed_factstabla, en lugar de toda la información adicional innecesaria que estaría presente en una fila de la meas_factstabla.

Por lo tanto, necesita leer grandes cantidades de datos sobre un solo tipo de medición, podría obtener algún beneficio. Con su caso propuesto de 10 horas de datos a intervalos de un segundo, solo estaría leyendo 36,000 filas, por lo que nunca encontraría un beneficio significativo al hacer esto. Sin embargo, si estuviera buscando datos de medición de velocidad para 5,000 viajes que duraron alrededor de 10 horas, ahora está leyendo 180 millones de filas. Un aumento lineal de la velocidad para una consulta de este tipo podría generar algún beneficio, siempre que solo necesite acceder a uno o dos de los tipos de medición a la vez.

Matrices / HStore / & TOAST

Probablemente no necesite preocuparse por esta parte, pero sé de casos en los que sí importa. Si necesita acceder a ENORMES cantidades de datos de series temporales, y sabe que necesita acceder a todos ellos en un bloque enorme, puede usar una estructura que utilizará las Tablas TOAST , que esencialmente almacenan sus datos en archivos más grandes y comprimidos. segmentos Esto conduce a un acceso más rápido a los datos, siempre que su objetivo sea acceder a todos los datos.

Un ejemplo de implementación podría ser

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

En esta tabla, tstartalmacenaría la marca de tiempo para la primera entrada en la matriz, y cada entrada posterior sería el valor de una lectura para el siguiente segundo. Esto requiere que administre la marca de tiempo relevante para cada valor de matriz en una pieza de software de aplicación.

Otra posibilidad es

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

donde agrega sus valores de medición como (clave, valor) pares de (marca de tiempo, medición).

Caso de uso: esta implementación probablemente sea mejor para alguien que se sienta más cómodo con PostgreSQL, y solo si está seguro de que sus patrones de acceso deben ser patrones de acceso masivo.

Conclusiones?

Wow, esto se hizo mucho más de lo que esperaba, lo siento. :)

Esencialmente, hay una serie de opciones, pero probablemente obtendrá el mayor rendimiento de su inversión utilizando el segundo o el tercero, ya que se ajustan al caso más general.

PD: Su pregunta inicial implicaba que cargará en masa sus datos después de que se hayan recopilado. Si está transmitiendo los datos a su instancia de PostgreSQL, tendrá que hacer un trabajo adicional para manejar tanto la ingesta de datos como la carga de trabajo de consulta, pero lo dejaremos para otro momento. ;)


¡Guau, gracias por la respuesta detallada, Chris!
Examinaré el

¡Buena suerte para ti!
Chris

Vaya, votaría esta respuesta 1000 veces si pudiera. Gracias por la explicación detallada.
kikocorreoso

1

Es 2019 y esta pregunta merece una respuesta actualizada.

  • Si el enfoque es el mejor o no, es algo que te dejaré para evaluar y probar, pero aquí hay un enfoque.
  • Use una extensión de base de datos llamada timescaledb
  • Esta es una extensión instalada en PostgreSQL estándar y maneja varios problemas encontrados mientras almacena series de tiempo razonablemente bien

Tomando su ejemplo, primero cree una tabla simple en PostgreSQL

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

Paso 2

  • Convierta esto en lo que se llama un hipertable en el mundo de timecaledb.
  • En palabras simples, es una tabla grande que se divide continuamente en tablas más pequeñas de algún intervalo de tiempo, digamos un día donde cada mini tabla se conoce como un fragmento
  • Esta mini tabla no es obvia cuando ejecuta consultas, aunque puede incluirla o excluirla en sus consultas

    SELECCIONE create_hypertable ('viaje', 'ts', chunk_time_interval => intervalo '1 hora', if_not_exists => TRUE);

  • Lo que hemos hecho anteriormente es tomar nuestra tabla de viaje, dividirla en mini tablas de trozos cada hora en función de la columna 'ts'. Si agrega una marca de tiempo de 10:00 a 10:59, se agregarán a 1 fragmento, pero las 11:00 se insertarán en un nuevo fragmento y esto continuará infinitamente.

  • Si no desea almacenar datos infinitamente, también puede DROP trozos anteriores a 3 meses usando

    SELECCIONE drop_chunks (intervalo '3 meses', 'viaje');

  • También puede obtener una lista de todos los fragmentos creados hasta la fecha mediante una consulta como

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

  • Esto le dará una lista de todas las mini tablas creadas hasta la fecha y puede ejecutar una consulta en la última mini tabla si lo desea de esta lista

  • Puede optimizar sus consultas para incluir, excluir fragmentos u operar solo en los últimos N fragmentos, etc.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.