Отримуйте записи з максимальним значенням для кожної групи згрупованих результатів 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()тощо, а також проблеми повернення декількох рядків, коли є більше одного з тим самим максимальним значенням (як це робитимуть інші відповіді)

Примітка. Це рішення, яке стосується лише mysql . Усі інші бази даних, які мені відомі, призведуть до помилки синтаксису SQL з повідомленням "неагреговані стовпці не вказані в групі за допомогою пункту" або подібні. Оскільки в цьому рішенні використовується недокументована поведінка, більш обережні можуть захотіти включити тест, щоб стверджувати, що він продовжує працювати, якщо майбутня версія MySQL змінить цю поведінку.

Оновлення версії 5.7:

Починаючи з версії 5.7, sql-modeналаштування включає ONLY_FULL_GROUP_BYза замовчуванням, тому для здійснення цієї роботи у вас не повинно бути цієї опції (відредагуйте файл опцій для того, щоб сервер видалив цей параметр).


66
"mysql просто повертає перший рядок." - можливо, це так працює, але це не гарантується. Документація каже: «Сервер може вибрати будь-яке значення з кожної групи, так що, якщо вони не збігаються, то значення , вибрані невизначені.» . Сервер не вибирає рядки, але значення (не обов'язково з одного рядка) для кожного стовпця або виразу, що з’являється в SELECTпункті і не обчислюється за допомогою функції сукупності.
аксіак

16
Ця поведінка змінилася на MySQL 5.7.5 і за замовчуванням вона відхиляє цей запит, оскільки стовпці в SELECTпункті функціонально не залежать від GROUP BYстовпців. Якщо він налаштований прийняти його (`ONLY_FULL_GROUP_BY` вимкнено), він працює як попередні версії (тобто значення цих стовпців невизначені).
аксіак

17
Я здивований, що ця відповідь отримала так багато відгуків. Це неправильно, і це погано. Цей запит не гарантовано працює. Дані в підзапиті - це не упорядкований набір, незважаючи на порядок за пунктом. MySQL дійсно може замовити записи зараз і зберегти цей порядок, але це не порушило б жодне правило, якби він перестав це робити в якійсь майбутній версії. Тоді GROUP BYконденсується один запис, але всі поля будуть довільно вибиратись із записів. Це може бути , що MySQL в даний час просто завжди вибирає перший ряд, але вона могла б точно так же вибрати будь-яку іншу рядок або навіть значення з різних рядків у версії майбутнього.
Торстен Кеттнер

9
Гаразд, ми тут не згодні. Я не використовую незадокументовані функції, які просто зараз працюють і покладаються на деякі тести, які, сподіваємось, покриють це. Ви знаєте, що вам просто пощастило, що поточна реалізація отримує вам повний перший запис, де документи чітко зазначають, що ви можете отримати замість них невизначені значення, але ви все одно використовуєте їх. Деякі прості сеанси чи налаштування бази даних можуть змінити це в будь-який час. Я вважаю це занадто ризикованим.
Торстен Кеттнер

3
Ця відповідь здається неправильною. Згідно з документом , сервер може вибирати будь-яке значення з кожної групи ... Крім того, на вибір значень кожної групи не можна впливати, додаючи пункт ORDER BY. Сортування набору результатів відбувається після вибору значень, і ORDER BY не впливає на те, яке значення в кожній групі обирає сервер.
Тгр

296

Правильне рішення:

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 Antipatterns: Уникнення підводних каменів програмування баз даних


43
До речі, це може повернути два або більше рядків для однієї групи, якщо o.Age = b.Age, наприклад, якщо Пол з другої групи на 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. Я додам, що якщо є додаткові умови запиту, вони повинні бути додані ВІД І ЛІТЬКОГО ПРИЄДНАННЯ. Щось подобається: ВІД (ВИБРАТИ * Від людини, де вік! = 32) o ЛІВО ПРИЄДНАЙТЕСЬ (ВИБІРІТЬСЯ * від людини, де вік! = 32) b - якщо ви хочете звільнити людей, яким
виповнилося

1
@AlainZelink - чи не краще ці "подальші запити" внести до остаточного списку умов WHERE, щоб не вводити підзапити - які не потрібні були в оригінальній відповіді @ axiac?
тарілаби

5
Це рішення спрацювало; однак він почав отримувати звіт у журналі повільних запитів при спробі з 10 000+ рядків, що мають спільний ідентичний ідентифікатор. Приєднувався до індексованої колонки. Рідкісний випадок, але подумав, що варто згадати.
chaseisabelle

50

Ви можете приєднатися до підзапиту, який тягне за собою MAX(Group)і Age. Цей метод є портативним для більшості RDBMS.

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 не є неможливим, просто потрібна додаткова робота, якщо є додаткові стовпці - можливо, інший вкладений підзапит для витягування максимум асоційованого ідентифікатора для кожної подібної пари групи / віку, а потім приєднайтеся до цього, щоб отримати решту рядка на основі id.
Майкл Берковський

Це має бути прийнятою відповіддю (прийнята в даний час відповідь буде невдалою для більшості інших RDBMS, і насправді навіть не вдасться у багатьох версіях 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 або використовуватись у сукупній функції
Yarin

13
Запит MySQL може працювати лише випадково в багатьох випадках. "SELECT *" може повернути інформацію, яка не відповідає належному MAX (віку). Ця відповідь неправильна. Можливо, це стосується і SQLite.
Альберт Гендрікс

2
Але це відповідає випадку, коли нам потрібно вибрати згрупований стовпчик і максимальний стовпець. Це не відповідає зазначеній вимозі, де це призведе («Боб», 1, 42), але очікуваний результат («Шон», 1, 42)
Ram Babu S

1
Добре для
постгресів

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
Сель

Мені доведеться розібратися в цьому - я думаю, що відповідь ускладнює наш сценарій, але дякую, що ви навчили мене чомусь новому ..
Ярін

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;

1
Це так, починаючи з 8,0.
Ilja Everilä

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;

Сподіваюсь, це допомагає! Я думаю, має бути кращий спосіб зробити це, хоча ...


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 для створення упорядкованого списку konkat, а потім я підстроковую лише перший.


Можна підтвердити, що ви можете отримати кілька стовпців, відсортувавши один і той же ключ всередині 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

1

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

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1

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`

0

Ви також можете спробувати

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

1
Дякую, хоча це повертає кілька записів за вік, коли є нічия
Ярін

Крім того, цей запит був би невірним у випадку, якщо в групі 1. є 39-річний чоловік. У цьому випадку ця особа також буде обрана, хоча максимальний вік у групі 1 вище.
Джошуа Річардсон

0

Я б не використовував групу як назву стовпця, оскільки це зарезервоване слово. Однак наступний 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

Дякую, хоча це повертає кілька записів за вік, коли є нічия
Ярін

@Yarin як би вирішив, яка правильна найстаріша людина? Кілька відповідей, здається, є найправильнішою відповіддю, інакше використовуйте обмеження та порядок
Duncan

0

Цей метод має перевагу, що дозволяє класифікувати за іншим стовпцем, а не переробляти інші дані. Це досить корисно в ситуації, коли ви намагаєтеся перераховувати замовлення зі стовпцем для предметів, перелічуючи найважчі перші.

Джерело: http://dev.mysql.com/doc/refman/5.0/uk/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; 

0

Якщо ідентифікатор (і всі 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
  • щоб отримати max або min залежить від: co.id < ci.id
    • co.id <ci.id - макс
    • co.id> ci.id - хв

Повний приклад тут:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.