Uzyskaj rekordy o maksymalnej wartości dla każdej grupy zgrupowanych wyników SQL

229

Jak uzyskać wiersze zawierające maksymalną wartość dla każdego zgrupowanego zestawu?

Widziałem pewne zbyt skomplikowane warianty tego pytania i żadne z dobrą odpowiedzią. Próbowałem stworzyć najprostszy możliwy przykład:

Biorąc pod uwagę poniższą tabelę z kolumnami osoby, grupy i wieku, w jaki sposób uzyskasz najstarszą osobę w każdej grupie? (Remis w grupie powinien dać pierwszy wynik alfabetyczny)

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

Pożądany zestaw wyników:

Shawn | 1     | 42    
Laura | 2     | 39  
Yarin
źródło
3
Uwaga: Zaakceptowana odpowiedź zadziałała w 2012 roku, kiedy została napisana. Jednak nie działa już z wielu powodów, jak podano w komentarzach.
Rick James

Odpowiedzi:

132

Jest bardzo prosty sposób, aby to zrobić w mysql:

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

Działa to, ponieważ w mysql nie można agregować kolumn nie grupujących według, w którym to przypadku mysql zwraca tylko pierwszy wiersz. Rozwiązaniem jest takie uporządkowanie danych, aby dla każdej grupy najpierw był wiersz, który chcesz, a następnie grupowanie według kolumn, dla których chcesz uzyskać wartość.

Unikasz skomplikowanych podzapytań, które próbują znaleźć max()itp., A także problemów ze zwracaniem wielu wierszy, gdy jest więcej niż jeden o tej samej wartości maksymalnej (tak jak zrobiłyby to inne odpowiedzi)

Uwaga: jest to rozwiązanie tylko dla MySQL . Wszystkie inne bazy danych, które znam, wyrzucą błąd składniowy SQL z komunikatem „kolumny nie zagregowane nie są wymienione w grupie według klauzul” lub podobne. Ponieważ to rozwiązanie wykorzystuje nieudokumentowane zachowanie, bardziej ostrożni mogą chcieć dołączyć test, aby stwierdzić, że nadal działa, jeśli przyszła wersja MySQL zmieni to zachowanie.

Aktualizacja wersji 5.7:

Od wersji 5.7 sql-modeustawienie zawiera ONLY_FULL_GROUP_BYdomyślnie, więc aby to zadziałało, nie możesz mieć tej opcji (edytuj plik opcji dla serwera, aby usunąć to ustawienie).

Czeski
źródło
66
„mysql właśnie zwraca pierwszy wiersz.” - może tak to działa, ale nie jest to gwarantowane. Dokumentacja mówi: „Serwer może swobodnie wybrać dowolną wartość z każdej grupy, tak, chyba że są one takie same, wartości wybrane są nieokreślone.” . Serwer nie wybiera wierszy, ale wartości (niekoniecznie z tego samego wiersza) dla każdej kolumny lub wyrażenia, które pojawiają się w SELECTklauzuli i nie są obliczane przy użyciu funkcji agregującej.
axiac
16
To zachowanie zmieniło się w MySQL 5.7.5 i domyślnie odrzuca to zapytanie, ponieważ kolumny w SELECTklauzuli nie są funkcjonalnie zależne od GROUP BYkolumn. Jeśli jest skonfigurowany do akceptowania go (`ONLY_FULL_GROUP_BY` jest wyłączony), działa jak poprzednie wersje (tzn. Wartości tych kolumn są nieokreślone).
axiac
17
Dziwi mnie, że ta odpowiedź uzyskała tak wiele pozytywnych opinii. To jest złe i złe. Nie ma gwarancji, że to zapytanie zadziała. Dane w podzapytaniu są zestawem nieuporządkowanym pomimo kolejności według klauzuli. MySQL może naprawdę teraz zamawiać rekordy i zachować tę kolejność, ale nie złamie żadnej reguły, jeśli przestanie to robić w przyszłej wersji. Następnie GROUP BYkondensuje się do jednego rekordu, ale wszystkie pola zostaną arbitralnie wybrane z rekordów. Być może MySQL obecnie po prostu zawsze wybiera pierwszy wiersz, ale równie dobrze może wybrać dowolny inny wiersz, a nawet wartości z różnych wierszy w przyszłej wersji.
Thorsten Kettner,
9
Okej, nie zgadzamy się tutaj. Nie używam nieudokumentowanych funkcji, które po prostu działają obecnie i polegam na niektórych testach, które, mam nadzieję, obejmują to. Wiesz, że masz szczęście, że dzięki bieżącej implementacji otrzymujesz pełny pierwszy rekord, w którym dokumenty wyraźnie stwierdzają, że możesz otrzymać jakieś nieokreślone wartości, ale nadal z niego korzystasz. Niektóre proste ustawienia sesji lub bazy danych mogą to zmienić w dowolnym momencie. Uważam to za zbyt ryzykowne.
Thorsten Kettner,
3
Ta odpowiedź wydaje się błędna. Na tej doc , serwer może swobodnie wybrać dowolną wartość z każdej grupy ... Ponadto, wybór wartości z każdej grupy nie można wpływać przez dodanie klauzuli ORDER BY. Sortowanie zestawów wyników następuje po wybraniu wartości, a ORDER BY nie wpływa na to, jaką wartość w ramach każdej grupy wybiera serwer.
Tgr
296

