Получение последней записи в каждой группе - MySQL - PullRequest
824 голосов
/ 21 августа 2009

Существует таблица 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

Но это выглядит крайне неэффективно. Есть ли другие способы достижения того же результата?

Ответы [ 25 ]

843 голосов
/ 21 августа 2009

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;

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

Например, у меня есть копия дампа данных StackOverflow August . Я буду использовать это для сравнительного анализа. В таблице Posts имеется 1114 357 строк. Это работает на 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;
137 голосов
/ 06 января 2012

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 в EXPLAIN).

У меня есть пара других решений, основанных на тех же идеях:

  • , если базовым индексом является индекс 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 строк для каждой группы.

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

90 голосов
/ 21 августа 2009

Используйте ваш подзапрос , чтобы вернуть правильную группировку, потому что вы на полпути.

Попробуйте это:

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

Таким образом, вы избегаете коррелированных подзапросов и / или упорядочения в ваших подзапросах, которые, как правило, очень медленные / неэффективные.

42 голосов
/ 21 февраля 2012

Я пришел к другому решению: получить идентификаторы для последнего сообщения в каждой группе, а затем выбрать из таблицы сообщений, используя результат первого запроса в качестве аргумента для конструкции 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.

28 голосов
/ 25 декабря 2013

Решение по подзапросу Fiddle Link

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 уже предоставлен в других ответах.

8 голосов
/ 31 марта 2012

Я еще не тестировал большие БД, но думаю, это может быть быстрее, чем объединение таблиц

SELECT *, Max(Id) FROM messages GROUP BY Name
6 голосов
/ 08 июня 2017

Вот мое решение:

SELECT 
  DISTINCT NAME,
  MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES 
FROM MESSAGE;
6 голосов
/ 10 марта 2018

Подход со значительной скоростью выглядит следующим образом.

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
5 голосов
/ 21 августа 2009

Вот два предложения. Во-первых, если 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
)

Другими словами, выберите сообщения, в которых нет сообщения с более поздним идентификатором с таким же именем.

5 голосов
/ 29 марта 2014

Вот еще один способ получить последнюю связанную запись, используя 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 над этим списком, вы выберете первый

Демонстрация Fiddle

...