2009-06-30 14 views
70

Quiero poder seleccionar un grupo de filas de una tabla de correos electrónicos y agruparlas por el remitente. Mi consulta es el siguiente:MySQL "Agrupar por" y "Ordenar por"

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

La consulta casi funciona como yo quiero que - se selecciona registros agrupados por e-mail. El problema es que el tema y la marca de tiempo no corresponden al registro más reciente de una dirección de correo electrónico en particular.

Por ejemplo, podría volver:

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

Cuando los registros de la base de datos son:

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

Si la "cuestión de programación" sujeto es la más reciente, ¿cómo puedo obtener MySQL para seleccionar ese registro al agrupar los correos electrónicos?

Respuesta

110

Una solución sencilla es envolver la consulta en una subselección con la declaración ORDEN primera y aplicando el GRUPO POR tarde:

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

Esto es similar al uso de la unión, pero parece mucho más agradable.

El uso de columnas no agregadas en un SELECCIONAR con una cláusula GROUP BY no es estándar. MySQL generalmente devolverá los valores de la primera fila que encuentre y descartará el resto. Cualquier cláusula ORDER BY solo se aplicará al valor devuelto de la columna, no a los descartados.

ACTUALIZACIÓN IMPORTANTE Seleccionar columnas no agregadas usadas para trabajar en la práctica, pero no se debe confiar en ellas. Por el MySQL documentation "esto es útil principalmente cuando todos los valores en cada columna no agregada no nombrada en GROUP BY son iguales para cada grupo. El servidor es libre de elegir cualquier valor de cada grupo, entonces a menos que sean iguales, los valores elegidos son indeterminados ".

A partir del 5.6.21 he notado problemas con GROUP BY en la tabla temporal que revierte la clasificación ORDER BY.

A partir del 5.7.5 ONLY_FULL_GROUP_BY está habilitado de manera predeterminada, es decir, es imposible usar columnas no agregadas.

Ver 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

Una gran idea, nunca hubiera pensado hacerlo de esta manera. – philwilks

+4

Me surgió la misma solución hace unos años, y es una gran solución. felicitaciones a b7kich. Dos problemas aquí ...GROUP BY no distingue entre mayúsculas y minúsculas, por lo que LOWER() es innecesario y, en segundo lugar, $ userID parece ser una variable directamente de PHP, su código puede ser vulnerable a inyección SQL si $ userID es proporcionado por el usuario y no forzado a ser un entero. – velcrow

+0

Idea agradable. Muchas gracias –

40

Aquí es uno de los enfoques:

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) 

Básicamente, se unen a la tabla de la misma, en busca de filas posteriores. En la cláusula where indica que no puede haber filas posteriores. Esto le da solo la última fila.

Si puede haber varios correos electrónicos con la misma marca de tiempo, esta consulta necesitaría refinarse. Si hay una columna de ID incrementales en la tabla de correo electrónico, cambiar la junta como:

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

Dijo que 'textID' era ambiguo =/ –

+1

Luego elimine la ambuidad y póngala por el nombre de la tabla, como cur.textID. Cambiado en la respuesta también. – Andomar

+0

Esta es la única solución que se puede hacer con Doctrine DQL. – VisioN

21

Según el estándar SQL no se puede utilizar columnas no agregadas en la lista de selección. MySQL permite dicho uso (sin usar el modo ONLY_FULL_GROUP_BY) pero el resultado no es predecible.

ONLY_FULL_GROUP_BY

primer lugar, debe seleccionar fromEmail, MIN (lectura), y luego, con la segunda consulta (o subconsulta) - Asunto.

+0

MIN (leer) devolvería el valor mínimo de "leer". Probablemente esté buscando la bandera de "lectura" del último correo electrónico. – Andomar

2

tuve problemas con estos dos enfoques para consultas más complejas que las que se muestran, porque el enfoque sub consulta era terriblemente ineficient no importa lo que los índices Me puse, y porque no podía obtener la auto-unión externa a través de Hibernate

La mejor (y más fácil) forma de hacerlo es agrupar por algo que está construido para contener una concatenación de los campos que necesita y luego sacarlos usando expresiones en el SELECT cláusula. Si necesita hacer un MAX(), asegúrese de que el campo sobre el que desea MAX() esté siempre en el extremo más significativo de la entidad concatenada.

La clave para entender esto es que la consulta solo puede tener sentido si estos otros campos son invariables para cualquier entidad que satisfaga Max(), por lo que en términos del orden, las otras piezas de la concatenación pueden ignorarse. Explica cómo hacer esto en la parte inferior de este enlace. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Si puede obtener un evento de inserción/actualización (como un desencadenador) para precalcular la concatenación de los campos puede indexarlo y la consulta será tan rápida como si el grupo estuviera sobre el campo que en realidad quería MAX(). Incluso puede usarlo para obtener el máximo de múltiples campos. Lo uso para hacer consultas contra árboles multidimensionales expresados ​​como conjuntos anidados.

24

Hacer un GROUP BY después de que el ORDER BY envolviendo la consulta con el GRUPO POR así:

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

Gracias esto funcionó perfecto para mí en una consulta simlar que estaba haciendo. – Mark

+0

Entonces, ¿POR QUÉ GROUP BY' selecciona automáticamente la última "hora", o la última "hora", o al azar? – xrDDDD

+0

Selecciona el tiempo más nuevo porque estamos ordenando por 'tiempo DESC' y luego el grupo toma el primero (más reciente). – 11101101b

12

Como se señaló en una respuesta ya, la respuesta actual es incorrecto, ya que el GROUP BY selecciona arbitrariamente el grabar desde la ventana.

Si uno está usando MySQL 5.6, o MySQL 5.7 con ONLY_FULL_GROUP_BY, el (determinista) consulta correcta es:

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

Para que la consulta se ejecute de manera eficiente, se requiere una correcta indexación.

Tenga en cuenta que, para simplificar, he eliminado el LOWER(), que en la mayoría de los casos, no se utilizará.