MySQL "Agrupar por" y "Ordenar por"


96

Quiero poder seleccionar un montón de filas de una tabla de correos electrónicos y agruparlas por remitente. Mi consulta se ve así:

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

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

Por ejemplo, podría devolver:

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

Cuando los registros en la base de datos son:

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

Si el tema de la "pregunta de programación" es el más reciente, ¿cómo puedo hacer que MySQL seleccione ese registro al agrupar los correos electrónicos?

Respuestas:


140

Una solución simple es envolver la consulta en una subselección con la instrucción ORDER primero y aplicar GROUP BY más tarde :

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

Esto es similar a usar la combinación, pero se ve mucho mejor.

El uso de columnas no agregadas en un SELECT 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 de columna devuelto, no a las descartadas.

ACTUALIZACIÓN IMPORTANTE La selección de columnas no agregadas solía funcionar en la práctica, pero no se debe confiar en ellas. Según la documentación de MySQL, "esto es útil principalmente cuando todos los valores en cada columna no agregada no nombrada en GROUP BY son los mismos para cada grupo. El servidor es libre de elegir cualquier valor de cada grupo, así que a menos que sean iguales, los valores elegidos son indeterminados ".

A partir de 5.7.5, ONLY_FULL_GROUP_BY está habilitado de forma predeterminada, por lo que las columnas no agregadas causan errores de consulta (ER_WRONG_FIELD_WITH_GROUP)

Como @mikep señala a continuación, la solución es usar ANY_VALUE () de 5.7 y superior

Consulte 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 / es / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/mis Miscellaneous-functions.html#function_any-value


7
Se me ocurrió la misma solución hace unos años, y es una gran solución. felicitaciones a b7kich. Sin embargo, hay 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 la inyección de SQL si $ userID es proporcionado por el usuario y no forzado ser un número entero.
velcrow

La ACTUALIZACIÓN IMPORTANTE también se aplica 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.El modo SQL se puede cambiar durante el tiempo de ejecución sin privilegios de administrador, por lo que es muy fácil deshabilitar ONLY_FULL_GROUP_BY. Por ejemplo: SET SESSION sql_mode = '';. Demostración: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep

1
O otra alternativa para omitir habilitado ONLY_FULL_GROUP_BY es usar ANY_VALUE (). Ver más dev.mysql.com/doc/refman/8.0/en/...
Mikep

42

Aquí hay un enfoque:

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 une a la tabla sobre sí misma, buscando 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 debería perfeccionarse. Si hay una columna de ID incremental en la tabla de correo electrónico, cambie JOIN como:

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

Dijo que textIDera ambiguo = /
John Kurlak

1
Luego elimine la ambigüedad y anteponga el nombre de la tabla, como cur.textID. También cambió la respuesta.
Andomar

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

Esto no funciona cuando intentas unirte tú mismo para varias columnas tan bien. IE cuando está tratando de encontrar el correo electrónico más reciente y el nombre de usuario más reciente y necesita varias uniones a la izquierda para realizar esta operación en una sola consulta.
Loveen Dyall

Al trabajar con marcas de tiempo / fechas pasadas y futuras, para limitar el conjunto de resultados a fechas no futuras, debe agregar otra condición a los LEFT JOINcriteriosAND next.timestamp <= UNIX_TIMESTAMP()
fyrye

32

Como ya se señaló en una respuesta, la respuesta actual es incorrecta, porque GROUP BY selecciona arbitrariamente el registro de la ventana.

Si uno está usando MySQL 5.6 o MySQL 5.7 con ONLY_FULL_GROUP_BY, la consulta correcta (determinista) 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 indexación adecuada.

Tenga en cuenta que, por motivos de simplificación, eliminé el LOWER(), que en la mayoría de los casos no se utilizará.


2
Esta debería ser la respuesta correcta. Acabo de descubrir un error en mi sitio web relacionado con esto. El order byen la subselección en las otras respuestas, no tiene ningún efecto.
Jette

1
Dios mío, haz que esta sea la respuesta aceptada. El aceptado desperdició 5 horas de mi tiempo :(
Richard Kersey

29

Haga un GROUP BY después de ORDER BY envolviendo su consulta con GROUP BY de esta manera:

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

1
Entonces, ¿GROUP BY` selecciona automáticamente el último time, o el más nuevo time, o al azar?
xrDDDD

1
Selecciona la hora más nueva porque estamos ordenando por time DESCy luego el grupo por toma la primera (la última).
11101101b

Ahora, si solo pudiera hacer JOINS en sub-selecciones en VIEWS, en mysql 5.1. Quizás esa característica venga en una versión más reciente.
IcarusNM

21

Según el estándar SQL, no puede usar columnas no agregadas en la lista de selección. MySQL permite tal uso (a menos que se use el modo ONLY_FULL_GROUP_BY) pero el resultado no es predecible.

ONLY_FULL_GROUP_BY

Primero debe seleccionar desde Correo electrónico, MIN (lectura) y luego, con la segunda consulta (o subconsulta) - Asunto.


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

2

Luché con estos dos enfoques para consultas más complejas que las que se muestran, porque el enfoque de subconsultas era terriblemente ineficiente sin importar los índices que puse, y porque no pude obtener la autounión externa a través de Hibernate

La mejor (y más fácil) forma de hacer esto es agrupar por algo que esté construido para contener una concatenación de los campos que necesita y luego extraerlos usando expresiones en la cláusula SELECT. 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 invariantes para cualquier entidad que satisfaga el Max (), por lo que en términos del tipo, las otras partes 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 disparador) para precalcular la concatenación de los campos, puede indexarlo y la consulta será tan rápida como si el grupo por estuviera sobre el campo que realmente deseaba MAX ( ). Incluso puede usarlo para obtener el máximo de múltiples campos. Lo uso para hacer consultas en árboles multidimensionales expresados ​​como conjuntos anidados.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.