Ottieni i primi n record per ogni gruppo di risultati raggruppati


140

L'esempio seguente è il più semplice possibile, sebbene qualsiasi soluzione dovrebbe essere in grado di scalare in base alla necessità di molti n risultati migliori:

Data una tabella come quella qui sotto, con le colonne persona, gruppo ed età, come otterresti le 2 persone più anziane in ciascun gruppo? (I legami all'interno dei gruppi non dovrebbero produrre più risultati, ma fornire i primi 2 in ordine alfabetico)

+ -------- + ------- + ----- +
| Persona | Gruppo | Età |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paolo | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

Set di risultati desiderato:

+ -------- + ------- + ----- +
| Shawn | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paolo | 2 | 36 |
+ -------- + ------- + ----- +

NOTA: questa domanda si basa su una precedente. Ottieni record con il valore massimo per ciascun gruppo di risultati SQL raggruppati , per ottenere una riga superiore da ciascun gruppo e che ha ricevuto un'ottima risposta specifica per MySQL da @Bohemian:

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

Mi piacerebbe poterlo costruire, anche se non vedo come.



2
Guarda questo esempio. È praticamente vicino a ciò che chiedi: stackoverflow.com/questions/1537606/…
Savas Vedova

Usando LIMIT in GROUP BY per ottenere N risultati per gruppo? stackoverflow.com/questions/2129693/…
Edye Chan

Risposte:


88

Ecco un modo per farlo, usando UNION ALL(Vedi SQL Fiddle with Demo ). Funziona con due gruppi, se hai più di due gruppi, dovrai specificare il groupnumero e aggiungere query per ciascuno group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Esistono diversi modi per farlo, consulta questo articolo per determinare il percorso migliore per la tua situazione:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Modificare:

Questo potrebbe funzionare anche per te, genera un numero di riga per ogni record. Usando un esempio dal link sopra questo restituirà solo quei record con un numero di riga inferiore o uguale a 2:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

Vedi la demo


52
se avesse più di 1 000 gruppi, questo non lo renderebbe un po 'spaventoso?
Charles Forest,

1
@CharlesForest sì, sarebbe ed è per questo che ho dichiarato che avresti dovuto specificarlo per più di due gruppi. Diventerebbe brutto.
Taryn

1
@CharlesForest Penso di aver trovato una soluzione migliore, vedi la mia modifica
Taryn

1
Una nota per chiunque legga questo: la versione è che le variabili stanno per essere corrette. Tuttavia, MySQL non garantisce l'ordine di valutazione delle espressioni nel SELECT(e, di fatto, a volte le valuta in modo anomalo). La chiave della soluzione è mettere tutte le assegnazioni di variabili in una singola espressione; ecco un esempio: stackoverflow.com/questions/38535020/… .
Gordon Linoff,

1
@GordonLinoff Aggiornato la mia risposta, grazie per averlo sottolineato. Ci è voluto anche troppo tempo per aggiornarlo.
Taryn

63

In altri database è possibile farlo utilizzando ROW_NUMBER. MySQL non supporta ROW_NUMBERma è possibile utilizzare le variabili per emularlo:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Guardalo online: sqlfiddle


Modifica Ho appena notato che bluefeet ha pubblicato una risposta molto simile: +1 a lui. Tuttavia questa risposta ha due piccoli vantaggi:

  1. È una singola query. Le variabili sono inizializzate all'interno dell'istruzione SELECT.
  2. Gestisce i legami come descritto nella domanda (ordine alfabetico per nome).

Quindi lo lascerò qui nel caso in cui possa aiutare qualcuno.


1
Mark- Questo funziona bene per noi. Grazie per aver fornito un'altra buona alternativa al complimento @ bluefeet's- molto apprezzato.
Yarin,

+1. Questo ha funzionato per me. Davvero pulito e al punto di risposta. Puoi spiegare come funziona esattamente? Qual è la logica dietro questo?
Aditya Hajare,

3
Bella soluzione ma sembra che non funzioni nel mio ambiente (MySQL 5.6) perché la clausola order by viene applicata dopo la selezione, quindi non restituisce il risultato migliore, vedere la mia soluzione alternativa per risolvere questo problema
Laurent PELE

Durante l'esecuzione sono stato in grado di eliminare JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Ho avuto l'idea di dichiarare variabili vuote, ma sembra estraneo per MySql.
Joseph Cho,

1
Funziona benissimo per me in MySQL 5.7, ma sarebbe fantastico se qualcuno potesse spiegare come funziona
George B

41

Prova questo:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

DEMO


6
tabacco da fiuto che esce dal nulla con la soluzione più semplice! È più elegante di quello di Ludo / Bill Karwin ? Posso avere qualche commento
Yarin,

Hm, non sono sicuro che sia più elegante. Ma a giudicare dai voti, immagino che i bluefeet potrebbero avere la soluzione migliore.
Snuffn

2
C'è un problema con questo. Se c'è un pareggio per il secondo posto all'interno del gruppo, viene restituito solo un risultato migliore. Guarda la demo
Yarin,

2
Non è un problema se lo si desidera. È possibile impostare l'ordine di a.person.
Alberto Leal,

no, nel mio caso non funziona, né funziona la DEMO
Choix

31

Che ne dici di usare l'auto-unione:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

mi da:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Sono stato fortemente ispirato dalla risposta di Bill Karwin a selezionare i primi 10 record per ogni categoria

Inoltre, sto usando SQLite, ma questo dovrebbe funzionare su MySQL.

Un'altra cosa: in precedenza, ho sostituito la groupcolonna con una groupnamecolonna per comodità.

Modifica :

In seguito al commento del PO sui risultati dei pareggi mancanti, ho incrementato la risposta di Snuffin per mostrare tutti i legami. Ciò significa che se gli ultimi sono legami, è possibile restituire più di 2 righe, come mostrato di seguito:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

mi da:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      

@ Ludo- Ho appena visto la risposta di Bill Karwin - grazie per averla applicata qui
Yarin,

Cosa ne pensi della risposta di Snuffin? Sto cercando di confrontare i due
Yarin

2
C'è un problema con questo. Se c'è un pareggio per il secondo posto all'interno del gruppo, viene restituito solo un risultato migliore- Vedi la demo
Yarin

1
@ Ludo- il requisito originale era che ogni gruppo restituisse esattamente n risultati, con eventuali legami risolti in ordine alfabetico
Yarin

La modifica per includere i legami non funziona per me. Presumo ERROR 1242 (21000): Subquery returns more than 1 row, presumibilmente a causa del GROUP BY. Quando eseguo la SELECT MINsubquery da sola, genera tre righe: 34, 39, 112e lì sembra che il secondo valore dovrebbe essere 36, non 39.
verbamour

12

La soluzione di Snuffin sembra piuttosto lenta da eseguire quando hai molte righe e Mark Byers / Rick James e le soluzioni Bluefeet non funzionano sul mio ambiente (MySQL 5.6) perché l'ordine viene applicato dopo l'esecuzione di select, quindi ecco una variante delle soluzioni Marc Byers / Rick James per risolvere questo problema (con una selezione extra imbricated):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

Ho provato una query simile su una tabella con 5 milioni di righe e restituisce il risultato in meno di 3 secondi


3
Questa è l'unica query che ha funzionato nel mio ambiente. Grazie!
Herrherr,

3
Aggiungi LIMIT 9999999a qualsiasi tabella derivata con unORDER BY . Questo potrebbe impedire ORDER BYdi essere ignorato.
Rick James,

Ho eseguito una query simile su una tabella contenente alcune migliaia di righe e ci sono voluti 60 secondi per restituire un risultato, quindi ... grazie per il post, è un inizio per me. (ETA: fino a 5 secondi. Bene!)
Evan

10

Controllalo:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

SQL Fiddle: http://sqlfiddle.com/#!2/cdbb6/15


5
Amico, altri hanno trovato soluzioni molto più semplici ... Ho appena trascorso circa 15 minuti su questo ed ero incredibilmente orgoglioso di me stesso per aver escogitato una soluzione così complicata. Che schifo
Travesty3,

Ho dovuto trovare un numero di versione interno che era 1 in meno rispetto all'attuale - questo mi ha dato la risposta per fare questo: max(internal_version - 1)- quindi meno stress :)
Jamie Strauss

8

Se le altre risposte non sono abbastanza veloci Prova questo codice :

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Produzione:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...

Ho guardato il tuo sito: dove troverei l'origine dati per le popolazioni delle città? TIA e rgs.
Vérace,

maxmind.com/en/worldcities - Lo trovo utile per sperimentare ricerche , query, partizioni lat / lng , ecc. È abbastanza grande per essere interessante, ma abbastanza leggibile per riconoscere le risposte. Il sottoinsieme canadese è utile per questo tipo di domanda. (Meno province rispetto alle città statunitensi.)
Rick James,

2

Volevo condividere questo perché ho trascorso molto tempo a cercare un modo semplice per implementarlo in un programma Java su cui sto lavorando. Questo non dà esattamente l'output che stai cercando, ma è vicino. La funzione chiamata mysql ha GROUP_CONCAT()funzionato davvero bene per specificare quanti risultati restituire in ciascun gruppo. L'uso LIMITo uno qualsiasi degli altri modi fantasiosi per provare a farlo con COUNTnon ha funzionato per me. Quindi, se sei disposto ad accettare un output modificato, è un'ottima soluzione. Diciamo che ho un tavolo chiamato 'studente' con ID degli studenti, il loro genere e gpa. Diciamo che voglio top 5 gpas per ogni genere. Quindi posso scrivere la query in questo modo

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Si noti che il parametro '5' indica quante voci concatenare in ciascuna riga

E l'output sarebbe simile

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Puoi anche cambiare la ORDER BYvariabile e ordinarle in un modo diverso. Quindi, se avessi l'età dello studente, potrei sostituire la "gpa desc" con "age desc" e funzionerà! È inoltre possibile aggiungere variabili al gruppo in base all'istruzione per ottenere più colonne nell'output. Quindi questo è solo un modo in cui ho scoperto che è abbastanza flessibile e funziona bene se stai bene solo con i risultati della lista.


0

In SQL Server row_numer()è presente una potente funzione che può ottenere facilmente risultati come di seguito

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

Con 8.0 e 10.2 che sono GA, questa risposta sta diventando ragionevole.
Rick James,

@RickJames cosa significa "essere GA"? Le funzioni della finestra ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) hanno risolto molto bene il mio problema.
iedmrc,

