Очевидно, что существует множество разных способов получения одинаковых результатов, и, похоже, ваш вопрос состоит в том, как эффективно получить последние результаты в каждой группе в MySQL. Если вы работаете с огромными объемами данных и предполагаете, что используете InnoDB даже с самыми последними версиями MySQL (такими как 5.7.21 и 8.0.4-rc), тогда не может быть эффективного способа сделать это.
Иногда нам нужно делать это с таблицами с более чем 60 миллионами строк.
В этих примерах я буду использовать данные только с примерно 1,5 миллионами строк, где запросам нужно будет найти результаты для всех групп данных. В наших реальных случаях нам часто приходилось возвращать данные примерно из 2000 групп (что гипотетически не требовало бы изучения большой части данных).
Я буду использовать следующие таблицы:
CREATE TABLE temperature(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
groupID INT UNSIGNED NOT NULL,
recordedTimestamp TIMESTAMP NOT NULL,
recordedValue INT NOT NULL,
INDEX groupIndex(groupID, recordedTimestamp),
PRIMARY KEY (id)
);
CREATE TEMPORARY TABLE selected_group(id INT UNSIGNED NOT NULL, PRIMARY KEY(id));
Таблица температур заполнена примерно 1,5 миллионами случайных записей и 100 различными группами. Selected_group заполняется этими 100 группами (в наших случаях это обычно составляет менее 20% для всех групп).
Поскольку эти данные случайны, это означает, что несколько строк могут иметь одинаковые метки времени. Нам нужно получить список всех выбранных групп в порядке groupID с последним записанным значением метки для каждой группы, и если в одной и той же группе имеется более одной совпадающей строки, то последний совпадающий идентификатор этих строк.
Если гипотетически MySQL имеет функцию last (), которая возвращает значения из последней строки в специальном предложении ORDER BY, то мы можем просто сделать:
SELECT
last(t1.id) AS id,
t1.groupID,
last(t1.recordedTimestamp) AS recordedTimestamp,
last(t1.recordedValue) AS recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
ORDER BY t1.recordedTimestamp, t1.id
GROUP BY t1.groupID;
который должен был бы изучить только несколько 100 строк в этом случае, поскольку он не использует ни одну из обычных функций GROUP BY. Это будет выполнено за 0 секунд и, следовательно, будет очень эффективным. Обратите внимание, что обычно в MySQL мы видим предложение ORDER BY, следующее за предложением GROUP BY, однако это предложение ORDER BY используется для определения ORDER для функции last (), если это было после GROUP BY, то это было бы упорядочением GROUPS. Если предложение GROUP BY отсутствует, то последние значения будут одинаковыми во всех возвращаемых строках.
Однако в MySQL этого нет, поэтому давайте рассмотрим различные идеи того, что у него есть, и докажем, что ни один из них не эффективен.
Пример 1
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
SELECT t2.id
FROM temperature t2
WHERE t2.groupID = g.id
ORDER BY t2.recordedTimestamp DESC, t2.id DESC
LIMIT 1
);
Это проверило 3 009 254 строк и заняло ~ 0,859 секунды на 5.7.21 и немного дольше на 8.0.4-rc
Пример 2
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM temperature t1
INNER JOIN (
SELECT max(t2.id) AS id
FROM temperature t2
INNER JOIN (
SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
FROM selected_group g
INNER JOIN temperature t3 ON t3.groupID = g.id
GROUP BY t3.groupID
) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
GROUP BY t2.groupID
) t5 ON t5.id = t1.id;
Это проверило 1505,331 рядов и заняло ~ 1,25 секунды на 5.7.21 и немного дольше на 8.0.4-rc
Пример 3
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM temperature t1
WHERE t1.id IN (
SELECT max(t2.id) AS id
FROM temperature t2
INNER JOIN (
SELECT t3.groupID, max(t3.recordedTimestamp) AS recordedTimestamp
FROM selected_group g
INNER JOIN temperature t3 ON t3.groupID = g.id
GROUP BY t3.groupID
) t4 ON t4.groupID = t2.groupID AND t4.recordedTimestamp = t2.recordedTimestamp
GROUP BY t2.groupID
)
ORDER BY t1.groupID;
Это проверило 3 009 685 строк и заняло ~ 1,95 секунды на 5.7.21 и немного дольше на 8.0.4-rc
Пример 4
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.id = (
SELECT max(t2.id)
FROM temperature t2
WHERE t2.groupID = g.id AND t2.recordedTimestamp = (
SELECT max(t3.recordedTimestamp)
FROM temperature t3
WHERE t3.groupID = g.id
)
);
Это проверило 6,137,810 строк и заняло ~ 2,2 секунды на 5.7.21 и немного дольше на 8.0.4-rc
Пример 5
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM (
SELECT
t2.id,
t2.groupID,
t2.recordedTimestamp,
t2.recordedValue,
row_number() OVER (
PARTITION BY t2.groupID ORDER BY t2.recordedTimestamp DESC, t2.id DESC
) AS rowNumber
FROM selected_group g
INNER JOIN temperature t2 ON t2.groupID = g.id
) t1 WHERE t1.rowNumber = 1;
Это проверило 6 017 808 строк и заняло ~ 4.2 секунды на 8.0.4-rc
Пример 6
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM (
SELECT
last_value(t2.id) OVER w AS id,
t2.groupID,
last_value(t2.recordedTimestamp) OVER w AS recordedTimestamp,
last_value(t2.recordedValue) OVER w AS recordedValue
FROM selected_group g
INNER JOIN temperature t2 ON t2.groupID = g.id
WINDOW w AS (
PARTITION BY t2.groupID
ORDER BY t2.recordedTimestamp, t2.id
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
) t1
GROUP BY t1.groupID;
Это проверило 6 017 908 строк и заняло ~ 17.5 секунд на 8.0.4-rc
Пример 7
SELECT t1.id, t1.groupID, t1.recordedTimestamp, t1.recordedValue
FROM selected_group g
INNER JOIN temperature t1 ON t1.groupID = g.id
LEFT JOIN temperature t2
ON t2.groupID = g.id
AND (
t2.recordedTimestamp > t1.recordedTimestamp
OR (t2.recordedTimestamp = t1.recordedTimestamp AND t2.id > t1.id)
)
WHERE t2.id IS NULL
ORDER BY t1.groupID;
Этот брал навсегда, поэтому мне пришлось его убить.