Получение последней записи в каждой группе - MySQL

580

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

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

Показать ещё 5 комментариев
Теги:
group-by
greatest-n-per-group

18 ответов

598
Лучший ответ

Я пишу решение таким образом:

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 имеется 1114357 строк. Это работает на MySQL 5.0.75 на моем MacBook Pro 2.40 ГГц.

Я напишу запрос, чтобы найти самую последнюю запись для данного ID пользователя (мой).

Сначала используя , используя @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;
  • 7
    В самом деле? Что произойдет, если у вас есть тонна записей? Например, если вы работаете с внутренним управлением версиями, скажем, и у вас есть тонна версий на файл, результат объединения будет огромным. Вы когда-нибудь сравнивали метод подзапроса с этим? Мне довольно любопытно узнать, кто победит, но не настолько любопытно, чтобы не спросить вас первым.
  • 0
    Спасибо, Билл. Это работает отлично. Можете ли вы предоставить больше информации о производительности этого запроса по отношению к объединению, предоставленному Эриком?
Показать ещё 38 комментариев
110

UPD: 2017-03-31, версия 5.7.5 MySQL включила ONLY_FULL_GROUP_BY переключатель по умолчанию (следовательно, не детерминированный Запросы GROUP BY отключены). Более того, они обновили реализацию GROUP BY, и решение может работать не так, как ожидалось, даже с отключенным коммутатором. Нужно проверить.

Решение Bill Karwin выше работает отлично, когда количество элементов внутри групп довольно мало, но производительность запроса становится плохой, когда группы довольно велики, так как для решения требуется около n*n/2 + n/2 только сравнений IS NULL.

Я провел тесты в таблице InnoDB строк 18684446 с группами 1182. Таблица содержит тестовые результаты для функциональных тестов и имеет (test_id, request_id) в качестве первичного ключа. Таким образом, test_id является группой, и я искал последний request_id для каждого test_id.

Решение Bill уже работает несколько часов на моем 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 максимальных строк для каждой группы.

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

  • 0
    Соответствующий ответ: stackoverflow.com/a/14836418/68998
  • 0
    Пожалуйста, дайте ссылку на дамп ваших таблиц, чтобы люди могли проверить его на своих платформах.
Показать ещё 4 комментария
72

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

Попробуйте следующее:

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

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

  • 1
    Обратите внимание на предостережение для решения с other_col : если этот столбец не уникален, вы можете получить несколько записей с одним и тем же name , если они связываются для max(other_col) . Я нашел этот пост, который описывает решение для моих нужд, где мне нужно ровно одна запись на name .
  • 0
    В некоторых ситуациях вы можете использовать только это решение, но только одно из принятых.
Показать ещё 1 комментарий
28

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

  • 0
    Просто убедитесь, что у вас есть индекс (имя, идентификатор).
  • 1
    Гораздо лучше, что само присоединяется
Показать ещё 1 комментарий
19

Решение по подзапросу скрипка Ссылка

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

  • 0
    Какой смысл в «скрипке», если вы не можете запустить ее?
  • 1
    @AlexanderSuraphel mysql5.5 теперь недоступен в скрипте, ссылка для скрипта была создана с его помощью. Теперь дневная скрипка поддерживает mysql5.6, я изменил базу данных на mysql 5.6 и смог построить схему и запустить sql.
6

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

SELECT *, Max(Id) FROM messages GROUP BY Name
  • 4
    Это возвращает произвольные данные. Другими словами, возвращаемые столбцы могут отсутствовать в записи с MAX (Id).
  • 0
    Полезно для выбора максимального Id из набора записей с условием WHERE: «SELECT Max (Id) FROM Prod WHERE Pn = '" + Pn + "'" Возвращает максимальный Id из набора записей с тем же Pn.In c # используйте reader.GetString (0), чтобы получить результат
4

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

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

  • 8
    MySQL не поддерживает ROW_NUMBER () или CTE.
3

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

SELECT 
  DISTINCT NAME,
  MAX(MESSAGES) OVER(PARTITION BY NAME) MESSAGES 
FROM MESSAGE;
3
SELECT 
  column1,
  column2 
FROM
  table_name 
WHERE id IN 
  (SELECT 
    MAX(id) 
  FROM
    table_name 
  GROUP BY column1) 
ORDER BY column1 ;
  • 0
    Не могли бы вы рассказать немного о своем ответе? Почему ваш запрос предпочтительнее оригинального запроса Vijays?
2

Вы также можете посмотреть и здесь.

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 ;
  • 0
    Второе решение не работает для моего случая
2

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

Скриншот Демо

2

Попробуйте следующее:

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 )  
1

Привет @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
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;
1

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

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);
0
select * from messages group by name desc
0

Если вам нужна последняя строка для каждого 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

-1

Как насчет этого:

SELECT DISTINCT ON (name) *
FROM messages
ORDER BY name, id DESC;

У меня была аналогичная проблема (на postgresql tough) и на таблице записей 1M. Это решение занимает 1,7 с против 44, созданного с помощью LEFT JOIN. В моем случае мне пришлось отфильтровать корреспондент вашего поля имени со значениями NULL, что привело к еще лучшим результатам на 0,2 секунды

Ещё вопросы

Сообщество Overcoder
Наверх
Меню