SQL выбирает только строки с максимальным значением в столбце

У меня есть эта таблица для документов (упрощенная версия здесь):

+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+

Как выбрать одну строку на один идентификатор и только наибольший оборот?
С приведенными выше данными результат должен содержать две строки: [1, 3, ...] и [2, 1, ..]. Я использую MySQL.

В настоящее время я использую проверки в цикле while для обнаружения и перезаписи старых revs из набора результатов. Но является ли это единственным методом достижения результата? Разве нет решения SQL?

Обновление
Как показывают ответы, есть SQL-решение, а здесь демо-версия sqlfiddle.

Обновление 2
Я заметил, что после добавления вышеуказанного sqlfiddle скорость, с которой встал вопрос, превзошла скорость ответов. Это не было намерением! Скрипка основана на ответах, особенно на принятом ответе.

883
задан Majid Fouladpour 12 окт. '11 в 22:42
источник поделиться
30 ответов

На первый взгляд...

Все, что вам нужно, это предложение GROUP BY с функцией агрегации MAX:

SELECT id, MAX(rev)
FROM YourTable
GROUP BY id

Это так просто, не так ли?

Я просто заметил, что вам нужен столбец content.

Это очень распространенный вопрос в SQL: найдите все данные для строки с некоторым максимальным значением в столбце для каждого идентификатора группы. Я много слышал о своей карьере. Фактически, это был один из вопросов, которые я ответил в своем текущем техническом интервью.

На самом деле настолько распространено, что сообщество StackOverflow создало один тег, чтобы справиться с такими вопросами: .

В принципе, у вас есть два подхода к решению этой проблемы:

Соединение с простым group-identifier, max-value-in-group Подзапросом

В этом подходе вы сначала найдете group-identifier, max-value-in-group (уже разрешенный выше) в подзапросе. Затем вы присоединяете свою таблицу к подзапросу с равенством как на group-identifier, так и на max-value-in-group:

SELECT a.id, a.rev, a.contents
FROM YourTable a
INNER JOIN (
    SELECT id, MAX(rev) rev
    FROM YourTable
    GROUP BY id
) b ON a.id = b.id AND a.rev = b.rev

Левое соединение с помощью себя, настройка условий соединения и фильтров

В этом подходе вы оставили соединение с самим собой. Равенство, конечно же, идет в group-identifier. Затем два умных перемещения:

  • Второе условие объединения имеет левое боковое значение меньше правого значения
  • Когда вы выполните шаг 1, строки (ы), которые на самом деле имеют максимальное значение, будут иметь NULL в правой части (это a LEFT JOIN, помните?). Затем мы фильтруем объединенный результат, показывая только строки, где правая сторона NULL.

Итак, вы закончите:

SELECT a.*
FROM YourTable a
LEFT OUTER JOIN YourTable b
    ON a.id = b.id AND a.rev < b.rev
WHERE b.id IS NULL;

Заключение

Оба подхода приносят точный результат.

Если у вас есть две строки с max-value-in-group для group-identifier, обе строки будут в результате в обоих подходах.

Оба подхода совместимы с SQL ANSI, поэтому они будут работать с вашей любимой РСУБД, независимо от ее "вкуса".

Оба подхода также совместимы с производительностью, однако ваш пробег может варьироваться (РСУБД, структура БД, индексы и т.д.). Поэтому, когда вы выбираете один подход по сравнению с другим, ориентир. И убедитесь, что вы выбрали тот, который имеет для вас большой смысл.

1409
ответ дан Adrian Carneiro 12 окт. '11 в 22:43
источник поделиться

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

Вы можете сделать это, используя IN попробуйте следующее:

SELECT * 
FROM t1 WHERE (id,rev) IN 
( SELECT id, MAX(rev)
  FROM t1
  GROUP BY id
)

на мой взгляд, он менее сложный... легче читать и поддерживать.

173
ответ дан Kevin Burton 12 окт. '11 в 22:47
источник поделиться

Еще одно решение - использовать коррелированный подзапрос:

select yt.id, yt.rev, yt.contents
    from YourTable yt
    where rev = 
        (select max(rev) from YourTable st where yt.id=st.id)

Наличие индекса (id, rev) делает подзапрос почти как простой поиск...

