MySQL "Group By" e "Order By"


96

Voglio essere in grado di selezionare un gruppo di righe da una tabella di e-mail e raggrupparle in base al mittente. La mia domanda è simile a questa:

SELECT 
    `timestamp`, `fromEmail`, `subject`
FROM `incomingEmails` 
GROUP BY LOWER(`fromEmail`) 
ORDER BY `timestamp` DESC

La query funziona quasi come la desidero: seleziona i record raggruppati tramite posta elettronica. Il problema è che l'oggetto e il timestamp non corrispondono al record più recente per un particolare indirizzo e-mail.

Ad esempio, potrebbe restituire:

fromEmail: john@example.com, subject: hello
fromEmail: mark@example.com, subject: welcome

Quando i record nel database sono:

fromEmail: john@example.com, subject: hello
fromEmail: john@example.com, subject: programming question
fromEmail: mark@example.com, subject: welcome

Se l'oggetto della "domanda di programmazione" è il più recente, come posso fare in modo che MySQL selezioni quel record quando raggruppo le e-mail?

Risposte:


140

Una soluzione semplice è racchiudere la query in una sottoselezione con l'istruzione ORDER prima e applicare successivamente GROUP BY :

SELECT * FROM ( 
    SELECT `timestamp`, `fromEmail`, `subject`
    FROM `incomingEmails` 
    ORDER BY `timestamp` DESC
) AS tmp_table GROUP BY LOWER(`fromEmail`)

È simile all'uso del join ma sembra molto più bello.

L'utilizzo di colonne non aggregate in un SELECT con una clausola GROUP BY non è standard. MySQL generalmente restituirà i valori della prima riga che trova e scarterà il resto. Qualsiasi clausola ORDER BY si applicherà solo al valore della colonna restituito, non a quelli scartati.

AGGIORNAMENTO IMPORTANTE La selezione di colonne non aggregate funzionava nella pratica ma non dovrebbe essere invocata. Secondo la documentazione di MySQL "questo è utile principalmente quando tutti i valori in ogni colonna non aggregata non denominata in GROUP BY sono gli stessi per ogni gruppo. Il server è libero di scegliere qualsiasi valore da ciascun gruppo, quindi a meno che non siano gli stessi, i valori scelti sono indeterminati . "

A partire da 5.7.5 ONLY_FULL_GROUP_BY è abilitato per impostazione predefinita, quindi le colonne non aggregate causano errori di query (ER_WRONG_FIELD_WITH_GROUP)

Come @mikep sottolinea di seguito, la soluzione è usare ANY_VALUE () da 5.7 e superiori

Vedi http://www.cafewebmaster.com/mysql-order-sort-group https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html https: //dev.mysql .com / doc / refman / 5.7 / en / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value


7
Ho trovato la stessa soluzione alcuni anni fa ed è un'ottima soluzione. complimenti a b7kich. Due problemi qui però ... GROUP BY non fa distinzione tra maiuscole e minuscole, quindi LOWER () non è necessario, e in secondo luogo, $ userID sembra essere una variabile direttamente da PHP, il tuo codice potrebbe essere vulnerabile a sql injection se $ userID è fornito dall'utente e non forzato essere un numero intero.
velcrow

L'AGGIORNAMENTO IMPORTANTE si applica anche a MariaDB: mariadb.com/kb/en/mariadb/…
Arthur Shipkowski

1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.La modalità SQL può essere modificata durante il runtime senza privilegi di amministratore, quindi è molto facile disabilitare ONLY_FULL_GROUP_BY. Ad esempio: SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep

1
O un'altra alternativa per bypassare ONLY_FULL_GROUP_BY abilitato è usare ANY_VALUE (). Vedi di più dev.mysql.com/doc/refman/8.0/en/…
mikep

42

Ecco un approccio:

SELECT cur.textID, cur.fromEmail, cur.subject, 
     cur.timestamp, cur.read
FROM incomingEmails cur
LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.timestamp < next.timestamp
WHERE next.timestamp is null
and cur.toUserID = '$userID' 
ORDER BY LOWER(cur.fromEmail)

Fondamentalmente, ti unisci al tavolo su se stesso, cercando le righe successive. Nella clausola where si afferma che non possono esserci righe successive. Questo ti dà solo l'ultima riga.

Se possono esserci più e-mail con lo stesso timestamp, questa query dovrebbe essere raffinata. Se è presente una colonna ID incrementale nella tabella email, modifica il JOIN come:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id

