Ottieni record con valore massimo per ogni gruppo di risultati SQL raggruppati


229

Come si ottengono le righe che contengono il valore massimo per ciascun set raggruppato?

Ho visto alcune variazioni eccessivamente complicate su questa domanda, e nessuna con una buona risposta. Ho provato a mettere insieme l'esempio più semplice possibile:

Data una tabella come quella di seguito, con le colonne persona, gruppo ed età, come otterresti la persona più anziana in ciascun gruppo? (Un pareggio all'interno di un gruppo dovrebbe dare il primo risultato alfabetico)

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

Set di risultati desiderato:

Shawn | 1     | 42    
Laura | 2     | 39  

3
Attenzione: la risposta accettata ha funzionato nel 2012 quando è stata scritta. Tuttavia, non funziona più per diversi motivi, come indicato nei commenti.
Rick James,

Risposte:


132

C'è un modo semplicissimo per farlo in mysql:

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

Questo funziona perché in mysql ti è permesso di non aggregare colonne non raggruppate, nel qual caso mysql restituisce solo la prima riga. La soluzione è prima ordinare i dati in modo tale che per ogni gruppo sia la prima riga desiderata, quindi raggruppare per le colonne per le quali si desidera il valore.

Si evitano complicate query secondarie che tentano di trovare max()ecc, nonché i problemi di restituzione di più righe quando ce ne sono più di una con lo stesso valore massimo (come farebbero le altre risposte)

Nota: questa è una soluzione solo per mysql . Tutti gli altri database che conosco genereranno un errore di sintassi SQL con il messaggio "le colonne non aggregate non sono elencate nel gruppo per clausola" o simili. Poiché questa soluzione utilizza un comportamento non documentato , più prudente potrebbe voler includere un test per affermare che rimane funzionante qualora una versione futura di MySQL cambi questo comportamento.

Aggiornamento versione 5.7:

Dalla versione 5.7, l' sql-modeimpostazione include ONLY_FULL_GROUP_BYdi default, quindi per fare questo lavoro è necessario non avere questa opzione (Modificare il file opzione per il server di rimuovere questa impostazione).


66
"mysql restituisce solo la prima riga." - forse è così che funziona ma non è garantito. La documentazione dice: "Il server è libero di scegliere qualsiasi valore da ciascun gruppo, quindi a meno che non siano gli stessi, i valori scelti sono indeterminati". . Il server non seleziona le righe ma i valori (non necessariamente dalla stessa riga) per ogni colonna o espressione che appare nella SELECTclausola e non viene calcolata usando una funzione aggregata.
axiac,

16
Questo comportamento è cambiato su MySQL 5.7.5 e, per impostazione predefinita, rifiuta questa query perché le colonne nella SELECTclausola non dipendono funzionalmente dalle GROUP BYcolonne. Se è configurato per accettarlo (`ONLY_FULL_GROUP_BY` è disabilitato), funziona come le versioni precedenti (ovvero i valori di quelle colonne sono indeterminati).
axiac,

17
Sono sorpreso che questa risposta abbia ricevuto così tanti voti. È sbagliato ed è cattivo. Questa query non è garantita per funzionare. I dati in una sottoquery sono un set non ordinato nonostante la clausola order by. MySQL può davvero ordinare i record ora e mantenere quell'ordine, ma non infrange alcuna regola se smettesse di farlo in qualche versione futura. Quindi si GROUP BYcondensa in un record, ma tutti i campi verranno arbitrariamente scelti dai record. È possibile che MySQL al momento scelga semplicemente sempre la prima riga, ma potrebbe anche scegliere qualsiasi altra riga o persino valori da righe diverse in una versione futura.
Thorsten Kettner,

9
Ok, non siamo d'accordo qui. Non utilizzo funzionalità prive di documenti che attualmente funzionano e faccio affidamento su alcuni test che si spera copriranno questo. Sai che sei solo fortunato che l'attuale implementazione ti dia il primo record completo in cui i documenti dichiarano chiaramente che potresti ottenere valori indeterminati invece, ma lo usi ancora. Alcune semplici impostazioni di sessione o database possono cambiare in qualsiasi momento. Lo considererei troppo rischioso.
Thorsten Kettner,

3
Questa risposta sembra sbagliata. Secondo il documento , il server è libero di scegliere qualsiasi valore da ciascun gruppo ... Inoltre, la selezione di valori da ciascun gruppo non può essere influenzata dall'aggiunta di una clausola ORDER BY. L'ordinamento del set di risultati si verifica dopo che sono stati scelti i valori e ORDER BY non influenza il valore all'interno di ciascun gruppo scelto dal server.
Tgr

297

La soluzione corretta è:

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

Come funziona:

Corrisponde a ciascuna riga da ocon tutte le righe baventi lo stesso valore in colonna Groupe un valore maggiore in colonna Age. Qualsiasi riga da onon avere il valore massimo del suo gruppo nella colonna Agecorrisponderà a una o più righe da b.

Lo LEFT JOINfa abbinare la persona più anziana nel gruppo (comprese le persone che sono sole nel loro gruppo) con una fila piena di NULLs da b("nessuna maggiore età nel gruppo").
L'uso INNER JOINrende queste righe non corrispondenti e vengono ignorate.

La WHEREclausola mantiene solo le righe che hanno NULLs nei campi estratti b. Sono le persone più anziane di ogni gruppo.

Ulteriori letture

Questa soluzione e molte altre sono spiegate nel libro SQL Antipatterns: evitare le insidie ​​della programmazione di database


43
A proposito, questo può restituire due o più righe per uno stesso gruppo se o.Age = b.Age, ad esempio, se Paul del gruppo 2 è su 39 come Laura. Tuttavia, se non vogliamo tale comportamento, possiamo fare:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor

8
Incredibile! Per 20 milioni di record è 50 volte più veloce dell'algoritmo "ingenuo" (unisci contro una sottoquery con max ())
user2706534

3
Funziona perfettamente con i commenti di @Todor. Vorrei aggiungere che se ci sono ulteriori condizioni di query, devono essere aggiunti in FROM e in LEFT JOIN. Qualcosa di simile: DA (SELEZIONA * DA Persona DOVE Età! = 32) o SINISTRA ISCRIVITI (SELEZIONA * DA Persona DOVE Età! = 32) b - se vuoi licenziare persone che hanno 32 anni
Alain Zelink,

1
@AlainZelink queste "ulteriori condizioni di query" non possono essere meglio inserite nell'elenco finale delle condizioni WHERE, al fine di non introdurre sottoquery - che non erano necessarie nella risposta @ axiac originale?
Tarilabs,

5
Questa soluzione ha funzionato; tuttavia, ha iniziato a essere segnalato nel registro delle query lente quando si tenta con oltre 10.000 righe di condividere lo stesso ID. ISCRIVITI alla colonna indicizzata. Un caso raro, ma ho pensato che valesse la pena menzionarlo.
Chaseisabelle,

50

Puoi unirti a una subquery che tira il MAX(Group)e Age. Questo metodo è portatile nella maggior parte dei 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;

Michael, grazie per questo- ma hai una risposta per il problema di restituire più righe sui legami, secondo i commenti di Bohemian?
Yarin,

1
@Yarin Se ad esempio ci fossero 2 righe Group = 2, Age = 20, la sottoquery restituirebbe una di esse, ma la ONclausola di join corrisponderebbe a entrambe , quindi si otterrebbero 2 righe indietro con lo stesso gruppo / età sebbene valori diversi per le altre colonne, piuttosto che uno.
Michael Berkowski,

Quindi stiamo dicendo che è impossibile limitare i risultati a uno per gruppo a meno che non seguiamo la rotta Boemia solo per MySQL?
Yarin,

@Yarin no non impossibile, richiede solo più lavoro se ci sono colonne aggiuntive - possibilmente un'altra sottoquery nidificata per estrarre l'id massimo associato per ciascuna coppia simile di gruppo / età, quindi unirsi a quello per ottenere il resto della riga in base all'ID.
Michael Berkowski,

Questa dovrebbe essere la risposta accettata (la risposta attualmente accettata fallirà sulla maggior parte degli altri RDBMS, e infatti fallirebbe anche su molte versioni di MySQL).
Tim Biegeleisen,

28

La mia semplice soluzione per SQLite (e probabilmente MySQL):

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

Tuttavia, non funziona in PostgreSQL e forse in altre piattaforme.

In PostgreSQL puoi usare la clausola DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;

@Bohemian scusa, lo so, questo è solo MySQL in quanto include colonne non aggregate
Cec

2
@IgorKulagin - Non funziona in Postgres - Messaggio di errore: la colonna "mytable.id" deve apparire nella clausola GROUP BY o essere utilizzata in una funzione aggregata
Yarin,

13
La query MySQL può funzionare solo per caso in molte occasioni. "SELECT *" può restituire informazioni che non corrispondono al MAX (età) di appartenenza. Questa risposta è sbagliata Questo è probabilmente anche il caso di SQLite.
Albert Hendriks,

2
Ma questo si adatta al caso in cui dobbiamo selezionare la colonna raggruppata e la colonna massima. Ciò non corrisponde al requisito sopra indicato dove risulterebbe ("Bob", 1, 42) ma il risultato atteso è ("Shawn", 1, 42)
Ram Babu S

1
Buono per postgres
Karol Gasienica,

4

Utilizzando il metodo di classificazione.

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 - ho bisogno di qualche spiegazione - non l'ho mai visto :=prima - che cos'è?
Yarin,

1
: = è l'operatore di assegnazione. Puoi leggere di più su dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel

Dovrò scavare in questo- Penso che la risposta complichi troppo il nostro scenario, ma grazie per avermi insegnato qualcosa di nuovo ..
Yarin

3

Non sono sicuro che MySQL abbia la funzione row_number. Se è così puoi usarlo per ottenere il risultato desiderato. Su SQL Server puoi fare qualcosa di simile a:

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
Lo fa, dal 8.0.
Ilja Everilä,

2

La soluzione di axiac è ciò che ha funzionato meglio per me alla fine. Avevo comunque un'ulteriore complessità: un "valore massimo" calcolato, derivato da due colonne.

Usiamo lo stesso esempio: vorrei la persona più anziana in ciascun gruppo. Se ci sono persone ugualmente anziane, prendi la persona più alta.

Ho dovuto eseguire il join sinistro due volte per ottenere questo comportamento:

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;

Spero che questo ti aiuti! Immagino che dovrebbe esserci un modo migliore per farlo però ...


2

La mia soluzione funziona solo se è necessario recuperare solo una colonna, tuttavia per le mie esigenze è stata la migliore soluzione trovata in termini di prestazioni (utilizza solo una singola query!):

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

Usa GROUP_CONCAT per creare un elenco di concat ordinato e quindi sottostringa solo al primo.


Può confermare che è possibile ottenere più colonne ordinando sulla stessa chiave all'interno di group_concat, ma è necessario scrivere un group_concat / index / sottostringa separato per ogni colonna.
Rasika,

Il bonus qui è che puoi aggiungere più colonne all'ordinamento all'interno di group_concat e risolverebbe facilmente i legami e garantirà un solo record per gruppo. Complimenti per la soluzione semplice ed efficiente!
Rasika,

2

Ho una soluzione semplice usando 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

Utilizzo di CTE - Espressioni di tabella comuni:

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

In Oracle sotto query può dare il risultato desiderato.

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

Puoi anche provare

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

1
Grazie, anche se questo restituisce più dischi per un'età in cui c'è un pareggio
Yarin,

Inoltre, questa query non sarebbe corretta nel caso in cui ci fosse un 39enne nel gruppo 1. In quel caso, anche quella persona sarebbe selezionata, anche se l'età massima nel gruppo 1 è più alta.
Joshua Richardson,

0

Non userei Group come nome di colonna poiché è una parola riservata. Comunque seguire SQL funzionerebbe.

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

Grazie, anche se questo restituisce più dischi per un'età in cui c'è un pareggio
Yarin,

@Yarin come deciderebbe qual è la persona più anziana corretta? Le risposte multiple sembrano essere la risposta più giusta, altrimenti usa il limite e l'ordine
Duncan,

0

Questo metodo ha il vantaggio di permetterti di classificarti secondo una colonna diversa e di non eliminare gli altri dati. È abbastanza utile in una situazione in cui si sta tentando di elencare gli ordini con una colonna per gli articoli, elencando prima il più pesante.

Fonte: 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

lascia che il nome della tabella sia gente

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

Se è necessario un documento d'identità (e tutti i coulmns) da 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

Ecco come sto ottenendo le N max righe per gruppo in 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
;

come funziona:

  • self join al tavolo
  • i gruppi sono fatti da co.country = ci.country
  • N elementi per gruppo sono controllati da ) < 1così per 3 elementi -) <3
  • per ottenere il massimo o il minimo dipende da: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - min

Esempio completo qui:

mysql seleziona n valori massimi per gruppo

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.