Агрегирование данных временных рядов по нескольким осям?

#database-design #time-series #mariadb #rdbms

#проектирование базы данных #временные ряды #mariadb #rdbms

Вопрос:

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

 X, Y, value, TIMESTAMP
  

Первоначально они хранились в MariaDB, но размер таблицы растет слишком быстро. Выполнение простых агрегированных запросов (например SUM() ) занимает слишком много времени даже на сервере среднего размера с индексами.

Вот несколько примеров запросов:

 SELECT COUNT(*) FROM tbl 
WHERE X = 23 AND Y = 46 AND TIMESTAMP > NOW() - INTERVAL 30 DAY

SELECT X, Y, COUNT(*) FROM tbl
WHERE TIMESTAMP > NOW() - INTERVAL 30 DAY
GROUP BY X, Y
ORDER BY COUNT(*) DESC
  

У меня есть два индекса:

 X, Y, value
X, Y, TIMESTAMP
  

Я ищу рекомендации о способах (или новых базах данных) хранения этих данных, обеспечивающих быстрый поиск любой комбинации X и Y при фильтрации по временной метке или значению.

Комментарии:

1. Ну, вы можете выбрать имитацию материализованного представления в MySQL / MariaDB .. Это то, как вы бы имитировали / эмулировали это в MySQL / MariaDB.

2. Для материализованного представления требуется длительный запрос, такой же, как для исходного кода, описанного выше. Выполнение этих данных при каждой вставке или даже раз в день далеко не идеально и в конечном итоге будет слишком медленным по мере роста базы данных. Я ищу менее ресурсоемкие способы агрегирования данных. Возможно, поможет поиск других систем хранения данных или сочетание уровня кэширования MariaDB / MySQL.

3. Очень верно, все еще ожидающий MariaDB / MySQL поддерживает материализованный вид изначально, потому что это стандарт SQL 2003.. Также MariaDB поддерживает таблицы с системными версиями , которые могут быть оптимизированы для запроса информации на основе временных меток? Также можете ли вы опубликовать SHOW CREATE TABLE table инструкции для обеих таблиц и EXPLAIN query выходные данные для обоих запросов?

4. .. также на ум приходит разделение .. Кроме того, ORDER BY COUNT(*) DESC что всегда происходит медленно, потому что вы сортируете по вычисляемому столбцу, вы не можете оптимизировать это

Ответ №1:

Основываясь на вашем ответе об использовании материализованных представлений для ваших запросов, можно внести улучшение, если:

Данные временных рядов записываются в базу данных «в реальном времени»

это означает, что вы не записываете данные, которые проходили «окно» в прошлом, например. давайте предположим, что вчера.

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

Идея заключается в том, что когда запрос выполняется между определенными датами, например, startTime = 2019-03-03 12:00:00 -> EndTime = 2019-04-02 12:00:00:

  • Получите агрегированные данные из таблицы временных рядов, где ВРЕМЕННАЯ МЕТКА находится между временем начала — до конца дня времени начала (2019-03-03 12:00:00,2019-03-04 00:00:00)
  • Получение агрегированных данных из материализованного представления за несколько дней между (2019-03-04, 2019-04-01)
  • Получите агрегированные данные из таблицы временных рядов, где ВРЕМЕННАЯ МЕТКА находится между временем начала — до конца дня времени начала (2019-04-02 00:00:00,2019-04-02 12:00:00)
  • Наконец, объедините приведенные выше значения, используя union all.

введите описание изображения здесь

