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

182

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

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

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

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  
Теги:
greatest-n-per-group

17 ответов

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

Существует очень простой способ сделать это в MySQL:

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

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

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

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

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

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

  • 0
    Довольно круто, Бо, вы получаете зеленую - когда вы говорите «разрешено не агрегировать не по группам по столбцам», вы говорите, что именно так MySQL ведет себя по умолчанию? чем это отличается от других СУБД?
  • 2
    @ Yarin Большинство других СУБД не разрешают вам в этом случае GROUP BY Group так как в списке SELECT присутствуют другие столбцы.
Показать ещё 22 комментария
237

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

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 делает его совпадающим с самым старым человеком в группе (включая лиц, которые являются одинокими в своей группе), с строкой, полной NULL от b ( "не самый большой возраст в группе" ). < ш > Использование INNER JOIN делает эти строки не соответствующими, и они игнорируются.

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

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

Это решение и многие другие объясняются в книге SQL Antipatterns: устранение ошибок программирования баз данных

  • 37
    Кстати, это может вернуть две или более строки для одной и той же группы, если 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))
  • 1
    @ Тодор, ты прав. Я упустил это из виду.
Показать ещё 6 комментариев
30

Вы можете присоединиться к подзапросу, который тянет 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;
  • 0
    Майкл, спасибо за это, но есть ли у вас ответ на вопрос о возвращении нескольких строк на связи, согласно комментариям Богемского?
  • 0
    @Yarin Если бы было 2 строки, например, где Group = 2, Age = 20 , подзапрос возвратил бы одну из них, но предложение join ON совпало бы с ними обеими , поэтому вы получите 2 строки назад с той же группой / возрастом хотя разные значения для других столбцов, а не один.
Показать ещё 5 комментариев
27

Мое простое решение для 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;
  • 0
    @ Богемский, не является ли это решение правильным?
  • 0
    @ Bohemian извините, я знаю, это только для MySQL, так как он включает в себя неагрегированные столбцы
Показать ещё 8 комментариев
3

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

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
  • 0
    sel - нужно какое-то объяснение - я даже никогда не видел := раньше - что это?
  • 1
    : = является оператором присваивания. Вы можете прочитать больше на dev.mysql.com/doc/refman/5.0/en/user-variables.html
Показать ещё 1 комментарий
2

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

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

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

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;

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

1

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

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

1

Не уверен, что 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;
  • 0
    Так и есть, с 8.0.
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
0

У меня есть простое решение с помощью 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
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 максимальных значений для группы

0

Если для mytable

требуется идентификатор (и все coulmns)
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

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

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; 
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 * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
  • 1
    Спасибо, хотя это возвращает несколько записей для возраста, когда есть связь
  • 0
    Кроме того, этот запрос будет неверным в случае, если в группе 1 имеется 39 лет. В этом случае этот человек также будет выбран, даже если максимальный возраст в группе 1 выше.
-1

Я бы не использовал 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
  • 0
    Спасибо, хотя это возвращает несколько записей для возраста, когда есть связь
  • 0
    @ Ярин, как бы решить, какой самый старый человек? Множественные ответы кажутся самыми правильными, в противном случае используйте лимит и порядок.
-1
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`

Ещё вопросы

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