Ниже приведены сравнения с решениями в ответе @AdrianCarneiro (subquery, leftjoin), основанные на измерениях MySQL с таблицей InnoDB размером ~ 1 миллион записей, размер группы: 1-3.

В то время как для полного сканирования таблицы подзапросы/левые/коррелированные тайминги относятся друг к другу как 6/8/9, когда дело доходит до прямого поиска или партии (id in (1,2,3)), подзапрос выполняется намного медленнее, чем остальные (из-за повторной передачи подзапрос). Однако я не мог отличать ледяные и коррелированные решения в скорости.

Наконец, поскольку leftjoin создает n * (n + 1)/2, объединяется в группы, его производительность может сильно зависеть от размера групп...

56
ответ дан Vajk Hermecz 23 янв. '14 в 17:16
источник поделиться

Я не могу ручаться за производительность, но вот трюк, вдохновленный ограничениями Microsoft Excel. Он имеет некоторые хорошие функции.

GOOD STUFF

  • Он должен принудительно вернуть только одну "максимальную запись", даже если есть галстук (иногда полезный)
  • Он не требует соединения

ПОДХОД

Это немного уродливо и требует, чтобы вы знали что-то о диапазоне допустимых значений столбца rev. Предположим, что мы знаем, что столбец rev - это число от 0,00 до 999, включая десятичные числа, но что только две цифры справа от десятичной точки (например, 34.17 будет действительным значением).

Суть в том, что вы создаете единый синтетический столбец путем конкатенации/упаковки первичного поля сравнения вместе с данными, которые вы хотите. Таким образом, вы можете заставить агрегированную функцию SQL MAX() возвращать все данные (поскольку она была упакована в один столбец). Затем вам нужно распаковать данные.

Вот как он выглядит с приведенным выше примером, написанным на SQL

SELECT id, 
       CAST(SUBSTRING(max(packed_col) FROM 2 FOR 6) AS float) as max_rev,
       SUBSTRING(max(packed_col) FROM 11) AS content_for_max_rev 
FROM  (SELECT id, 
       CAST(1000 + rev + .001 as CHAR) || '---' || CAST(content AS char) AS packed_col
       FROM yourtable
      ) 
GROUP BY id

Упаковка начинается с того, что для того, чтобы столбец rev был числом известных символов, независимо от значения rev, чтобы, например,

  • 3.2 становится 1003.201
  • 57 становится 1057.001
  • 923.88 становится 1923.881

Если вы это сделаете правильно, сравнение строк двух чисел должно давать то же самое "max", что и числовое сравнение двух чисел, и легко преобразовать обратно к исходному номеру с помощью функции подстроки (которая доступна в одной форме или другой почти везде).

35
ответ дан David Foster 30 июня '13 в 9:02
источник поделиться

Я смущен, что ни один из ответов не предложил решение функции SQL-окна:

SELECT a.id, a.rev, a.contents
  FROM (SELECT id, rev, contents,
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
          FROM YourTable) a
 WHERE a.rank = 1 

Добавлен в стандарт SQL Стандарт ANSI/ISO SQL: 2003 и более поздние версии с ANSI/ISO Standard SQL: 2008, теперь доступны функции окна (или окна) со всеми основными поставщиками. Существует больше типов ранговых функций, доступных для решения проблемы связи: RANK, DENSE_RANK, PERSENT_RANK.

31
ответ дан topchef 09 авг. '16 в 18:29
источник поделиться

Я думаю, что это самое простое решение:

SELECT *
FROM
    (SELECT *
    FROM Employee
    ORDER BY Salary DESC)
AS employeesub
GROUP BY employeesub.Salary;
  • SELECT *: вернуть все поля.
  • FROM Employee: найденная таблица.
  • (SELECT *...) подзапрос: вернуть всех людей, отсортированных по зарплате.
  • GROUP BY employeesub.Salary:: Принудительно, чтобы возвращаемый результат отображал верхнюю сортировку, Заработная строка каждого сотрудника.

Если вам нужна только одна строка, это еще проще:

SELECT *
FROM Employee
ORDER BY Employee.Salary DESC
LIMIT 1

Я также считаю, что проще всего сломать, понять и изменить в других целях:

  • ORDER BY Employee.Salary DESC: закажите результаты по зарплате с наивысшими зарплатами.
  • LIMIT 1: возвращает только один результат.

