Recuperando el último registro de cada grupo - MySQL

Resuelto Vijay Dev asked hace 15 años • 34 respuestas

Hay una tabla messagesque 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?

Vijay Dev avatar Aug 22 '09 00:08 Vijay Dev
Aceptado

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 Poststabla. 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 BYen 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 EXPLAINaná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 EXPLAINaná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 Postsmesa:

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.

Bill Karwin avatar Aug 21 '2009 17:08 Bill Karwin

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/2comparaciones IS NULL.

Hice mis pruebas en una tabla InnoDB de 18684446filas con 1182grupos. La tabla contiene resultados de pruebas funcionales y tiene (test_id, request_id)como clave principal. Por lo tanto, test_ides un grupo y estaba buscando el último request_idpara 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 indexEXPLIQUE).

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 uno group_id, es decir, el primero de cada uno group_idsi 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.

newtover avatar Jan 06 '2012 11:01 newtover

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í, idquieres 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.

Eric avatar Aug 21 '2009 17:08 Eric

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 INconstrucció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.

JYelton avatar Feb 20 '2012 21:02 JYelton