Как хранить данные временных рядов


22

У меня есть то, что я считаю набором данных временного ряда (пожалуйста, исправьте меня, если я ошибаюсь), у которого есть множество связанных значений.

Примером может служить моделирование автомобиля и отслеживание его различных атрибутов во время поездки. Например:

метка времени | скорость | пройденное расстояние | температура | так далее

Как лучше всего хранить эти данные, чтобы веб-приложение могло эффективно запрашивать поля, чтобы найти максимальные, минимальные значения и построить каждый набор данных с течением времени?

Я начал наивный подход с разбора дампа данных и кэширования результатов, чтобы их никогда не пришлось хранить. Однако, немного поиграв с ним, похоже, что это решение не будет масштабироваться в долгосрочной перспективе из-за ограничений памяти, и если кэш должен быть очищен, то все данные необходимо будет повторно проанализировать и повторно кэшировать.

Кроме того, предполагая, что данные отслеживаются каждую секунду с редкой возможностью набора данных более 10 часов, рекомендуется ли обрезать набор данных путем выборки каждые N секунд?

Ответы:


31

На самом деле не существует «лучшего способа» хранения данных временных рядов, и это, честно говоря, зависит от ряда факторов. Тем не менее, я собираюсь сосредоточиться в первую очередь на двух факторах:

(1) Насколько серьезен этот проект, который заслуживает ваших усилий по оптимизации схемы?

(2) Каковы ваши модели доступа запрос действительно будет похоже?

Помня об этих вопросах, давайте обсудим несколько вариантов схемы.

Плоский стол

Возможность использовать плоскую таблицу имеет гораздо больше общего с вопросом (1) , где, если это не серьезный или крупномасштабный проект, вам будет гораздо проще не слишком много думать о схеме, и просто используйте плоский стол, как:

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

Не так много случаев, когда я бы рекомендовал этот курс, только если это крошечный проект, который не требует много времени.

Размеры и факты

Таким образом, если вы устранили препятствие в вопросе (1) и хотите получить более производительную схему, это один из первых вариантов, который следует рассмотреть. Он включает некоторую базовую нормализацию, но извлекает «размерные» величины из измеренных «фактических» величин.

По сути, вы хотите таблицу для записи информации о поездках,

CREATE trips(
  trip_id integer,
  other_info text);

и таблица для записи временных меток,

CREATE tstamps(
  tstamp_id integer,
  tstamp timestamptz);

и, наконец, все ваши измеренные факты со ссылками внешнего ключа на таблицы измерений (то есть meas_facts(trip_id)ссылки trips(trip_id)и meas_facts(tstamp_id)ссылки tstamps(tstamp_id))

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

На первый взгляд может показаться, что это не очень полезно, но если у вас есть, например, тысячи одновременных поездок, то все они могут проводить измерения один раз в секунду, а во второй. В этом случае вам придется перезаписывать отметку времени каждый раз для каждой поездки, а не просто использовать одну запись в tstampsтаблице.

Вариант использования: этот вариант будет полезен, если существует много одновременных отключений, для которых вы записываете данные, и вы не против получить доступ ко всем типам измерений одновременно.

Поскольку Postgres читает по строкам, в любое время, например, когда вам нужно, например, speedизмерения в заданном временном диапазоне, вы должны прочитать всю строку из meas_factsтаблицы, что определенно замедлит запрос, хотя, если набор данных, с которым вы работаете, не слишком большой, тогда вы даже не заметите разницу.

Разделение ваших измеренных фактов

Чтобы немного расширить последний раздел, вы можете разбить ваши измерения на отдельные таблицы, где, например, я покажу таблицы скорости и расстояния:

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

а также

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

Конечно, вы можете видеть, как это может быть распространено на другие измерения.

Вариант использования: Таким образом, это не даст вам огромную скорость запроса, возможно, только линейное увеличение скорости, когда вы запрашиваете один тип измерения. Это потому, что когда вы хотите посмотреть информацию о скорости, вам нужно только прочитать строки из speed_factsтаблицы, а не всю лишнюю, ненужную информацию, которая будет присутствовать в строке meas_factsтаблицы.