Понимание этого подхода, решение любой из этих подобных проблем становится тривиальным: получить сотрудника с наименьшей зарплатой (изменить DESC на ASC), получить топ-десять сотрудников (изменить LIMIT 1 до LIMIT 10), отсортировать с помощью другого поля ( измените ЗАКАЗАТЬ от Employee.Salary к ORDER BY Employee.Commission) и т.д.

21
ответ дан HoldOffHunger 14 сент. '16 в 3:28
источник поделиться

Что-то вроде этого?

SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
    SELECT id, max(rev) as maxrev FROM yourtable
    WHERE yourtable
    GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)
15
ответ дан Marc B 12 окт. '11 в 22:48
источник поделиться

Поскольку это самый популярный вопрос в отношении этой проблемы, я снова отправлю еще один ответ на этот вопрос:

Похоже, что есть более простой способ сделать это (но только в MySQL):

select *
from (select * from mytable order by id, rev desc ) x
group by id

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

EDIT: хотя это решение работает для многих людей, оно может быть нестабильным в долгосрочной перспективе, поскольку MySQL не гарантирует, что оператор GROUP BY вернет значимые значения для столбцов, не входящих в список GROUP BY. Поэтому используйте это решение на свой страх и риск

6
ответ дан Yura 03 июля '14 в 17:33
источник поделиться

Третье решение, о котором я почти никогда не упоминал, имеет специфику MySQL и выглядит следующим образом:

SELECT id, MAX(rev) AS rev
 , 0+SUBSTRING_INDEX(GROUP_CONCAT(numeric_content ORDER BY rev DESC), ',', 1) AS numeric_content
FROM t1
GROUP BY id

Да, это выглядит ужасно (преобразование в строку и обратно и т.д.), но по моему опыту это обычно быстрее, чем другие решения. Возможно, это только для моих случаев использования, но я использовал его на таблицах с миллионами записей и множеством уникальных идентификаторов. Может быть, потому, что MySQL довольно плохо оптимизирует другие решения (по крайней мере, в 5,0 дней, когда я придумал это решение).

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

В любом случае вышеупомянутое не работает напрямую, если ваше поле содержимого уже является текстом. В этом случае вы, вероятно, захотите использовать другой разделитель, например, \0. Вы также быстрее столкнетесь с префиксом group_concat_max_len.

4
ответ дан Jannes 10 окт. '14 в 14:57
источник поделиться

Мне нравится использовать решение на основе NOT EXIST для этой проблемы:

SELECT id, rev
FROM YourTable t
WHERE NOT EXISTS (
   SELECT * FROM YourTable t WHERE t.id = id AND rev > t.rev
)
4
ответ дан Bulat 06 сент. '14 в 0:58
источник поделиться

Если у вас много полей в инструкции select, и вы хотите получить последнее значение для всех этих полей с помощью оптимизированного кода:

select * from
(select * from table_name
order by id,rev desc) temp
group by id 
3
ответ дан seahawk 04 сент. '15 в 8:33
источник поделиться

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

select all_fields.*  
from  (select id, MAX(rev) from yourtable group by id) as max_recs  
left outer join yourtable as all_fields  
on max_recs.id = all_fields.id
2
ответ дан inor 14 июля '13 в 19:09
источник поделиться

Я бы использовал это:

select t.*
from test as t
join
   (select max(rev) as rev
    from test
    group by id) as o
on o.rev = t.rev

Подзапрос SELECT не слишком эффективен, но в разделе JOIN кажется полезным. Я не эксперт в оптимизации запросов, но я пробовал в MySQL, PostgreSQL, FireBird и работает очень хорошо.

Вы можете использовать эту схему в нескольких соединениях и с предложением WHERE. Это мой рабочий пример (решение идентично вашей проблеме с таблицей "твердое" ):

select *
from platnosci as p
join firmy as f
on p.id_rel_firmy = f.id_rel
join (select max(id_obj) as id_obj
      from firmy
      group by id_rel) as o
on o.id_obj = f.id_obj and p.od > '2014-03-01'

Его спрашивают на таблицах с подростками таких записей, и он занимает менее 0,01 секунды на действительно не слишком сильной машине.

Я бы не использовал предложение IN (как упоминается выше). IN предоставляется для использования с короткими списками констант, а не как фильтр запросов, построенный на подзапросе. Это связано с тем, что подзапрос в IN выполняется для каждой отсканированной записи, которая может сделать запрос очень медленным.

