Получить записи с максимальным значением для каждой группы сгруппированных результатов SQL

229

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

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

Учитывая приведенную ниже таблицу с столбцами персонажа, группы и возраста, как бы вы получили самого старого человека в каждой группе? (Галстук внутри группы должен дать первый алфавитный результат)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  

Требуемый набор результатов:

Shawn | 1     | 42    
Laura | 2     | 39  
Ярина
источник
3
Внимание: принятый ответ сработал в 2012 году, когда он был написан. Тем не менее, он больше не работает по нескольким причинам, как указано в комментариях.
Рик Джеймс

Ответы:

132

Есть супер-простой способ сделать это в MySQL:

select * 
from (select * from mytable order by `Group`, age desc, Person) x
group by `Group`

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

Вы избегаете сложных подзапросов, которые пытаются найти max()etc, а также проблем с возвратом нескольких строк, когда существует более одной строки с одинаковым максимальным значением (как и другие ответы)

Примечание: это решение только для mysql . Все другие известные мне базы данных будут выдавать синтаксическую ошибку SQL с сообщением «неагрегированные столбцы не перечислены в предложении group by» или аналогичными. Поскольку это решение использует недокументированное поведение, более осторожный может захотеть включить тест, чтобы утверждать, что он продолжает работать, если будущая версия MySQL изменит это поведение.

Обновление версии 5.7:

Начиная с версии 5.7, sql-modeнастройка включает ONLY_FULL_GROUP_BYпо умолчанию, поэтому для этой работы у вас не должно быть этой опции (отредактируйте файл опции для сервера, чтобы удалить эту настройку).

Богемский
источник
66
msgstr "mysql просто возвращает первый ряд." - возможно, так оно и есть, но это не гарантировано. Документация говорит: «Сервер волен выбрать любое значение из каждой группы, так что, если они не совпадают, то значения , выбранные неопределенны.» , Сервер выбирает не строки, а значения (не обязательно из одной и той же строки) для каждого столбца или выражения, которое появляется в SELECTпредложении и не вычисляется с использованием статистической функции.
Аксиак
16
Это поведение изменилось в MySQL 5.7.5, и по умолчанию он отклоняет этот запрос, поскольку столбцы в SELECTпредложении функционально не зависят от GROUP BYстолбцов. Если он настроен на его принятие (`ONLY_FULL_GROUP_BY` отключен), он работает так же, как и в предыдущих версиях (т.е. значения этих столбцов не определены).
Аксиак
17
Я удивлен, что этот ответ получил так много голосов. Это неправильно и это плохо. Этот запрос не гарантированно работает. Данные в подзапросе - это неупорядоченный набор, несмотря на предложение order by. MySQL может действительно упорядочить записи сейчас и сохранить этот порядок, но он не нарушит никаких правил, если прекратит делать это в какой-то будущей версии. Затем GROUP BYсжимается до одной записи, но все поля будут произвольно выбраны из записей. Это может быть , что MySQL в настоящее время просто всегда выбирает первый ряд, но она могла бы точно так же выбрать любую другую строку или даже значение из разных строк в версии будущего.
Торстен Кеттнер
9
Хорошо, мы не согласны здесь. Я не использую недокументированные функции, которые в настоящее время работают, и полагаюсь на некоторые тесты, которые, надеюсь, покроют это. Вы знаете, что вам просто повезло, что текущая реализация дает вам полную первую запись, где в документах четко указано, что вы могли бы получить какие-то неопределенные значения, но вы все равно используете ее. Некоторые простые настройки сеанса или базы данных могут изменить это в любое время. Я бы посчитал это слишком рискованным.
Торстен Кеттнер
3
Этот ответ кажется неправильным. Согласно документу , сервер может выбрать любое значение из каждой группы ... Кроме того, на выбор значений из каждой группы нельзя повлиять, добавив предложение ORDER BY. Сортировка набора результатов происходит после выбора значений, и ORDER BY не влияет на то, какое значение в каждой группе выберет сервер.
Tgr
298

Правильное решение:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

Как это устроено:

Он сопоставляет каждую строку oсо всеми строками, bимеющими одинаковое значение в столбце Groupи большее значение в столбце Age. Любая строка, oне имеющая максимального значения своей группы в столбце, Ageбудет соответствовать одной или нескольким строкам из b.

Он LEFT JOINпозволяет сопоставить самого старого человека в группе (включая лиц, которые одиноки в своей группе) с целой строкой из NULLs b(«не самый большой возраст в группе»).
Использование INNER JOINделает эти строки не совпадающими, и они игнорируются.

Предложение WHEREсохраняет только строки, имеющие NULLs в полях, извлеченных из b. Это самые старые люди в каждой группе.

Дальнейшие чтения