1
@iedmrc - "GA" significa "Generalmente disponibile". È un linguaggio tecnico per "pronto per la prima serata" o "rilasciato". Stanno sviluppando la versione e si concentreranno sui bug che hanno perso. Questo link discute l'implementazione di MySQL 8.0, che potrebbe essere diversa dall'implementazione di MariaDB 10.2.
Rick James,

-1

MySQL ha una risposta davvero interessante a questo problema : come ottenere le migliori righe per ciascun gruppo

In base alla soluzione nel collegamento di riferimento, la query sarebbe simile a:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

dove nè il top ned your_tableè il nome della tabella.

Penso che la spiegazione nel riferimento sia davvero chiara. Per una rapida consultazione, lo copierò e incollerò qui:

Attualmente MySQL non supporta la funzione ROW_NUMBER () che può assegnare un numero di sequenza all'interno di un gruppo, ma come soluzione alternativa possiamo usare le variabili di sessione MySQL.

Queste variabili non richiedono una dichiarazione e possono essere utilizzate in una query per eseguire calcoli e archiviare risultati intermedi.

@current_country: = country Questo codice viene eseguito per ogni riga e memorizza il valore della colonna country nella variabile @current_country.

@country_rank: = IF (@current_country = country, @country_rank + 1, 1) In questo codice, se @current_country è lo stesso, incrementiamo il ranking, altrimenti lo impostiamo su 1. Per la prima riga @current_country è NULL, quindi il ranking è anche impostato su 1.

Per un corretto posizionamento, è necessario disporre di ORDINA PER Paese, popolazione DESC


Bene, è il principio usato dalle soluzioni di Marc Byers, Rick James e il mio.
Laurent PELE,

Difficile dire quale post (StackTranslate.it o SQLlines) è stato il primo
Laurent PELE il

@LaurentPELE - Il mio è stato pubblicato febbraio 2015. Non vedo data o ora su SQLlines. I blog di MySQL sono in circolazione da abbastanza tempo che alcuni di essi sono obsoleti e dovrebbero essere rimossi - le persone citano informazioni errate.
Rick James,
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.