2
ответ дан Marek Wysmułek 04 марта '15 в 21:12
источник поделиться

Многие, если не все, другие ответы здесь подходят для небольших наборов данных. Для масштабирования требуется больше внимания. См. здесь.

В нем обсуждается несколько более быстрых способов сделать groupwise max и top-N для каждой группы.

1
ответ дан Rick James 02 апр. '17 в 1:06
источник поделиться

Здесь другое решение для получения записей только с полем, которое имеет максимальное значение для этого поля. Это работает для SQL400, на котором я работаю. В этом примере записи с максимальным значением в поле FIELD5 будут получены следующим оператором SQL.

SELECT A.KEYFIELD1, A.KEYFIELD2, A.FIELD3, A.FIELD4, A.FIELD5
  FROM MYFILE A
 WHERE RRN(A) IN
   (SELECT RRN(B) 
      FROM MYFILE B
     WHERE B.KEYFIELD1 = A.KEYFIELD1 AND B.KEYFIELD2 = A.KEYFIELD2
     ORDER BY B.FIELD5 DESC
     FETCH FIRST ROW ONLY)
1
ответ дан Cesar 17 окт. '17 в 2:48
источник поделиться

SELECT * ОТ сотрудника где Employee.Salary in (выберите max (зарплата) из группы Employee Employe_id) ORDER BY Employee.Salary

1
ответ дан guru008 30 июля '17 в 21:12
источник поделиться

Ни один из этих ответов не работал у меня.

Это то, что сработало для меня.

with score as (select max(score_up) from history)
select history.* from score, history where history.score_up = score.max
1
ответ дан qaisjp 13 июля '17 в 21:19
источник поделиться

вот еще одно решение, которое поможет кому-то

Select a.id , a.rev, a.content from Table1 a
inner join 
(SELECT id, max(rev) rev FROM Table1 GROUP BY id) x on x.id =a.id and x.rev =a.rev
1
ответ дан Abdul Samad 20 июня '17 в 13:10
источник поделиться

Отсортировано поле rev в обратном порядке, а затем сгруппировано по id, которое дало первую строку каждой группы, которая является самой высокой величиной rev.

SELECT * FROM (SELECT * FROM table1 ORDER BY id, rev DESC) X GROUP BY X.id;

Протестировано в http://sqlfiddle.com/ со следующими данными

CREATE TABLE table1
    (`id` int, `rev` int, `content` varchar(11));

INSERT INTO table1
    (`id`, `rev`, `content`)
VALUES
    (1, 1, 'One-One'),
    (1, 2, 'One-Two'),
    (2, 1, 'Two-One'),
    (2, 2, 'Two-Two'),
    (3, 2, 'Three-Two'),
    (3, 1, 'Three-One'),
    (3, 3, 'Three-Three')
;

Это дало следующий результат в MySql 5.5 и 5.6

id  rev content
1   2   One-Two
2   2   Two-Two
3   3   Three-Two
1
ответ дан blokeish 11 дек. '15 в 6:14
источник поделиться

Если кто-то ищет Linq verson, это, похоже, работает для меня:

public static IQueryable<BlockVersion> LatestVersionsPerBlock(this IQueryable<BlockVersion> blockVersions)
{
    var max_version_per_id = blockVersions.GroupBy(v => v.BlockId)
        .Select( v => new { BlockId = v.Key, MaxVersion = v.Max(x => x.Version) } );    

    return blockVersions.Where( v => max_version_per_id.Any(x => x.BlockId == v.BlockId && x.MaxVersion == v.Version) );
}
1
ответ дан Dirk Boer 17 сент. '15 в 16:22
источник поделиться

Мне нравится делать это путем ранжирования записей в каком-то столбце. В этом случае значения ранга rev сгруппированы по id. Те, у кого выше rev, будут иметь более низкий рейтинг. Таким образом, наивысший rev будет иметь рейтинг 1.

select id, rev, content
from
 (select
    @rowNum := if(@prevValue = id, @rowNum+1, 1) as row_num,
    id, rev, content,
    @prevValue := id
  from
   (select id, rev, content from YOURTABLE order by id asc, rev desc) TEMP,
   (select @rowNum := 1 from DUAL) X,
   (select @prevValue := -1 from DUAL) Y) TEMP
