2009-06-30 23 views
70

Desidero poter selezionare un gruppo di righe da una tabella di e-mail e raggrupparle per il mittente. La mia domanda è simile al seguente:MySQL "Raggruppa per" e "Ordina per"

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

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

Ad esempio, potrebbe tornare:

fromEmail: [email protected], subject: hello 
fromEmail: [email protected], subject: welcome 

Quando i record del database sono:

fromEmail: [email protected], subject: hello 
fromEmail: [email protected], subject: programming question 
fromEmail: [email protected], subject: welcome 

Se la "questione di programmazione" soggetto è la più recente, come posso ottenere MySQL per selezionare quel record quando si raggruppano le e-mail?

risposta

110

Una soluzione semplice è quella di avvolgere la query in un sub-SELECT con l'istruzione ORDER primo e applicando il GROUP BY tardi:

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

Questo è simile ad usare il 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 trovata e scarterà il resto. Qualsiasi clausola ORDER BY si applicherà solo al valore della colonna restituita, non a quelli scartati.

IMPORTANTE AGGIORNAMENTO Selezionare le colonne non aggregate utilizzate per funzionare nella pratica ma non dovrebbe essere invocato. Per lo MySQL documentation "questo è utile soprattutto quando tutti i valori in ogni colonna non aggregata non nominata in GROUP BY sono uguali per ciascun gruppo.Il server è libero per scegliere qualsiasi valore da ciascun gruppo, quindi a meno che non siano gli stessi, i valori scelti sono indeterminati. "

A partire da 5.6.21 ho notato problemi con GROUP BY sulla tabella temporanea che ripristina l'ordinamento ORDER BY.

A partire da 5.7.5 ONLY_FULL_GROUP_BY è abilitato per impostazione predefinita, vale a dire che è impossibile utilizzare colonne non aggregate.

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

+0

Ottima idea, non avrei mai pensato di farlo in questo modo. – philwilks

+4

Qualche anno fa mi è venuta in mente la stessa soluzione, ed è un'ottima soluzione. complimenti a bkkich. Due problemi qui però ...GROUP BY è case insensitive così più basso() non è necessaria, e la seconda, $ userID sembra essere una variabile direttamente da PHP, il codice può essere SQL injection vulnerabile se $ userID è fornito dall'utente e non costretto a essere un numero intero. – velcrow

+0

Bella idea. La ringrazio molto –

40

Ecco uno 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) 

In sostanza, si uniscono al tavolo su se stesso, alla ricerca per le righe successive. Nella clausola where dichiari che non ci possono essere righe successive. Questo ti dà solo l'ultima riga.

Se possono esserci più e-mail con lo stesso timestamp, questa query richiede la rifinitura. Se c'è una colonna ID incrementale nella tabella posta elettronica, modificare il join come:

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

Detto che 'textID' era ambiguo =/ –

+1

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

+0

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

21

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

ONLY_FULL_GROUP_BY

Si dovrebbe prima selezionare fromEmail, MIN (leggi), e poi, con la seconda query (o subquery) - Soggetto.

+0

MIN (leggi) restituirebbe il valore minimo di "lettura". Probabilmente sta cercando il flag "read" dell'ultima email. – Andomar

2

Ho lottato con entrambi questi approcci per le query più complesse rispetto a quelli mostrati, perché l'approccio sottoquery era orribilmente ineficient non importa quello che ho messo su indici, e perché non potevo ottenere il self-join esterno tramite Hibernate

Il modo migliore (e più semplice) per fare ciò è raggruppare per qualcosa che è costruito per contenere una concatenazione dei campi richiesti e quindi per estrarli usando le espressioni in SELECT clausola. Se hai bisogno di fare un MAX() assicurati che il campo che vuoi MAX() sia sempre alla fine più significativa dell'entità concatenata.

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

se è possibile ottenere am inserto/evento di aggiornamento (come un trigger) per pre-calcolare la concatenazione dei campi è possibile indicizzarlo e la query sarà veloce come se il gruppo da era finito solo il campo in realtà voleva MAX(). Puoi persino usarlo per ottenere il massimo da più campi. Lo uso per fare interrogazioni su alberi multi-dimensionali espressi come insiemi nidificati.

24

Fare un GROUP BY dopo ORDER BY avvolgendo la query con GROUP BY in questo modo:

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

Grazie questo ha funzionato perfettamente per me su una domanda simlar che stavo facendo. – Mark

+0

Quindi GROUP BY' seleziona automaticamente l'ora più recente o il tempo più recente o casuale? – xrDDDD

+0

Seleziona il tempo più recente perché siamo ordinare da 'tempo desc' e poi il gruppo dalla prende il primo (ultimo). – 11101101b

12

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

Se si sta usando MySQL 5.6 o 5.7 con MySQL ONLY_FULL_GROUP_BY, il corretto (deterministico) query è:

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 da eseguire in modo efficiente, è necessario corretta indicizzazione.

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