Recuperando el último registro de cada grupo - MySQL
Hay una tabla messages
que contiene datos como se muestra a continuación:
Id Name Other_Columns
-------------------------
1 A A_data_1
2 A A_data_2
3 A A_data_3
4 B B_data_1
5 B B_data_2
6 C C_data_1
Si ejecuto una consulta select * from messages group by name
, obtendré el resultado como:
1 A A_data_1
4 B B_data_1
6 C C_data_1
¿Qué consulta devolverá el siguiente resultado?
3 A A_data_3
5 B B_data_2
6 C C_data_1
Es decir, se debe devolver el último registro de cada grupo.
Actualmente, esta es la consulta que uso:
SELECT
*
FROM (SELECT
*
FROM messages
ORDER BY id DESC) AS x
GROUP BY name
Pero esto parece muy ineficiente. ¿Alguna otra forma de lograr el mismo resultado?
MySQL 8.0 ahora admite funciones de ventanas , como casi todas las implementaciones SQL populares. Con esta sintaxis estándar, podemos escribir consultas con el mayor número de n por grupo:
WITH ranked_messages AS (
SELECT m.*, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rn
FROM messages AS m
)
SELECT * FROM ranked_messages WHERE rn = 1;
Este y otros enfoques para encontrar filas máximas por grupos se ilustran en el manual de MySQL.
A continuación se muestra la respuesta original que escribí para esta pregunta en 2009:
Escribo la solución de esta manera:
SELECT m1.*
FROM messages m1 LEFT JOIN messages m2
ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL;
En cuanto al rendimiento, una solución u otra puede ser mejor, dependiendo de la naturaleza de tus datos. Por lo tanto, debe probar ambas consultas y utilizar la que tenga mejor rendimiento según su base de datos.
Por ejemplo, tengo una copia del volcado de datos de agosto de StackOverflow . Lo usaré para realizar evaluaciones comparativas. Hay 1.114.357 filas en la Posts
tabla. Esto se ejecuta en MySQL 5.0.75 en mi Macbook Pro 2.40GHz.
Escribiré una consulta para encontrar la publicación más reciente para un ID de usuario determinado (el mío).
Primero usando la técnica mostrada por @Eric con GROUP BY
en una subconsulta:
SELECT p1.postid
FROM Posts p1
INNER JOIN (SELECT pi.owneruserid, MAX(pi.postid) AS maxpostid
FROM Posts pi GROUP BY pi.owneruserid) p2
ON (p1.postid = p2.maxpostid)
WHERE p1.owneruserid = 20860;
1 row in set (1 min 17.89 sec)
Incluso el EXPLAIN
análisis dura más de 16 segundos:
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 76756 | |
| 1 | PRIMARY | p1 | eq_ref | PRIMARY,PostId,OwnerUserId | PRIMARY | 8 | p2.maxpostid | 1 | Using where |
| 2 | DERIVED | pi | index | NULL | OwnerUserId | 8 | NULL | 1151268 | Using index |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
3 rows in set (16.09 sec)
Ahora produzca el mismo resultado de consulta usando mi técnica con LEFT JOIN
:
SELECT p1.postid
FROM Posts p1 LEFT JOIN posts p2
ON (p1.owneruserid = p2.owneruserid AND p1.postid < p2.postid)
WHERE p2.postid IS NULL AND p1.owneruserid = 20860;
1 row in set (0.28 sec)
El EXPLAIN
análisis muestra que ambas tablas pueden utilizar sus índices:
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| 1 | SIMPLE | p1 | ref | OwnerUserId | OwnerUserId | 8 | const | 1384 | Using index |
| 1 | SIMPLE | p2 | ref | PRIMARY,PostId,OwnerUserId | OwnerUserId | 8 | const | 1384 | Using where; Using index; Not exists |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
2 rows in set (0.00 sec)
Aquí está el DDL para mi Posts
mesa:
CREATE TABLE `posts` (
`PostId` bigint(20) unsigned NOT NULL auto_increment,
`PostTypeId` bigint(20) unsigned NOT NULL,
`AcceptedAnswerId` bigint(20) unsigned default NULL,
`ParentId` bigint(20) unsigned default NULL,
`CreationDate` datetime NOT NULL,
`Score` int(11) NOT NULL default '0',
`ViewCount` int(11) NOT NULL default '0',
`Body` text NOT NULL,
`OwnerUserId` bigint(20) unsigned NOT NULL,
`OwnerDisplayName` varchar(40) default NULL,
`LastEditorUserId` bigint(20) unsigned default NULL,
`LastEditDate` datetime default NULL,
`LastActivityDate` datetime default NULL,
`Title` varchar(250) NOT NULL default '',
`Tags` varchar(150) NOT NULL default '',
`AnswerCount` int(11) NOT NULL default '0',
`CommentCount` int(11) NOT NULL default '0',
`FavoriteCount` int(11) NOT NULL default '0',
`ClosedDate` datetime default NULL,
PRIMARY KEY (`PostId`),
UNIQUE KEY `PostId` (`PostId`),
KEY `PostTypeId` (`PostTypeId`),
KEY `AcceptedAnswerId` (`AcceptedAnswerId`),
KEY `OwnerUserId` (`OwnerUserId`),
KEY `LastEditorUserId` (`LastEditorUserId`),
KEY `ParentId` (`ParentId`),
CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`PostTypeId`) REFERENCES `posttypes` (`PostTypeId`)
) ENGINE=InnoDB;
Nota para los comentaristas: si desea otro punto de referencia con una versión diferente de MySQL, un conjunto de datos diferente o un diseño de tabla diferente, no dude en hacerlo usted mismo. He mostrado la técnica anterior. Stack Overflow está aquí para mostrarle cómo realizar el trabajo de desarrollo de software, no para hacer todo el trabajo por usted.
ACTUALIZACIÓN: 2017-03-31, la versión 5.7.5 de MySQL activó el interruptor ONLY_FULL_GROUP_BY de forma predeterminada (por lo tanto, las consultas GROUP BY no deterministas quedaron deshabilitadas). Además, actualizaron la implementación de GROUP BY y es posible que la solución ya no funcione como se esperaba, incluso con el interruptor desactivado. Hay que comprobarlo.
La solución anterior de Bill Karwin funciona bien cuando el recuento de elementos dentro de los grupos es bastante pequeño, pero el rendimiento de la consulta se vuelve malo cuando los grupos son bastante grandes, ya que la solución requiere solo n*n/2 + n/2
comparaciones IS NULL
.
Hice mis pruebas en una tabla InnoDB de 18684446
filas con 1182
grupos. La tabla contiene resultados de pruebas funcionales y tiene (test_id, request_id)
como clave principal. Por lo tanto, test_id
es un grupo y estaba buscando el último request_id
para cada uno test_id
.
La solución de Bill ya ha estado ejecutándose durante varias horas en mi Dell e4310 y no sé cuándo terminará a pesar de que opera con un índice de cobertura (por lo tanto, using index
EXPLIQUE).
Tengo un par de otras soluciones que se basan en las mismas ideas:
- si el índice subyacente es el índice BTREE (que suele ser el caso), el
(group_id, item_value)
par más grande es el último valor dentro de cada unogroup_id
, es decir, el primero de cada unogroup_id
si recorremos el índice en orden descendente; - si leemos los valores que están cubiertos por un índice, los valores se leen en el orden del índice;
- cada índice contiene implícitamente columnas de clave principal adjuntas (es decir, la clave principal está en el índice de cobertura). En las soluciones siguientes, opero directamente con la clave principal; en su caso, solo necesitará agregar columnas de clave principal en el resultado.
- en muchos casos, es mucho más económico recopilar los identificadores de fila requeridos en el orden requerido en una subconsulta y unir el resultado de la subconsulta en el identificador. Dado que para cada fila en el resultado de la subconsulta, MySQL necesitará una única búsqueda basada en la clave principal, la subconsulta se colocará primero en la combinación y las filas se generarán en el orden de los identificadores en la subconsulta (si omitimos ORDER BY explícito para la unión)
3 formas en que MySQL usa índices es un gran artículo para comprender algunos detalles.
Solución 1
Este es increíblemente rápido, toma alrededor de 0,8 segundos en mis más de 18 millones de filas:
SELECT test_id, MAX(request_id) AS request_id
FROM testresults
GROUP BY test_id DESC;
Si desea cambiar el orden a ASC, colóquelo en una subconsulta, devuelva solo los identificadores y utilícelo como subconsulta para unirse al resto de las columnas:
SELECT test_id, request_id
FROM (
SELECT test_id, MAX(request_id) AS request_id
FROM testresults
GROUP BY test_id DESC) as ids
ORDER BY test_id;
Éste tarda aproximadamente 1,2 segundos en mis datos.
Solución 2
Aquí hay otra solución que tarda unos 19 segundos en mi mesa:
SELECT test_id, request_id
FROM testresults, (SELECT @group:=NULL) as init
WHERE IF(IFNULL(@group, -1)=@group:=test_id, 0, 1)
ORDER BY test_id DESC, request_id DESC
También devuelve pruebas en orden descendente. Es mucho más lento ya que realiza un escaneo de índice completo, pero está aquí para darle una idea de cómo generar N filas máximas para cada grupo.
La desventaja de la consulta es que el caché de consultas no puede almacenar en caché su resultado.
Utilice su subconsulta para devolver la agrupación correcta, porque ya está a mitad de camino.
Prueba esto:
select
a.*
from
messages a
inner join
(select name, max(id) as maxid from messages group by name) as b on
a.id = b.maxid
Si no es así, id
quieres el máximo de:
select
a.*
from
messages a
inner join
(select name, max(other_col) as other_col
from messages group by name) as b on
a.name = b.name
and a.other_col = b.other_col
De esta manera, evita subconsultas correlacionadas y/o ordenamiento en sus subconsultas, que tienden a ser muy lentas/ineficientes.
Llegué a una solución diferente, que consiste en obtener los ID de la última publicación dentro de cada grupo y luego seleccionarlos de la tabla de mensajes usando el resultado de la primera consulta como argumento para una WHERE x IN
construcción:
SELECT id, name, other_columns
FROM messages
WHERE id IN (
SELECT MAX(id)
FROM messages
GROUP BY name
);
No sé cómo funciona esto en comparación con otras soluciones, pero funcionó espectacularmente para mi tabla con más de 3 millones de filas. (Ejecución de 4 segundos con más de 1200 resultados)
Esto debería funcionar tanto en MySQL como en SQL Server.