where row_num = 1;

Не уверен, что введение переменных делает все это медленнее. Но, по крайней мере, я не дважды запрашиваю YOURTABLE.

1
ответ дан user5124980 16 июля '15 в 21:52
источник поделиться

Вот хороший способ сделать это

Используйте следующий код:

with temp as  ( 
select count(field1) as summ , field1
from table_name
group by field1 )
select * from temp where summ = (select max(summ) from temp)
1
ответ дан shay 07 янв. '15 в 14:36
источник поделиться

Это решение делает только один выбор из YourTable, поэтому он быстрее. Он работает только для MySQL и SQLite (для SQLite удаляет DESC) в соответствии с тестом на sqlfiddle.com. Возможно, он может быть настроен для работы на других языках, с которыми я не знаком.

SELECT *
FROM ( SELECT *
       FROM ( SELECT 1 as id, 1 as rev, 'content1' as content
              UNION
              SELECT 2, 1, 'content2'
              UNION
              SELECT 1, 2, 'content3'
              UNION
              SELECT 1, 3, 'content4'
            ) as YourTable
       ORDER BY id, rev DESC
   ) as YourTable
GROUP BY id
1
ответ дан plavozont 29 янв. '14 в 10:49
источник поделиться

НЕ mySQL, но для других людей, которые находят этот вопрос и используют SQL, другим способом решения проблемы является использование Cross Apply в MS SQL

WITH DocIds AS (SELECT DISTINCT id FROM docs)

SELECT d2.id, d2.rev, d2.content
FROM DocIds d1
CROSS APPLY (
  SELECT Top 1 * FROM docs d
  WHERE d.id = d1.id
  ORDER BY rev DESC
) d2

Вот пример в SqlFiddle

1
ответ дан KyleMit 30 мая '14 в 16:47
источник поделиться

Я использовал ниже, чтобы решить свою проблему. Сначала я создал временную таблицу и вставил максимальное значение rev на уникальный идентификатор.

CREATE TABLE #temp1
(
    id varchar(20)
    , rev int
)
INSERT INTO #temp1
SELECT a.id, MAX(a.rev) as rev
FROM 
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as a 
GROUP BY a.id
ORDER BY a.id

Затем я присоединил эти максимальные значения (# temp1) ко всем возможным комбинациям id/content. Делая это, я, естественно, отфильтровываю не максимальные комбинации id/content, и оставляю их только с максимальными значениями rev для каждого.

SELECT a.id, a.rev, content
FROM #temp1 as a
LEFT JOIN
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as b on a.id = b.id and a.rev = b.rev
GROUP BY a.id, a.rev, b.content
ORDER BY a.id
0
ответ дан Richard Ball 05 янв. '18 в 13:51
источник поделиться

Другой способ выполнения задания - использовать аналитическую функцию MAX() в предложении OVER PARTITION

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,MAX(rev) OVER (PARTITION BY id) as max_rev
      FROM YourTable
    ) t
  WHERE t.rev = t.max_rev 

Другое решение OVER PARTITION, уже зарегистрированное в этом сообщении,

SELECT t.*
  FROM
    (
    SELECT id
          ,rev
          ,contents
          ,ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
      FROM YourTable
    ) t
  WHERE t.rank = 1 

Этот 2 SELECT хорошо работает на Oracle 10g.

0
ответ дан schlebe 20 февр. '18 в 12:07
источник поделиться
SELECT id, MAX(rev)
FROM yourTable
GROUP BY id

Это работает в Oracle Sql

-1
ответ дан SevincQurbanova 04 марта '18 в 2:11
источник поделиться
select * from yourtable
group by id
having rev=max(rev);
-1
ответ дан Terry 11 июля '13 в 15:53
источник поделиться

Это работает для меня в sqlite3:

SELECT *, MAX(rev) FROM t1 GROUP BY id

С *, вы получаете дублированный столбец rev, но это не так много проблемы.

-2
ответ дан Fredrik Eldh 08 янв. '15 в 19:49
источник поделиться
SELECT * FROM t1 ORDER BY rev DESC LIMIT 1;
-4
ответ дан Tricky 18 июня '13 в 10:49
источник поделиться

Другие вопросы по меткам