Это решение и многие другие объясняются в книге « Антипаттерны SQL: предотвращение ловушек программирования баз данных».

axiac
источник
43
Кстати, это может вернуть две или более строки для одной и той же группы, если o.Age = b.Age, например, если Пол из группы 2 находится на 39, как Лора. Однако, если мы не хотим такого поведения, мы можем сделать:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Тодор
8
Невероятно! Для 20M записей это примерно в 50 раз быстрее, чем «наивный» алгоритм (присоединиться к подзапросу с помощью max ())
user2706534
3
Прекрасно работает с комментариями @Todor. Я хотел бы добавить, что если есть дополнительные условия запроса, они должны быть добавлены в FROM и в LEFT JOIN. Нечто похожее: ОТ (ВЫБРАТЬ * ОТ ЛИЦА, ГДЕ ВОЗРАСТ! = 32) o ОСТАВИТЬСЯ (ВЫБРАТЬ * ОТ ЛИЦА, ГДЕ ВОЗРАСТ! = 32) b - если вы хотите уволить людей, которым 32 года
Ален Зелинк
1
@AlainZelink разве эти «дополнительные условия запроса» лучше не включать в окончательный список условий WHERE, чтобы не вводить подзапросы - которые не были нужны в исходном ответе @ axiac?
tarilabs
5
Это решение сработало; Тем не менее, он начал получать сообщения в медленном журнале запросов при попытке более 10000 строк с одинаковым идентификатором. СОЕДИНЯЕТСЯ на проиндексированном столбце. Редкий случай, но решил, что стоит упомянуть.
Chaseisabelle
50

Вы можете присоединиться к подзапросу, который тянет MAX(Group)и Age. Этот метод является переносимым для большинства СУБД.

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;
Майкл Берковски
источник
Майкл, спасибо за это, но есть ли у вас ответ на вопрос о возвращении нескольких строк на связях, согласно комментариям Богемиана?
Ярин
1
@Yarin Если бы было 2 строки, например, где Group = 2, Age = 20, подзапрос возвратил бы одну из них, но предложение соединения соответствовало ONбы им обоим , поэтому вы получите 2 строки с одинаковой группой / возрастом, хотя разные значения для других столбцов, а не один.
Майкл Берковски
Итак, мы говорим, что невозможно ограничить результаты одним по группе, если мы не пойдем по Bohemians MySQL-маршруту?
Ярин
@ Yarin нет, не невозможно, просто требуется больше работы, если есть дополнительные столбцы - возможно, еще один вложенный подзапрос, чтобы получить максимальный связанный идентификатор для каждой подобной пары группа / возраст, а затем объединиться с этим, чтобы получить оставшуюся часть строки на основе идентификатора.
Майкл Берковски
Это должен быть принятый ответ (в настоящее время принятый ответ потерпит неудачу на большинстве других СУБД, а на самом деле даже на многих версиях MySQL).
Тим Бигелейзен
28

Мое простое решение для SQLite (и, вероятно, MySQL):

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;

Однако это не работает в PostgreSQL и, возможно, на некоторых других платформах.

В PostgreSQL вы можете использовать предложение DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Игорь Кулагин
источник
@ Bohemian извините, я знаю, это только для MySQL, так как он включает в себя неагрегированные столбцы
Cec
2
@IgorKulagin - не работает в Postgres - сообщение об ошибке: столбец «mytable.id» должен появляться в предложении GROUP BY или использоваться в статистической функции
Ярин
13
Запрос MySQL может работать случайно только во многих случаях. «SELECT *» может возвращать информацию, которая не соответствует соответствующему MAX (возрасту). Этот ответ неверен. Вероятно, это также относится и к SQLite.
Альберт Хендрикс
2
Но это подходит для случая, когда нам нужно выбрать сгруппированный столбец и столбец max. Это не соответствует вышеприведенному требованию, где это приведет («Боб», 1, 42), но ожидаемый результат («Шон», 1, 42)
Рам Бабу S
1
Хорошо для postgres
Кароль Гасеница
4

Используя метод ранжирования.

SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person
сель
источник
sel - нужно какое-то объяснение - я никогда раньше не видел :=- что это?
Ярин
1
: = является оператором присваивания. Вы можете прочитать больше на dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel
Я должен разобраться в этом - я думаю, что ответ усложняет наш сценарий, но спасибо, что научил меня чему-то новому ..
Ярин
3

Не уверен, что в MySQL есть функция row_number. Если это так, вы можете использовать его, чтобы получить желаемый результат. На SQL Server вы можете сделать что-то похожее на:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;
user130268
источник
1
Так и есть, с 8.0.
Илья Эвериля
2