Предположим, что таблицы data и AggData :

 CREATE TABLE `data` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `X` varchar(32) NOT NULL,
 `Y` varchar(32) NOT NULL,
 `value` float(10,2) NOT NULL,
 `TIMESTAMP` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`)
);

CREATE TABLE `AggData` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `X` varchar(32) NOT NULL,
 `Y` varchar(32) NOT NULL,
 `DAY` date NOT NULL,
 `sum1` float NOT NULL,
 PRIMARY KEY (`id`)
)
  

Вы можете объединить данные, используя эту процедуру:

 CREATE DEFINER=`root`@`localhost` PROCEDURE `getDataForPeriods`(IN `startTime` INT(32), IN `endTime` INT(32), OUT `AggSum1` FLOAT)
    NO SQL
BEGIN
SELECT SUM(allData.summed1) INTO AggSum1
FROM (SELECT SUM(d1.value) AS summed1,d1.X AS X,d1.Y AS Y FROM `data` d1
WHERE UNIX_TIMESTAMP(d1.`TIMESTAMP`) > startTime
AND UNIX_TIMESTAMP(d1.`TIMESTAMP`) <  UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(startTime   24*60*60)))
GROUP BY d1.X,d1.Y
      UNION ALL
SELECT SUM(s1.`sum1`) AS summed1,s1.X AS X,s1.Y AS Y FROM AggData s1
WHERE UNIX_TIMESTAMP(s1.DAY) > startTime 
AND UNIX_TIMESTAMP(s1.DAY)   24*60*60 < endTime
GROUP BY s1.X,s1.Y
     UNION ALL
     SELECT SUM(d2.value) AS summed1,d2.X AS X,d2.Y AS Y FROM `data` d2
WHERE UNIX_TIMESTAMP(d2.`TIMESTAMP`) > UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(endTime)))
AND UNIX_TIMESTAMP(d2.`TIMESTAMP`) < endTime
GROUP BY d2.X,d2.Y) allData
GROUP BY allData.X,allData.Y;
END
  

Рассматривая условие WHERE TIMESTAMP > NOW() - INTERVAL 30 DAY , это было бы улучшением для условий, подобных этому, поскольку:

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

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

Комментарии:

1. Менее частые обновления для материализованной таблицы — отличная идея. Однако, в зависимости от ежедневного объема, это может подойти не всем. Вы не затронули фактическое поддержание / обновление материализованной таблицы, но в моем ответе я говорил о способах постепенного построения одной из них, которая должна работать для еще больших наборов данных.

Ответ №2:

MySQL и MariaDB не обладают необходимой спецификой, но сводные таблицы — это правильный путь. Но сначала…

 mysql> SELECT NOW() - INTERVAL 30 DAY;
 ------------------------- 
| NOW() - INTERVAL 30 DAY |
 ------------------------- 
| 2019-03-10 11:48:24     |
 ------------------------- 
  

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

 WHERE ts >= CURDATE() - INTERVAL 30 DAY
  AND ts  < CURDATE();

mysql> SELECT CURDATE() - INTERVAL 30 DAY, CURDATE();
 ----------------------------- ------------ 
| CURDATE() - INTERVAL 30 DAY | CURDATE()  |
 ----------------------------- ------------ 
| 2019-03-10                  | 2019-04-09 |
 ----------------------------- ------------ 
1 row in set (0.00 sec)
  

или даже учитывать месяцы переменной длины:

 WHERE ts >= CURDATE() - INTERVAL 1 MONTH
  AND ts  < CURDATE();

mysql> SELECT CURDATE() - INTERVAL 1 MONTH, CURDATE();
 ------------------------------ ------------ 
| CURDATE() - INTERVAL 1 MONTH | CURDATE()  |
 ------------------------------ ------------ 
| 2019-03-09                   | 2019-04-09 |
 ------------------------------ ------------ 
  

Если вы готовы рассматривать только целые дни, создание и ведение сводной таблицы (а-ля материализованный вид) легко и очень эффективно:

 CREATE TABLE SummaryXY (
    x ...,
    y ...,
    dy DATE,
    ct INT UNSIGNED,
    PRIMARY KEY(x,y,dy)
) ENGINE=InnoDB;
  

У вас была бы задача добавлять новые строки сразу после полуночи каждую высоту.

Если, с другой стороны, вам нужно перейти к текущей секунде, обновление может осуществляться через IODKU ( INSERT ... ON DUPLICATE KEY UPDATE... ), который позаботится об обновлении или вставке по мере необходимости.

Если вам нужно вернуться к часам вместо дней, измените dy . Но если вам действительно нужно вернуться к произвольной секунде, то выполните задачу в 2 шага:

 SELECT
    ( SELECT COUNT(*) FROM RawData WHERE ... (the partial day 30 days ago) )  
    ( SELECT SUM(ct) FROM SummaryXY WHERE ... (the 30 full days) );
  

(И обрабатывайте часть текущего дня либо с помощью IODKU, либо аналогичного SELECT COUNT(*) FROM RawDATA .)

Был ли ваш простой пример таким сложным, как вам нужно? То, что я описываю, будет хорошо работать для X=constant AND y=constant AND ts... , но не для X>constant etc.

Если вам нужно AVG(value) , то сохраните COUNT(*) (как указано выше) и SUM(VALUE) . Тогда это дает вам среднее:

 SUM(value_sum) / SUM(ct)
  

Если вам также нужно WHERE x=1 AND w=2 AND ts... , то создайте вторую сводную таблицу на основе x,w,ts .

Если вам также нужно WHERE x=1 AND y=1 AND z=3 AND ts... , то создайте сводную таблицу на основе x,y,z,ts , но используйте ее для x,y,ts . Возможно, типичным является 5 сводных таблиц, обрабатывающих 40 случаев.

Подробнее о сводных таблицах:http://mysql.rjweb.org/doc.php/summarytables

Ваш второй запрос ( GROUP BY X, Y ORDER BY COUNT(*) DESC ) в настоящее время выполняет сканирование таблицы большой необработанной таблицы, даже если вы индексируете ts . С предложенной мной сводной таблицей запрос будет представлять собой таблицу can сводной таблицы. Поскольку это может быть в 10 раз меньше, сканирование таблицы будет значительно быстрее.

Дополнительная сортировка COUNT(*) является незначительной нагрузкой; это зависит от количества строк в результирующем наборе.

Комментарии:

1. В настоящее время я фактически использую сводную таблицу. Однако ежедневное их создание сопряжено с трудностями, и концепция требует некоторой дополнительной предусмотрительности для распределения нагрузки. То же самое верно при использовании хранилища NoSQL (см. Мой ответ ниже).

2. @Xeoncross — Предоставьте более подробную информацию; у меня могут быть дополнительные советы. (Начните новый вопрос с большей конкретики.) Быстрое предположение: обновляйте сводную таблицу каждый час, а не каждый день. Также смотрите Советы по моей ссылке.

Ответ №3:

Раймонд Найланд опубликовал рекомендацию использовать материализованное представление (таблицу, созданную на основе запросов к другим таблицам). Сначала я отклонил это, потому что запрос, который я в настоящее время использовал для построения материализованного представления, требовал (почти) полного сканирования таблицы для выполнения вычисления, чего я и пытался избежать.

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

RDBMS

Если поступила вставка для оси X и Y , извлеките только записи с осями X и Y и повторно запустите вычисление для них. В моем случае это отлично работает, потому что частота ежедневных вставок на пару осей очень низкая (хотя все вставки пары осей высоки).

Когда:

 INSERT X, Y, value, TIMESTAMP
  

Затем запустите:

 INSERT INTO reports (X, Y, cnt, updated_at, ...) 
SELECT X, Y, COUNT(*), NOW(), ...(other columns)... FROM tbl 
WHERE X = ? AND Y = ? AND TIMESTAMP BETWEEEN ? AND ?)
  

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

Если есть оси, которые обновляются не очень часто, вы можете запустить вторую фоновую задачу для идентификации и удаления / обновления строк WHERE updated_at < NOW() - INTERVAL 1 DAY .

Redis

Атомарные счетчики — очень полезный способ вести совокупный счет для входящих показателей. После каждой вставки просто обновляйте отдельный счетчик составных ключей для нужной вам оси:

 redis> SET X#Y#2020-01-01 1
"OK"
redis> INCR X#Y#2020-01-01
(integer) 2
  

Это сложнее для данных по нескольким осям.

DynamoDB, MongoDB и т.д…

  • В AWS DynamoDB есть «потоки», которые предоставляют способ уведомления функции AWS Lambda об изменении.

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

В обоих случаях вы можете выполнить фоновое сопоставление / уменьшение данных и обновить вычисление на основе отсканированных данных.

Часто это намного дороже операции, чем то, что выполняется с меньшими наборами данных, которые помещаются в память (Redis) или RDMBS (выше).

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