Prawidłowe rozwiązanie to:

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

Jak to działa:

Pasuje do każdego wiersza oze wszystkimi wierszami bo tej samej wartości w kolumnie Groupi większej wartości w kolumnie Age. Każdy wiersz, który onie ma maksymalnej wartości swojej grupy w kolumnie, Agebędzie pasował do jednego lub więcej wierszy z b.

LEFT JOINSprawia, że pasuje najstarszą osobą w grupie (w tym osoby, które są same w grupie) z rzędu pełnej NULLsz b( „bez największego wieku w grupie”).
Użycie INNER JOINpowoduje, że te wiersze nie pasują i są ignorowane.

WHEREKlauzula utrzymuje tylko wiersze o NULLsw pól wydobytych z b. Są to najstarsze osoby z każdej grupy.

Dalsze odczyty

To i wiele innych rozwiązań wyjaśniono w książce SQL Antipatterns: Unikanie pułapek programowania baz danych

aksjomat
źródło
43
BTW może to zwrócić dwa lub więcej wierszy dla tej samej grupy, jeśli o.Age = b.Agenp. Jeśli Paul z grupy 2 ma 39, jak Laura. Jeśli jednak nie chcemy takiego zachowania, możemy:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor
8
Niesamowite! W przypadku rekordów 20 mln jest to około 50 razy szybszy niż algorytm „naiwny” (połącz z podzapytaniem za pomocą max ())
user2706534
3
Działa idealnie z komentarzami @Todor. Dodałbym, że jeśli są dalsze warunki zapytania, należy je dodać w FROM i w LEWYM DOŁĄCZ. Coś JAK: OD (WYBIERZ * OD OSOBY, GDZIE Wiek! = 32) o LEWE DOŁĄCZ (WYBIERZ * OD OSOBY, GDZIE Wiek! = 32) b - jeśli chcesz zwolnić osoby, które mają 32 lata
Alain Zelink
1
@AlainZelink, czy te „dalsze warunki zapytania” nie powinny być lepiej umieszczone na końcowej liście warunków GDZIE, aby nie wprowadzać podkwerend - które nie były potrzebne w oryginalnej odpowiedzi @ axiac?
tarilabs
5
To rozwiązanie działało; jednak zaczęto go raportować w dzienniku powolnych zapytań, gdy podjęto próbę z ponad 10 000 wierszy o tym samym identyfikatorze. ŁĄCZYŁ się w indeksowanej kolumnie. Rzadki przypadek, ale uznałem, że warto o tym wspomnieć.
chaseisabelle
50

Możesz dołączyć do podzapytania, które ściąga MAX(Group)i Age. Ta metoda jest przenośna w większości 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;
Michał Berkowski
źródło
Michael, dzięki za to - ale czy masz odpowiedź na pytanie o zwrócenie wielu wierszy na remis, zgodnie z komentarzami Czecha?
Yarin
1
@Yarin Gdyby na przykład były 2 wiersze, gdzie Group = 2, Age = 20podzapytanie zwróciłoby jeden z nich, ale ONklauzula łączenia pasowałaby do obu z nich, więc otrzymalibyśmy 2 wiersze z tą samą grupą / wiekiem, ale różne wartości dla innych kolumn, zamiast jednego.
Michał Berkowski
Czy więc mówimy, że nie można ograniczyć wyników do jednego na grupę, chyba że pójdziemy tylko drogą Bohemian MySQL?
Yarin
@Yarin nie nie jest niemożliwe, wymaga tylko więcej pracy, jeśli istnieją dodatkowe kolumny - być może inne zagnieżdżone podkwerenda, aby pobrać maksymalny powiązany identyfikator dla każdej podobnej pary grupy / wieku, a następnie połączyć się z tym, aby uzyskać resztę wiersza na podstawie identyfikatora.
Michał Berkowski,
Powinna to być odpowiedź zaakceptowana (obecnie akceptowana odpowiedź nie powiedzie się na większości innych RDBMS, a nawet na wielu wersjach MySQL).
Tim Biegeleisen,
28

Moje proste rozwiązanie dla SQLite (i prawdopodobnie MySQL):

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

Jednak nie działa w PostgreSQL i może na niektórych innych platformach.