Ha detto che textIDera ambiguo = /
John Kurlak,

1
Quindi rimuovere l'ambiguità e aggiungere il prefisso al nome della tabella, come cur.textID. Modificato anche nella risposta.
Andomar

Questa è l'unica soluzione possibile con Doctrine DQL.
VisioN

Questo non funziona quando stai tentando di unirti automaticamente a più colonne così bene. IE quando stai cercando di trovare l'ultima e-mail e il nome utente più recente e hai bisogno di più join self left per eseguire questa operazione in una singola query.
Loveen Dyall

Quando si lavora con timestamp / date passati e futuri, per limitare il set di risultati a date non future, è necessario aggiungere un'altra condizione ai LEFT JOINcriteriAND next.timestamp <= UNIX_TIMESTAMP()
fyrye

32

Come già indicato in una risposta, la risposta corrente è sbagliata, perché GROUP BY seleziona arbitrariamente il record dalla finestra.

Se si utilizza MySQL 5.6 o MySQL 5.7 con ONLY_FULL_GROUP_BY, la query corretta (deterministica) è:

SELECT incomingEmails.*
  FROM (
    SELECT fromEmail, MAX(timestamp) `timestamp`
    FROM incomingEmails
    GROUP BY fromEmail
  ) filtered_incomingEmails
  JOIN incomingEmails USING (fromEmail, timestamp)
GROUP BY fromEmail, timestamp

Affinché la query venga eseguita in modo efficiente, è necessaria un'indicizzazione adeguata.

Si noti che per motivi di semplificazione, ho rimosso il LOWER(), che nella maggior parte dei casi non verrà utilizzato.


2
Questa dovrebbe essere la risposta corretta. Ho appena scoperto un bug sul mio sito web relativo a questo. Il order bynella sottoselezione nelle altre risposte, non ha alcun effetto.
Jette

1
OMG, per favore rendi questa la risposta accettata. Quello accettato ha perso 5 ore del mio tempo :(
Richard Kersey

29

Esegui un GROUP BY dopo ORDER BY racchiudendo la tua query con GROUP BY in questo modo:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from

1
Quindi il GRUPPO BY` seleziona automaticamente l'ultimo time, il più recente timeo casuale?
xrDDDD

1
Seleziona l'ora più recente perché stiamo ordinando entro time DESCe poi il gruppo prende la prima (l'ultima).
11101101b

Ora, se solo potessi fare JOINS sulle sotto-selezioni in VIEWS, in mysql 5.1. Forse quella funzione è disponibile in una versione più recente.
IcarusNM

21

Secondo lo standard SQL non è possibile utilizzare colonne non aggregate nell'elenco di selezione. MySQL consente tale utilizzo (è utilizzata la modalità ONLY_FULL_GROUP_BY) ma il risultato non è prevedibile.

ONLY_FULL_GROUP_BY

Dovresti prima selezionare da Email, MIN (lettura) e poi, con la seconda query (o sottoquery) - Oggetto.


MIN (read) restituirà il valore minimo di "read". Probabilmente sta cercando invece il flag di "lettura" dell'ultima email.
Andomar

2

Ho lottato con entrambi questi approcci per query più complesse di quelle mostrate, perché l'approccio della subquery era orribilmente inefficace indipendentemente dagli indici che avevo inserito, e perché non riuscivo a ottenere l'auto-join esterno tramite Hibernate

Il modo migliore (e più semplice) per farlo è raggruppare per qualcosa che è costruito per contenere una concatenazione dei campi richiesti e poi estrarli usando le espressioni nella clausola SELECT. Se è necessario eseguire MAX (), assicurarsi che il campo su cui si desidera MAX () sia sempre all'estremità più significativa dell'entità concatenata.

La chiave per comprenderlo è che la query può avere senso solo se questi altri campi sono invarianti per qualsiasi entità che soddisfi Max (), quindi in termini di ordinamento gli altri pezzi della concatenazione possono essere ignorati. Spiega come farlo in fondo a questo collegamento. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Se riesci a ottenere un evento di inserimento / aggiornamento (come un trigger) per pre-calcolare la concatenazione dei campi, puoi indicizzarlo e la query sarà veloce come se il gruppo fosse sopra solo il campo che desideri effettivamente MAX ( ). Puoi persino usarlo per ottenere il massimo di più campi. Lo uso per eseguire query su alberi multidimensionali espressi come insiemi annidati.

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.