#mysql #greatest-n-per-group
#mysql #наибольшее количество n на группу
Вопрос:
У меня есть большая таблица, содержащая данные датчиков временных рядов. Большой — это что-то от нескольких тысяч до 10 миллионов записей, разделенных между различными отслеживаемыми каналами. Для определенного типа датчика мне нужно вычислить временной интервал между текущим и предыдущим показаниями, т. е. Найти наибольшую временную метку, предшествующую текущей.
На ум приходят очевидные подходы, каждый из которых измеряется на Core i5 для канала из 40 тыс. записей:
Коррелированный подзапрос
SELECT collect.*, prev.timestamp AS prev_timestamp
FROM data AS collect
LEFT JOIN data AS prev ON prev.channel_id = collect.channel_id AND prev.timestamp = (
SELECT MAX(timestamp)
FROM data
WHERE data.channel_id = collect.channel_id AND data.timestamp < collect.timestamp
)
WHERE collect.channel_id=14 AND collect.timestamp >= 0
ORDER BY collect.timestamp
Время (exec, выборка) 11 сек, 21 сек
План
---- -------------------- --------- ------ ------------------------------ --------- --------- ------------------------- ------- --------------------------
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---- -------------------- --------- ------ ------------------------------ --------- --------- ------------------------- ------- --------------------------
| 1 | PRIMARY | collect | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | const | 45820 | Using where |
| 1 | PRIMARY | prev | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 13 | const,func | 1 | Using index |
| 2 | DEPENDENT SUBQUERY | data | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | nils.collect.channel_id | 2495 | Using where; Using index |
---- -------------------- --------- ------ ------------------------------ --------- --------- ------------------------- ------- --------------------------
Антисоединение
SELECT d1.*, d2.timestamp AS prev_timestamp
FROM data d1
LEFT JOIN data d2 ON
d2.channel_id=14 AND
d2.timestamp < d1.timestamp
LEFT JOIN data d3 ON
d3.channel_id=14 AND
d3.timestamp < d1.timestamp AND
d3.timestamp > d2.timestamp
WHERE
d3.timestamp IS NULL AND
d1.channel_id=14
ORDER BY timestamp
Время 12 секунд, 21 секунда
План
---- ------------- ------- ------ ------------------------------ --------- --------- ------- ------- --------------------------------------
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---- ------------- ------- ------ ------------------------------ --------- --------- ------- ------- --------------------------------------
| 1 | SIMPLE | d1 | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | const | 45820 | Using where |
| 1 | SIMPLE | d2 | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | const | 47194 | Using index |
| 1 | SIMPLE | d3 | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | const | 47194 | Using where; Using index; Not exists |
---- ------------- ------- ------ ------------------------------ --------- --------- ------- ------- --------------------------------------
И я придумал еще один паттерн, который я называю Наивным подсчетом
SELECT current.*, prev.timestamp AS prev_timestamp FROM
(
SELECT data.*, @r1 := @r1 1 AS rownum from data
CROSS JOIN (SELECT @r1 := 0) AS vars
WHERE channel_id=14
ORDER BY timestamp
) AS current
LEFT JOIN
(
SELECT data.*, @r2 := @r2 1 AS rownum from data
CROSS JOIN (SELECT @r2 := 0) AS vars
WHERE channel_id=14
ORDER BY timestamp
) AS prev
ON current.rownum = prev.rownum 1
Время 1.1 сек (этот на самом деле самый быстрый!)
План
---- ------------- ------------ -------- ------------------------------ --------- --------- ----- ------- ----------------
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---- ------------- ------------ -------- ------------------------------ --------- --------- ----- ------- ----------------
| 1 | PRIMARY | <derived2> | ALL | | | | | 24475 | |
| 1 | PRIMARY | <derived4> | ALL | | | | | 24475 | |
| 4 | DERIVED | <derived5> | system | | | | | 1 | |
| 4 | DERIVED | data | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | | 45820 | Using where |
| 5 | DERIVED | | | | | | | | No tables used |
| 2 | DERIVED | <derived3> | system | | | | | 1 | |
| 2 | DERIVED | data | ref | ts_uniq,IDX_ADF3F36372F5A1AA | ts_uniq | 5 | | 45820 | Using where |
| 3 | DERIVED | | | | | | | | No tables used |
---- ------------- ------------ -------- ------------------------------ --------- --------- ----- ------- ----------------
Поскольку запрос, вероятно, выполняется на небольших платформах, таких как RasPi, производительность имеет решающее значение — пара секунд является максимально приемлемой.
Мой вопрос: является ли последний подход хорошим для наибольшего n-на-группу или есть лучшие? Ожидается ли, что коррелированный подзапрос выполняется так же медленно, как и раньше?
Ответ №1:
Последний подход с переменными является разумным. Вы также можете попробовать:
SELECT collect.*,
(select max(timestamp)
from data
where data.channel_id = collect.channel_id AND data.timestamp < collect.timestamp
) AS prev_timestamp
FROM data AS collect
WHERE collect.channel_id = 14 AND collect.timestamp >= 0
ORDER BY collect.timestamp;
Кроме того, создайте индексы на: collect(channel_id, timestamp).
Редактировать:
Следующее может быть самым быстрым:
select d.*,
if(@channel_id = channel_id, @prev_timestamp, NULL) as prev_timestamp,
@channel_id := channel_id, @prev_timestamp = timestamp
from data d cross join
(select @channel_id := 0, @prev_timestamp := 0) vars
where collect.channel_id = 14 AND collect.timestamp >= 0
order by channel_id, timestamp;
Комментарии:
1. Хороший. Несмотря на то, что это также коррелированный подзапрос (это так, не так ли?), Это занимает всего 3,5 секунды.
2. Ты гений. Это невероятно быстро. И что еще более важно — она не разрушается — в отличие от других — при переносе в ГРУППУ BY. Я немного нервничаю, поскольку это зависит от порядка присвоения переменных внутри предложения SELECT . Не уверен, что это полностью стабильно / законно?
3. дох: не похоже, чтобы он был стабильным. При запуске изнутри PHP он возвращает результаты, отличные от идентичных запросов, выполняемых из MySQL. Ищу, чтобы сузить и открыть ошибку с MySQL.
4. @andig . , , MySQL явно говорит, что выражения в
select
не обязательно вычисляются по порядку, поэтому переменные не должны устанавливаться и использоваться в одном и том же выражении. Рабочая версия, использующая переменные, является более уродливой версией этой.5. Опять же — хорошо сказано. Найдено решение, обертывающее чтение vars xaprb.com/blog/2006/12/15 /…