Решение Axiac - то, что сработало для меня лучше всего в конце. Однако у меня была дополнительная сложность: вычисленное «максимальное значение», полученное из двух столбцов.

Давайте использовать тот же пример: я хотел бы, чтобы самый старый человек в каждой группе. Если есть люди одинаково старые, возьмите самого высокого человека.

Мне пришлось выполнить левое соединение два раза, чтобы получить такое поведение:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

Надеюсь это поможет! Я думаю, что должен быть лучший способ сделать это, хотя ...

Артур С
источник
2

Мое решение работает только в том случае, если вам нужно извлечь только один столбец, однако для моих нужд было найдено лучшее решение с точки зрения производительности (оно использует только один запрос!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

Он использует GROUP_CONCAT для создания упорядоченного списка конкатов, а затем я подстроку только к первому.

Антонио Джованази
источник
Можно подтвердить, что вы можете получить несколько столбцов путем сортировки по одному и тому же ключу внутри group_concat, но для каждого столбца нужно написать отдельную group_concat / index / substring.
Расика
Преимущество здесь в том, что вы можете добавить несколько столбцов к сортировке внутри group_concat, и это легко разрешит связи и гарантирует только одну запись на группу. Хорошо сделано на простом и эффективном решении!
Расика
2

У меня есть простое решение с помощью WHERE IN

SELECT a.* FROM `mytable` AS a    
WHERE a.age IN( SELECT MAX(b.age) AS age FROM `mytable` AS b GROUP BY b.group )    
ORDER BY a.group ASC, a.person ASC
Халид Муса Сагар
источник
1

Использование CTE - общие табличные выражения:

WITH MyCTE(MaxPKID, SomeColumn1)
AS(
SELECT MAX(a.MyTablePKID) AS MaxPKID, a.SomeColumn1
FROM MyTable1 a
GROUP BY a.SomeColumn1
  )
SELECT b.MyTablePKID, b.SomeColumn1, b.SomeColumn2 MAX(b.NumEstado)
FROM MyTable1 b
INNER JOIN MyCTE c ON c.MaxPKID = b.MyTablePKID
GROUP BY b.MyTablePKID, b.SomeColumn1, b.SomeColumn2

--Note: MyTablePKID is the PrimaryKey of MyTable
Marvin
источник
1

В Oracle ниже запрос может дать желаемый результат.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
kiruba
источник
0
with CTE as 
(select Person, 
[Group], Age, RN= Row_Number() 
over(partition by [Group] 
order by Age desc) 
from yourtable)`


`select Person, Age from CTE where RN = 1`
Harshad
источник
0

Вы также можете попробовать

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
источник
1
Спасибо, хотя это возвращает несколько записей для возраста, когда есть связь
Ярин
Кроме того, этот запрос будет неверным в случае, если в группе 1 имеется 39 лет. В этом случае этот человек также будет выбран, даже если максимальный возраст в группе 1 выше.
Джошуа Ричардсон
0

Я бы не использовал Group в качестве имени столбца, так как это зарезервированное слово. Однако следующий SQL будет работать.

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest
Bae Cheol Shin
источник
Спасибо, хотя это возвращает несколько записей для возраста, когда есть связь
Ярин
@ Ярин, как бы решить, какой самый старый человек? Множественные ответы кажутся самыми правильными, в противном случае используйте лимит и порядок
Дункан,
0

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

Источник: http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_group-concat

SELECT person, group,
    GROUP_CONCAT(
        DISTINCT age
        ORDER BY age DESC SEPARATOR ', follow up: '
    )
FROM sql_table
GROUP BY group;
Рэй Фосс
источник
0

пусть имя таблицы будет людьми

select O.*              -- > O for oldest table
from people O , people T
where O.grp = T.grp and 
O.Age = 
(select max(T.age) from people T where O.grp = T.grp
  group by T.grp)
group by O.grp; 
user3475425
источник
0

Если ID (и все coulmns) необходимы из mytable

SELECT
    *
FROM
    mytable
WHERE
    id NOT IN (
        SELECT
            A.id
        FROM
            mytable AS A
        JOIN mytable AS B ON A. GROUP = B. GROUP
        AND A.age < B.age
    )
майянк кумар
источник
0

Вот как я получаю N max строк на группу в MySQL

SELECT co.id, co.person, co.country
FROM person co
WHERE (
SELECT COUNT(*)
FROM person ci
WHERE  co.country = ci.country AND co.id < ci.id
) < 1
;

как это устроено:

  • самостоятельно присоединиться к столу
  • группы сделаны co.country = ci.country
  • N элементов в группе контролируются ) < 1так для 3 элементов -) <3
  • чтобы получить максимум или минимум зависит от: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - мин

Полный пример здесь:

mysql выбрать n максимальных значений для группы

Ванко
источник