Таким образом, вам нужно прочитать огромные объемы данных только об одном типе измерения, вы можете получить некоторую выгоду. С вашим предложенным случаем 10 часов данных с интервалами в одну секунду вы будете читать только 36 000 строк, поэтому вы никогда не сможете получить существенную выгоду от этого. Однако, если вам нужно было посмотреть данные измерения скорости для 5000 поездок, которые длились около 10 часов, теперь вы смотрите на чтение 180 миллионов строк. Линейное увеличение скорости для такого запроса может принести некоторую выгоду, при условии, что вам нужно только получить доступ к одному или двум типам измерений одновременно.

Массивы / Магазин / и тосты

Вам, вероятно, не нужно беспокоиться об этой части, но я знаю случаи, когда это имеет значение. Если вам нужен доступ к ОГРОМНОМУ количеству данных временных рядов, и вы знаете, что вам нужен доступ ко всем этим в одном огромном блоке, вы можете использовать структуру, которая будет использовать таблицы TOAST , которая по существу хранит ваши данные в больших сжатых данных. сегменты. Это приводит к более быстрому доступу к данным, если ваша цель - получить доступ ко всем данным.

Одним из примеров реализации может быть

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

В этой таблице tstartбудет храниться отметка времени для первой записи в массиве, и каждая последующая запись будет значением чтения для следующей секунды. Это требует от вас управления соответствующей отметкой времени для каждого значения массива в части прикладного программного обеспечения.

Другая возможность

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

где вы добавляете свои значения измерений в виде (ключ, значение) пар (метка времени, измерение).

Вариант использования: эту реализацию, вероятно, лучше оставить кому-то, кому более комфортно работать с PostgreSQL, и только если вы уверены, что ваши шаблоны доступа должны быть шаблонами массового доступа.

Выводы?

Вау, это стало намного дольше, чем я ожидал, извини. :)

По сути, есть несколько вариантов, но вы, вероятно, получите наибольшую отдачу от своих денег, используя второй или третий, поскольку они соответствуют более общему случаю.

PS: Ваш первоначальный вопрос подразумевал, что вы будете массово загружать свои данные после того, как все они будут собраны. Если вы передаете данные в ваш экземпляр PostgreSQL, вам потребуется проделать дополнительную работу, чтобы справиться как с потреблением данных, так и с рабочей нагрузкой запросов, но мы оставим это в другой раз. ;)


Вау, спасибо за подробный ответ, Крис! Я рассмотрю вариант 2 или 3.
guest82

Удачи тебе!
Крис

Ого, я бы проголосовал за этот ответ 1000 раз, если бы мог. Спасибо за подробное объяснение.
kikocorreoso

1

Его 2019 и этот вопрос заслуживает обновленного ответа.

  • Является ли подход лучшим или нет, я оставлю вас на тестирование и тестирование, но здесь есть подход.
  • Используйте расширение базы данных под названием timescaledb
  • Это расширение установлено в стандартном PostgreSQL и решает несколько проблем, возникающих при сохранении временных рядов достаточно хорошо

Используя ваш пример, сначала создайте простую таблицу в PostgreSQL

Шаг 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
) 

Шаг 2

  • Преврати это в так называемую гипертаблику в мире timescaledb.
  • Проще говоря, это большая таблица, которая непрерывно делится на меньшие таблицы некоторого временного интервала, скажем, дня, когда каждая мини-таблица называется порцией
  • Эта мини-таблица неочевидна при выполнении запросов, хотя вы можете включить или исключить ее в своих запросах

    SELECT create_hypertable ('trip', 'ts', chunk_time_interval => интервал '1 час', if_not_exists => TRUE);

  • То, что мы сделали выше, это возьмите нашу таблицу поездок, делите ее на таблицы мини-чанков каждый час на основе столбца «ts». Если вы добавите метку времени с 10:00 до 10:59, они будут добавлены к 1 чанку, но 11:00 будут вставлены в новый чанк, и это будет продолжаться бесконечно.

  • Если вы не хотите хранить данные бесконечно, вы также можете ОТБРАТЬ куски старше, чем, скажем, 3 месяца, используя

    SELECT drop_chunks (интервал «3 месяца», «поездка»);

  • Вы также можете получить список всех чанков, созданных до даты, используя запрос типа

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

  • Это даст вам список всех мини-таблиц, созданных до даты, и вы можете выполнить запрос к последней мини-таблице, если хотите из этого списка

  • Вы можете оптимизировать свои запросы для включения, исключения чанков или работы только с последними N чанками и т. Д.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.