Получение последней записи в каждой группе-MySQL
есть таблица messages
, который содержит данные, как показано ниже:
Id Name Other_Columns
-------------------------
1 A A_data_1
2 A A_data_2
3 A A_data_3
4 B B_data_1
5 B B_data_2
6 C C_data_1
если я запускаю запрос select * from messages group by name
, я получу результат в виде:
1 A A_data_1
4 B B_data_1
6 C C_data_1
какой запрос вернет следующий результат?
3 A A_data_3
5 B B_data_2
6 C C_data_1
то есть, последняя запись в каждой группе должен быть возвращен.
в настоящее время, это запрос, который я использую:
SELECT
*
FROM (SELECT
*
FROM messages
ORDER BY id DESC) AS x
GROUP BY name
но это выглядит крайне неэффективным. Есть ли другие способы достичь того же результата?
21 ответов
MySQL 8.0 теперь поддерживает оконные функции, как почти все популярные реализации SQL. С помощью этого стандартного синтаксиса мы можем писать запросы greatest-n-per-group:
WITH ranked_messages AS (
SELECT m.*, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rn
FROM messages AS m
)
SELECT * FROM ranked_messages WHERE rn = 1;
Ниже приведен оригинальный ответ, который я написал для этого вопроса в 2009 году:
Я пишу решение таким образом:
SELECT m1.*
FROM messages m1 LEFT JOIN messages m2
ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL;
что касается производительности, то одно или другое решение может быть лучше, в зависимости от характера ваших данных. Поэтому вы должны протестировать оба запроса и использовать тот, который лучше работает с вашей базой данных.
например, у меня есть копия!--23-->августовский дамп данных StackOverflow. Я использую это для сравнения. Есть 1,114,357 строк в Posts
таблица. Это работает на в MySQL 5.0.75 на моем Macbook Pro 2.40 GHz.
я напишу запрос, чтобы найти самый последний пост для данного идентификатора пользователя (мой).
сначала используя технику показали by @Эрик с GROUP BY
в подзапросе:
SELECT p1.postid
FROM Posts p1
INNER JOIN (SELECT pi.owneruserid, MAX(pi.postid) AS maxpostid
FROM Posts pi GROUP BY pi.owneruserid) p2
ON (p1.postid = p2.maxpostid)
WHERE p1.owneruserid = 20860;
1 row in set (1 min 17.89 sec)
даже EXPLAIN
анализ занимает более 16 секунд:
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 76756 | |
| 1 | PRIMARY | p1 | eq_ref | PRIMARY,PostId,OwnerUserId | PRIMARY | 8 | p2.maxpostid | 1 | Using where |
| 2 | DERIVED | pi | index | NULL | OwnerUserId | 8 | NULL | 1151268 | Using index |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
3 rows in set (16.09 sec)
Теперь создайте тот же результат запроса, используя моя техника С LEFT JOIN
:
SELECT p1.postid
FROM Posts p1 LEFT JOIN posts p2
ON (p1.owneruserid = p2.owneruserid AND p1.postid < p2.postid)
WHERE p2.postid IS NULL AND p1.owneruserid = 20860;
1 row in set (0.28 sec)
на EXPLAIN
анализ показывает, что обе таблицы могут использовать свои индексы:
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| 1 | SIMPLE | p1 | ref | OwnerUserId | OwnerUserId | 8 | const | 1384 | Using index |
| 1 | SIMPLE | p2 | ref | PRIMARY,PostId,OwnerUserId | OwnerUserId | 8 | const | 1384 | Using where; Using index; Not exists |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
2 rows in set (0.00 sec)
вот DDL для моего Posts
стол:
CREATE TABLE `posts` (
`PostId` bigint(20) unsigned NOT NULL auto_increment,
`PostTypeId` bigint(20) unsigned NOT NULL,
`AcceptedAnswerId` bigint(20) unsigned default NULL,
`ParentId` bigint(20) unsigned default NULL,
`CreationDate` datetime NOT NULL,
`Score` int(11) NOT NULL default '0',
`ViewCount` int(11) NOT NULL default '0',
`Body` text NOT NULL,
`OwnerUserId` bigint(20) unsigned NOT NULL,
`OwnerDisplayName` varchar(40) default NULL,
`LastEditorUserId` bigint(20) unsigned default NULL,
`LastEditDate` datetime default NULL,
`LastActivityDate` datetime default NULL,
`Title` varchar(250) NOT NULL default '',
`Tags` varchar(150) NOT NULL default '',
`AnswerCount` int(11) NOT NULL default '0',
`CommentCount` int(11) NOT NULL default '0',
`FavoriteCount` int(11) NOT NULL default '0',
`ClosedDate` datetime default NULL,
PRIMARY KEY (`PostId`),
UNIQUE KEY `PostId` (`PostId`),
KEY `PostTypeId` (`PostTypeId`),
KEY `AcceptedAnswerId` (`AcceptedAnswerId`),
KEY `OwnerUserId` (`OwnerUserId`),
KEY `LastEditorUserId` (`LastEditorUserId`),
KEY `ParentId` (`ParentId`),
CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`PostTypeId`) REFERENCES `posttypes` (`PostTypeId`)
) ENGINE=InnoDB;
UPD: 2017-03-31, версия 5.7.5 MySQL сделал переключатель ONLY_FULL_GROUP_BY включенным по умолчанию (следовательно, недетерминированная группа по запросам стала отключенной). Кроме того, они обновили группу по реализации, и решение может больше не работать, как ожидалось, даже с отключенным коммутатором. Нужно проверить.
решение Билла Карвина выше отлично работает, когда количество элементов в группах довольно мало, но производительность запроса становится плохо, когда группы довольно большие, так как решение требует около n*n/2 + n/2
только IS NULL
сравнения.
я сделал свои тесты на InnoDB таблице 18684446
строки 1182
группы. В таблице появится для функциональных тестов и (test_id, request_id)
в качестве первичного ключа. Таким образом, test_id
это группа, и я искал последний request_id
для каждого test_id
.
решение Билла уже работает в течение нескольких часов на моем dell e4310, и я этого не делаю знайте, когда он собирается закончить, даже если он работает с индексом покрытия (следовательно,using index
in EXPLAIN).
у меня есть несколько других решений, которые основаны на тех же идеях:
- если базовый индекс является индексом BTREE (что обычно имеет место), самый большой
(group_id, item_value)
pair-последнее значение в каждомgroup_id
, это первое для каждогоgroup_id
если мы пройдем через индекс в порядке убывания; - если мы читаем значения покрытые индексом, значения считываются в порядке следования индекса;
- каждый индекс неявно содержит столбцы первичного ключа, добавленные к этому (то есть первичный ключ находится в индексе покрытия). В приведенных ниже решениях я работаю непосредственно с первичным ключом, в вашем случае вам просто нужно будет добавить столбцы первичного ключа в результат.
- во многих случаях гораздо дешевле собрать требуемые идентификаторы строк в требуемом порядке в подзапросе и присоединиться к результату подзапроса на идентификатор. Поскольку для каждой строки в результате подзапроса MySQL потребуется одна выборка на основе первичного ключа, подзапрос будет помещен первым в соединение, и строки будут выведены в порядке идентификаторов в подзапросе (если мы опустим явный порядок для соединения)
3 способа MySQL использует индексы большая статья, чтобы понять некоторые детали.
Решение 1
это невероятно быстро, это занимает около 0,8 секунды на моих строках 18M+:
SELECT test_id, MAX(request_id), request_id
FROM testresults
GROUP BY test_id DESC;
если вы хотите изменить порядок на ASC, поместите его в подзапрос, верните только идентификаторы и используйте его в качестве подзапроса для присоединения к остальным столбцам:
SELECT test_id, request_id
FROM (
SELECT test_id, MAX(request_id), request_id
FROM testresults
GROUP BY test_id DESC) as ids
ORDER BY test_id;
это занимает около 1,2 секунд на моих данных.
решение 2
вот еще одно решение, которое занимает около 19 секунд на моем столе:
SELECT test_id, request_id
FROM testresults, (SELECT @group:=NULL) as init
WHERE IF(IFNULL(@group, -1)=@group:=test_id, 0, 1)
ORDER BY test_id DESC, request_id DESC
он также возвращает тесты в порядке убывания. Это намного медленнее, так как он выполняет полное сканирование индекса, но здесь, чтобы дать вам представление о том, как выводить N максимальных строк для каждой группы.
недостатком запроса является то, что его результат не может быть кэширован кэшем запроса.
использовать подзапрос вернуть правильную группировку, потому что вы на полпути там.
попробуйте это:
select
a.*
from
messages a
inner join
(select name, max(id) as maxid from messages group by name) as b on
a.id = b.maxid
Если это не id
вы хотите максимум:
select
a.*
from
messages a
inner join
(select name, max(other_col) as other_col
from messages group by name) as b on
a.name = b.name
and a.other_col = b.other_col
таким образом, вы избегаете коррелированных подзапросов и/или упорядочивания в своих подзапросах, которые, как правило, очень медленные/неэффективные.
Я пришел к другому решению, которое состоит в том, чтобы получить идентификаторы для последнего сообщения в каждой группе, а затем выбрать из таблицы сообщений, используя результат первого запроса в качестве аргумента для WHERE x IN
конструкция:
SELECT id, name, other_columns
FROM messages
WHERE id IN (
SELECT MAX(id)
FROM messages
GROUP BY name
);
Я не знаю, как это работает по сравнению с некоторыми другими решениями, но он работал эффектно для моей таблицы с 3+ миллионами строк. (4-секундное исполнение с 1200 + результатами)
это должно работать как на MySQL, так и на SQL Сервер.
решение по подзапросу скрипка ссылке
select * from messages where id in
(select max(id) from messages group by Name)
решение по условию соединения скрипка ссылке
select m1.* from messages m1
left outer join messages m2
on ( m1.id<m2.id and m1.name=m2.name )
where m2.id is null
причина этого сообщения-дать только ссылку на скрипку. Тот же SQL уже представлен в других ответах.
Я еще не тестировал с большой БД, но я думаю, что это может быть быстрее, чем присоединение таблиц:
SELECT *, Max(Id) FROM messages GROUP BY Name
здесь два предложения. Во-первых, если mysql поддерживает ROW_NUMBER (), это очень просто:
WITH Ranked AS (
SELECT Id, Name, OtherColumns,
ROW_NUMBER() OVER (
PARTITION BY Name
ORDER BY Id DESC
) AS rk
FROM messages
)
SELECT Id, Name, OtherColumns
FROM messages
WHERE rk = 1;
Я предполагаю, что под "последним" вы подразумеваете последний в порядке Id. Если нет, измените предложение ORDER BY окна ROW_NUMBER () соответственно. Если ROW_NUMBER () недоступен, это другое решение:
во-вторых, если это не так, это часто хороший способ продолжить:
SELECT
Id, Name, OtherColumns
FROM messages
WHERE NOT EXISTS (
SELECT * FROM messages as M2
WHERE M2.Name = messages.Name
AND M2.Id > messages.Id
)
другими словами, выберите Сообщения, где нет сообщения later-Id с тем же Имя.
вот еще один способ получить последнюю связанную запись, используя GROUP_CONCAT
с заказом и SUBSTRING_INDEX
выбрать запись из списка
SELECT
`Id`,
`Name`,
SUBSTRING_INDEX(
GROUP_CONCAT(
`Other_Columns`
ORDER BY `Id` DESC
SEPARATOR '||'
),
'||',
1
) Other_Columns
FROM
messages
GROUP BY `Name`
запрос группы все Other_Columns
в этом Name
группы и с помощью ORDER BY id DESC
все Other_Columns
в конкретной группе в порядке убывания с указанным разделителем в моем случае я использовал ||
,используя SUBSTRING_INDEX
над этим списком будет выбрать первый
Скрипка Демо
SELECT
column1,
column2
FROM
table_name
WHERE id IN
(SELECT
MAX(id)
FROM
table_name
GROUP BY column1)
ORDER BY column1 ;
попробуйте это:
SELECT jos_categories.title AS name,
joined .catid,
joined .title,
joined .introtext
FROM jos_categories
INNER JOIN (SELECT *
FROM (SELECT `title`,
catid,
`created`,
introtext
FROM `jos_content`
WHERE `sectionid` = 6
ORDER BY `id` DESC) AS yes
GROUP BY `yes`.`catid` DESC
ORDER BY `yes`.`created` DESC) AS joined
ON( joined.catid = jos_categories.id )
вы также можете посмотреть отсюда.
http://sqlfiddle.com#!9 / ef42b/9
ПЕРВОЕ РЕШЕНИЕ
SELECT d1.ID,Name,City FROM Demo_User d1
INNER JOIN
(SELECT MAX(ID) AS ID FROM Demo_User GROUP By NAME) AS P ON (d1.ID=P.ID);
ВТОРОЕ РЕШЕНИЕ
SELECT * FROM (SELECT * FROM Demo_User ORDER BY ID DESC) AS T GROUP BY NAME ;
есть ли способ использовать этот метод для удаления дубликатов в таблице? Результирующий набор-это в основном коллекция уникальных записей, поэтому, если бы мы могли удалить все записи не в результирующем наборе, у нас бы фактически не было дубликатов? Я пробовал это, но mySQL дал ошибку 1093.
DELETE FROM messages WHERE id NOT IN
(SELECT m1.id
FROM messages m1 LEFT JOIN messages m2
ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL)
есть ли способ сохранить вывод в переменную temp, а затем удалить из NOT IN (переменная temp)? @Билл, спасибо за очень полезное решение.
EDIT: думаю, я нашел решение:
DROP TABLE IF EXISTS UniqueIDs;
CREATE Temporary table UniqueIDs (id Int(11));
INSERT INTO UniqueIDs
(SELECT T1.ID FROM Table T1 LEFT JOIN Table T2 ON
(T1.Field1 = T2.Field1 AND T1.Field2 = T2.Field2 #Comparison Fields
AND T1.ID < T2.ID)
WHERE T2.ID IS NULL);
DELETE FROM Table WHERE id NOT IN (SELECT ID FROM UniqueIDs);
приведенный ниже запрос будет работать нормально, как на ваш вопрос.
SELECT M1.*
FROM MESSAGES M1,
(
SELECT SUBSTR(Others_data,1,2),MAX(Others_data) AS Max_Others_data
FROM MESSAGES
GROUP BY 1
) M2
WHERE M1.Others_data = M2.Max_Others_data
ORDER BY Others_data;
Привет @Vijay Dev, если ваша таблица сообщения содержит Id который является автоматическим приращением первичного ключа, чтобы получить последнюю запись на основе первичного ключа, ваш запрос должен прочитать, как показано ниже:
SELECT m1.* FROM messages m1 INNER JOIN (SELECT max(Id) as lastmsgId FROM messages GROUP BY Name) m2 ON m1.Id=m2.lastmsgId
если вы хотите последнюю строку для каждого Name
, то вы можете дать номер строки для каждой группы строки Name
и Id
в порядке убывания.
запрос
SELECT t1.Id,
t1.Name,
t1.Other_Columns
FROM
(
SELECT Id,
Name,
Other_Columns,
(
CASE Name WHEN @curA
THEN @curRow := @curRow + 1
ELSE @curRow := 1 AND @curA := Name END
) + 1 AS rn
FROM messages t,
(SELECT @curRow := 0, @curA := '') r
ORDER BY Name,Id DESC
)t1
WHERE t1.rn = 1
ORDER BY t1.Id;
SQL Fiddle
подход со значительной скоростью выглядит следующим образом.
SELECT *
FROM messages a
WHERE Id = (SELECT MAX(Id) FROM messages WHERE a.Name = Name)
результат
Id Name Other_Columns
3 A A_data_3
5 B B_data_2
6 C C_data_1
Как насчет этого:
SELECT DISTINCT ON (name) *
FROM messages
ORDER BY name, id DESC;
У меня была аналогичная проблема (на postgresql) и в таблице записей 1M. Это решение принимает 1.7 s против 44s произведенных одним с левым соединением. В моем случае мне пришлось фильтровать corrispondant вашего имя поле против нулевых значений, что приводит к еще лучшим показателям на 0,2 сек
очевидно, что есть много разных способов получения тех же результатов, ваш вопрос, похоже, является эффективным способом получения последних результатов в каждой группе в 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% для всех групп).
поскольку эти данные случайны, это означает, что несколько строк могут иметь одинаковые метки recordedTimestamps. Мы хотим получить список всех выбранных групп в порядке groupID с последней меткой recordedTimestamp для каждой группы, и если одна и та же группа имеет более одной соответствующей строки, то последний соответствующий идентификатор этих строк.
если гипотетически MySQL имел функцию last (), которая возвращала значения из последней строки в специальном порядке по предложению, тогда мы могли просто сделать:
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 используется для определения порядка для функции last (), если она была после группы к тому времени, она будет упорядочивать группы. Если предложение 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;
это исследовало 1,505,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;
это заняло целую вечность, поэтому мне пришлось убить его.
Если производительность действительно ваша забота, вы можете ввести новый столбец в таблице под названием IsLastInGroup
типа BIT.
установите значение true для столбцов, которые являются последними, и поддерживайте его с каждой строкой insert/update/delete. Запись будет медленнее, но вы выиграете от чтения. Это зависит от вашего варианта использования, и я рекомендую его, только если вы сосредоточены на чтении.
поэтому запрос будет выглядеть так:
SELECT * FROM Messages WHERE IsLastInGroup = 1