W PostgreSQL możesz użyć klauzuli DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Igor Kulagin
źródło
@Bohemian przepraszam, wiem, to tylko MySQL, ponieważ zawiera niezagregowane kolumny
Cec
2
@IgorKulagin - Nie działa w Postgres- Komunikat o błędzie: kolumna „mytable.id” musi pojawić się w klauzuli GROUP BY lub zostać użyta w funkcji agregującej
Yarin
13
Zapytanie MySQL może działać przypadkowo tylko przy wielu okazjach. „WYBIERZ *” może zwrócić informacje, które nie odpowiadają przynależnemu MAX (wiek). Ta odpowiedź jest zła. Prawdopodobnie dotyczy to również SQLite.
Albert Hendriks
2
Ale to pasuje do przypadku, w którym musimy wybrać kolumnę zgrupowaną i kolumnę maksymalną. To nie pasuje do powyższego wymogu, w którym przyniosłoby to wynik („Bob”, 1, 42), ale oczekiwany wynik to („Shawn”, 1, 42)
Ram Babu S
1
Dobry na postgres
Karol Gasienica
4

Przy użyciu metody rankingu.

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
źródło
sel - potrzebuję wyjaśnienia - nigdy wcześniej nie widziałem :=- co to jest?
Yarin
1
: = jest operatorem przypisania. Możesz przeczytać więcej na dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel
Będę musiał się w to zagłębić - myślę, że odpowiedź zbyt skomplikowała nasz scenariusz, ale dziękuję za nauczenie mnie czegoś nowego.
Yarin
3

Nie jestem pewien, czy MySQL ma funkcję numer_wiersza. Jeśli tak, możesz go użyć, aby uzyskać pożądany rezultat. Na SQL Server możesz zrobić coś podobnego do:

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;
użytkownik130268
źródło
1
Tak, od wersji 8.0.
Ilja Everilä
2

Ostatecznie rozwiązanie Axiac było dla mnie najlepsze. Miałem jednak dodatkową złożoność: obliczoną „wartość maksymalną”, uzyskaną z dwóch kolumn.

Użyjmy tego samego przykładu: chciałbym, aby najstarsza osoba w każdej grupie. Jeśli są ludzie w tym samym wieku, weź najwyższą osobę.

Musiałem wykonać lewe połączenie dwa razy, aby uzyskać takie zachowanie:

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;

Mam nadzieję że to pomoże! Myślę jednak, że powinien być lepszy sposób na zrobienie tego ...

Arthur C.
źródło
2

Moje rozwiązanie działa tylko wtedy, gdy potrzebujesz pobrać tylko jedną kolumnę, jednak dla moich potrzeb było to najlepsze rozwiązanie znalezione pod względem wydajności (używa tylko jednego zapytania!):

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

Używa GROUP_CONCAT, aby utworzyć uporządkowaną listę konkat, a następnie podciąć tylko do pierwszej.

Antonio Giovanazzi
źródło
Potwierdza, że ​​można uzyskać wiele kolumn, sortując według tego samego klucza wewnątrz group_concat, ale trzeba napisać osobny group_concat / index / substring dla każdej kolumny.
Rasika
Bonus polega na tym, że możesz dodać wiele kolumn do sortowania wewnątrz group_concat, a to z łatwością rozwiąże więzi i zagwarantuje tylko jeden rekord na grupę. Dobra robota na proste i wydajne rozwiązanie!
Rasika
2

Mam proste rozwiązanie, używając 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
Khalid Musa Sagar
źródło
1

Korzystanie z CTE - typowe wyrażenia tabelowe:

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
źródło
1

W Oracle poniżej zapytanie może dać pożądany wynik.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
Kiruba
źródło
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
źródło
0

Możesz także spróbować

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
źródło
1
Dzięki, choć zwraca wiele rekordów dla wieku, w którym jest remis
Yarin
Również to zapytanie byłoby niepoprawne w przypadku, gdy w grupie 1 jest 39-latek. W takim przypadku osoba ta zostałaby również wybrana, mimo że maksymalny wiek w grupie 1 jest wyższy.
Joshua Richardson
0

Nie użyłbym grupy jako nazwy kolumny, ponieważ jest to słowo zastrzeżone. Jednak następujące SQL działałoby.

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
źródło
Dzięki, choć zwraca wiele rekordów dla wieku, w którym jest remis
Yarin
@Yarin, jak zdecydowałby, która osoba jest prawidłowa najstarsza? Wiele odpowiedzi wydaje się być właściwą odpowiedzią, w przeciwnym razie użyj limitu i porządku
Duncan
0

Zaletą tej metody jest możliwość pozycjonowania według innej kolumny i nie niszczenie innych danych. Jest to bardzo przydatne w sytuacji, gdy próbujesz wyświetlić listę zamówień z kolumną zawierającą pozycje, na początku najcięższą.

Źródło: 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;
Ray Foss
źródło
0

niech nazwa stołu to ludzie

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; 
użytkownik3475425
źródło
0

Jeśli potrzebne jest ID (i wszystkie coulmns) z 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
    )
mayank kumar
źródło
0

W ten sposób otrzymuję N maks wierszy na grupę w 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
;

jak to działa:

  • samodzielnie dołączyć do stołu
  • grupy są tworzone przez co.country = ci.country
  • N elementów na grupę jest kontrolowanych przez ) < 1to dla 3 elementów -) <3
  • uzyskanie maksymalnej lub minimalnej zależy od: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - min

Pełny przykład tutaj:

mysql wybierz n maks. wartości na grupę

Vanko
źródło