Получение последней записи в каждой группе - 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
Но это выглядит крайне неэффективно. Есть ли другие способы достижения того же результата?
37 ответов
MySQL 8.0 теперь поддерживает оконные функции, как почти все популярные реализации SQL. С помощью этого стандартного синтаксиса мы можем писать запросы с наибольшим числом групп:
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;
Что касается производительности, то одно или другое решение может быть лучше в зависимости от характера ваших данных. Таким образом, вы должны протестировать оба запроса и использовать тот, который лучше работает с учетом вашей базы данных.
Например, у меня есть копия дампа данных Stackru в августе. Я буду использовать это для сравнительного анализа. Есть 1114 357 строк в Posts
Таблица. Это работает на MySQL 5.0.75 на моем Macbook Pro 2,40 ГГц.
Я напишу запрос, чтобы найти самый последний пост для данного идентификатора пользователя (мой).
Сначала используйте технику, показанную @Eric с 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, версия MySQL 5.7.5 сделала переключатель ONLY_FULL_GROUP_BY включенным по умолчанию (следовательно, недетерминированные запросы GROUP BY стали отключены). Более того, они обновили реализацию 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
в объяснении).
У меня есть несколько других решений, основанных на тех же идеях:
- если базовым индексом является индекс BTREE (который обычно имеет место), наибольшим
(group_id, item_value)
пара является последним значением в каждомgroup_id
это первый для каждогоgroup_id
если мы пройдемся по указателю в порядке убывания; - если мы читаем значения, охватываемые индексом, значения читаются в порядке индекса;
- каждый индекс неявно содержит столбцы первичного ключа, добавленные к нему (то есть первичный ключ находится в индексе покрытия). В приведенных ниже решениях я работаю непосредственно с первичным ключом, в вашем случае вам просто нужно добавить столбцы первичного ключа в результат.
- во многих случаях гораздо дешевле собрать требуемые идентификаторы строк в нужном порядке в подзапросе и присоединить результат подзапроса к идентификатору. Поскольку для каждой строки в результате подзапроса MySQL потребуется отдельная выборка на основе первичного ключа, подзапрос будет помещен первым в объединении, а строки будут выведены в порядке идентификаторов в подзапросе (если мы опускаем явное ORDER BY для объединения)
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 max строк для каждой группы.
Недостатком запроса является то, что его результат не может быть кэширован кешем запроса.
Используйте свой подзапрос, чтобы вернуть правильную группировку, потому что вы на полпути.
Попробуй это:
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 Server.
Решение по скрипте подзапроса
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 уже предоставлен в других ответах.
Мы рассмотрим, как вы можете использовать MySQL для получения последней записи в группе записей. Например, если у вас есть этот набор результатов сообщений.
id category_id post_title
1 1 Title 1
2 1 Title 2
3 1 Title 3
4 2 Title 4
5 2 Title 5
6 3 Title 6
Я хочу иметь возможность получать последние сообщения в каждой категории: "Заголовок 3", "Заголовок 5" и "Заголовок 6.". Чтобы получить посты по категориям, вы будете использовать клавиатуру MySQL Group By.
select * from posts group by category_id
Но результаты, которые мы получаем от этого запроса, таковы.
id category_id post_title
1 1 Title 1
4 2 Title 4
6 3 Title 6
Группировка по всегда возвращает первую запись в группе в наборе результатов.
SELECT id, category_id, post_title
FROM posts
WHERE id IN (
SELECT MAX(id)
FROM posts
GROUP BY category_id
);
Это вернет сообщения с самыми высокими идентификаторами в каждой группе.
id category_id post_title
3 1 Title 3
5 2 Title 5
6 3 Title 6
Подход со значительной скоростью заключается в следующем.
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
Вот два предложения. Во-первых, если 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
)
Другими словами, выберите сообщения, в которых нет сообщения с более поздним идентификатором с таким же именем.
Очевидно, что существует много разных способов получения одинаковых результатов, и, похоже, ваш вопрос состоит в том, как эффективно получить последние результаты в каждой группе в 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;
Этот брал навсегда, поэтому мне пришлось его убить.
Вот еще один способ получить последнюю связанную запись, используя 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
по этому списку выберу первый
Скрипка Демо
Здравствуйте, @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
Я еще не тестировал большие БД, но думаю, что это может быть быстрее, чем объединение таблиц:
SELECT *, Max(Id) FROM messages GROUP BY Name
Если вам нужна самая последняя или самая старая запись текстового столбца в сгруппированном запросе, и вы не хотите использовать подзапрос, вы можете сделать это ...
Бывший. У вас есть список фильмов, и вам нужно получить счет в сериале и в последнем фильме.
MAX вернет строку с наивысшим значением, поэтому, объединив идентификатор с именем, вы теперь получите самую новую запись, а затем просто удалите идентификатор для вашего окончательного результата.
Более эффективно, чем использование подзапроса.
Итак, для данного примера:
SELECT MAX(Id), Name, SUBSTRING(MAX(CONCAT(Id, Other_Columns)), LENGTH(Id) + 1),
FROM messages
GROUP BY Name
Удачного кодирования и «Да пребудет с тобой сила» :)
SELECT
column1,
column2
FROM
table_name
WHERE id IN
(SELECT
MAX(id)
FROM
table_name
GROUP BY column1)
ORDER BY column1 ;
Вы также можете посмотреть отсюда.
ПЕРВЫЕ РЕШЕНИЯ
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 ;
**
Привет, этот запрос может помочь:
**
SELECT
*
FROM
message
WHERE
`Id` IN (
SELECT
MAX(`Id`)
FROM
message
GROUP BY
`Name`
)
ORDER BY
`Id` DESC
Вот мое решение:
SELECT
DISTINCT NAME,
MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES
FROM MESSAGE;
SELECT * FROM table_name WHERE primary_key IN (SELECT MAX(primary_key) FROM table_name GROUP BY column_name )
Попробуй это:
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 )
Я нахожу лучшее решение в https://dzone.com/articles/get-last-record-in-each-mysql-group
select * from `data` where `id` in (select max(`id`) from `data` group by `name_id`)
Есть ли способ, которым мы могли бы использовать этот метод для удаления дубликатов в таблице? Результирующий набор в основном представляет собой набор уникальных записей, поэтому, если бы мы могли удалить все записи, не входящие в результирующий набор, у нас фактически не было бы дубликатов? Я пробовал это, но 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)
Есть ли способ сохранить вывод во временную переменную, а затем удалить из NOT IN (временная переменная)? @ Билл, спасибо за очень полезное решение.
РЕДАКТИРОВАТЬ: Думаю, я нашел решение:
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);
В поисках универсального groupwise-max я видел много ответов и сообщений в блогах на эту тему. Даже мой любимый (на самом деле часть фантастической серии на эту тему) не смог найти портативное решение, вместо этого углубившись в особенности RDMBS.
К счастью, портативное решение существует!
Вторичный индекс, который вам нужен для этого, — . (будет идентично, поскольку первичный ключ всегда включается неявно.)
Создавайте группы изmessage
s и используйте зависимый подзапрос, чтобы получить последнюю строку для каждой группы.
SELECT m.*
-- Step 1: Start by obtaining the groupwise maximums
FROM
(
SELECT (
-- Step 1b: Find the ID of the group maximum by seeking in the index
SELECT id
FROM messages m
WHERE m.name = groups.name
ORDER BY m.name DESC, m.id DESC -- Match the index EXACTLY, and indicate direction
LIMIT 1
) AS id
-- Step 1a: Find the groups by seeking through the index
FROM messages AS groups
GROUP BY groups.name
) AS maxes
-- Step 2: For each group, join the max row by ID
-- This neatly separates any potential followup SQL from the groupwise-max tactics
INNER JOIN messages m ON m.id = maxes.id
;
Это переносимый вариант, поскольку для него требуется только следующая комбинация строительных блоков:
- Индексировано
GROUP BY
. - Индексировано
SELECT
сORDER BY [ASC/DESC]
иLIMIT/TOP
. - Зависимые подзапросы.
Просто убедитесь, что у вас правильный индекс:GroupKeyColumn(s), GroupWinnerColumn(s), PrimaryKeyColumn(s)
.
В случае OP групповой ключname
, победитель группы определяетсяid
, и первичный ключ уже охвачен этим, поэтому:name, id
.
Многие предлагали решения, связанные с подзапросами, но наиболее упущенным из виду аспектом является очень специфический набор предложений упорядочивания, который заставляет использовать правильный индекс - не меньше, в правильном направлении обхода.
Дополнительные преимущества
- Легко настраивается для минимального () и максимального ().
- Победитель в группе может быть составным, например
timestamp, id
. (Это также позволяет нам устранить неоднозначность среди неуникальных победителей, таких как «последняя временная метка».) - Групповой ключ может быть составным, например
company_id, department_name
. - Легко расширяется за счет выбора групп.
- Легко расширяется с помощью
WHERE
о том, какие элементы игнорировать, оба индексируются (id >= 1000
) и неиндексированные (is_deleted = 0
).
Почему это работает [оптимально]?
Представьте себе, что вы листаете физическую телефонную книгу и находите последнюю запись для каждого города, то есть запись с самым большим названием этого города в алфавитном порядке. Как бы вы это сделали?
Вы бы начали с самого конца. Самая последняя запись в книге — это групповой максимум последнего города. Это первая строка результата, с которой вы сталкиваетесь.
Для каждой последующей строки желаемого результата вы будете выполнять двоичный поиск назад, до следующего по величине города. В тот момент, когда текущий город переходит в свой предшественник, находится последняя строка предшественника (наибольшее имя в алфавитном порядке), т. е. ваша следующая строка результата. Повторяйте, пока городов больше не останется.
Грубо говоря, телефонная книга похожа на вторичный указатель на{ Town, Name, PhoneNumber }
, сPhoneNumber
выступающий в качестве первичного ключа. (Ради аргументации я упрощаю ситуацию, притворяясь, что номера телефонов закреплены за одним человеком, а имена образуют один столбец.)
Вы фактически выполняете обратный поиск по индексу. Благодаря многократному эффективному переходу к следующему городу (благодаря двоичному поиску или структуре B-дерева) работа ограничивается количеством строк результатов , а не общим количеством строк. Это асимптотически оптимально. А благодаря обратному направлению обхода каждый город, с которым вы сталкиваетесь, «начинается» с самого большого ряда — вашей цели. Это важно: представьте себе абсурдный объем ненужной работы, если вам придется сканировать все строки в поисках города.
Изменение решения на групповой минимум так же тривиально, как и изменение направления обхода, т.е.DESC
кASC
.
Примечания к СУРБД
- В то время как MySQL 8 правильно показывает
Using index
в этом отношении MySQL 5.7 демонстрирует тревожныеUsing where; Using index
, но на самом деле он работает правильно. (Протестировано на огромном наборе данных с участием очень больших групп. Десятки тысяч результатов, распределенных по сотням миллионов записей, были получены примерно за 3 секунды.) - Для SQL Server синтаксис такой:
SELECT TOP 1
вместоSELECT ... LIMIT 1
.
MariaDB 10.3 и новее с использованием GROUP_CONCAT.
Идея состоит в том, чтобы использовать
ORDER BY
+
LIMIT
:
SELECT GROUP_CONCAT(id ORDER BY id DESC LIMIT 1) AS id,
name,
GROUP_CONCAT(Other_columns ORDER BY id DESC LIMIT 1) AS Other_columns
FROM t
GROUP BY name;
Если производительность действительно важна, вы можете ввести в таблицу новый столбец под названием IsLastInGroup
типа БИТ.
Установите значение true в столбцах, которые являются последними, и сохраняйте его для каждой строки вставки / обновления / удаления. Запись будет медленнее, но вы получите пользу от чтения. Это зависит от вашего варианта использования, и я рекомендую его, только если вы ориентированы на чтение.
Таким образом, ваш запрос будет выглядеть так:
SELECT * FROM Messages WHERE IsLastInGroup = 1
Приведенный ниже запрос будет хорошо работать в соответствии с вашим вопросом.
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;
Если вы хотите последний ряд для каждого Name
затем вы можете присвоить номер каждой группе строк Name
и заказать по Id
в порядке убывания.
QUERY
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 DISTINCT ON (name) *
FROM messages
ORDER BY name, id DESC;
У меня была похожая проблема (на жестком postgresql) и в таблице записей 1M. Это решение занимает 1,7 с против 44 с, созданных с помощью LEFT JOIN. В моем случае мне пришлось отфильтровать соответствующий компонент вашего имени по значениям NULL, что привело к еще лучшей производительности на 0,2 с.
Начиная с MySQL 8.0.14, это также может быть достигнуто с помощью Lateral Derived Tables:
SELECT t.*
FROM messages t
JOIN LATERAL (
SELECT name, MAX(id) AS id
FROM messages t1
WHERE t.name = t1.name
GROUP BY name
) trn ON t.name = trn.name AND t.id = trn.id
Еще один вариант без подзапросов.
Это решение использует MySQLLAST_VALUE
оконная функция, эксплуатирующаяWindow Function Frame
доступный инструмент MySQL из .
SELECT DISTINCT
LAST_VALUE(Id)
OVER(PARTITION BY Name
ORDER BY Id
ROWS BETWEEN 0 PRECEDING
AND UNBOUNDED FOLLOWING),
Name,
LAST_VALUE(Other_Columns)
OVER(PARTITION BY Name
ORDER BY Id
ROWS BETWEEN 0 PRECEDING
AND UNBOUNDED FOLLOWING)
FROM
tab
Попробуйте здесь .
Что о:
select *, max(id) from messages group by name
Я тестировал его на sqlite, и он возвращает все столбцы и максимальное значение идентификатора для всех имен.