Агрегирование данных временных рядов по нескольким осям? - PullRequest
1 голос
/ 01 апреля 2019

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

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 при фильтрации по TIMESTAMP или значению.

Ответы [ 3 ]

1 голос
/ 07 апреля 2019

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

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

означает, что вы не пишете данные, которые пропускают "окно" в прошлом , например. допустим вчера.

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

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

  • Получить агрегированные данные из таблицы временных рядов, где TIMESTAMP находится между startTime - до конца дня startTime (2019-03-03 12: 00: 00,2019-03-04 00:00:00)
  • Получение агрегированных данных из материализованного представления за дни между (2019-03-04,2019-04-01)
  • Получить агрегированные данные из таблицы временных рядов, где TIMESTAMP находится между startTime - до конца дня startTime (2019-04-02 00: 00: 00,2019-04-02 12:00:00)
  • Наконец, объедините значения выше, используя объединение всех .

enter image description here

Допустим, таблицы 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 голос
/ 09 апреля 2019

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 и т. Д.

Если вам нужно 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) в настоящее время выполняет сканирование таблицы большой таблицы Raw, даже если вы индексировали ts. С моей предлагаемой сводной таблицей запрос будет представлять собой таблицу из сводной таблицы. Поскольку это может быть в 10 раз меньше, сканирование таблицы будет значительно быстрее.

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

0 голосов
/ 03 апреля 2019

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

Однако материализованное представление также может быть построено по одной части за раз , что оказывается отличным решением этой проблемы как для баз данных 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 об изменении.

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

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

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

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

...