Obtener registros con valor máximo para cada grupo de resultados SQL agrupados


¿Cómo se obtienen las filas que contienen el valor máximo para cada conjunto agrupado?

He visto algunas variaciones demasiado complicadas sobre esta pregunta, y ninguna con una buena respuesta. He tratado de armar el ejemplo más simple posible:

Dada una tabla como esa a continuación, con columnas de persona, grupo y edad, ¿cómo obtendrías la persona más antigua en cada grupo? (Un empate dentro de un grupo debe dar el primer resultado alfabético)

Person | Group | Age
---
Bob  | 1     | 32  
Jill | 1     | 34  
Shawn| 1     | 42  
Jake | 2     | 29  
Paul | 2     | 36  
Laura| 2     | 39  

Conjunto de resultados deseado:

Shawn | 1     | 42    
Laura | 2     | 39  
Author: axiac, 2012-08-24

17 answers

Hay una forma súper sencilla de hacer esto en mysql:

select * 
from (select * from mytable order by `Group`, age desc, Person) x
group by `Group`

Esto funciona porque en mysql se le permite no agregar columnas no agrupadas, en cuyo caso mysql solo devuelve la primera fila. La solución es ordenar primero los datos de manera que para cada grupo la fila que desea sea la primera, luego agrupe por las columnas para las que desea el valor.

Evita subconsultas complicadas que intentan encontrar el max() etc, y también los problemas de devolver varias filas cuando hay más de uno con el mismo valor máximo (como lo harían las otras respuestas)

Nota: Esta es una solución solo para mysql. Todas las demás bases de datos que conozco lanzarán un error de sintaxis SQL con el mensaje "las columnas no agregadas no se enumeran en la cláusula group by" o similar. Debido a que esta solución utiliza el comportamiento undocumented , los más cautelosos pueden querer incluir una prueba para afirmar que sigue funcionando en caso de que una versión futura de MySQL cambie esto comportamiento.

Actualización de la versión 5.7:

Desde la versión 5.7, el sql-mode el ajuste incluye ONLY_FULL_GROUP_BY de forma predeterminada, por lo que para que esto funcione debe no tener esta opción (edite el archivo de opciones para que el servidor elimine esta configuración).

 117
Author: Bohemian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-30 20:49:49

La solución correcta es:

SELECT o.*
FROM `Persons` o                    # 'o' from 'oldest person in group'
  LEFT JOIN `Persons` b             # 'b' from 'bigger age'
      ON o.Group = b.Group AND o.Age < b.Age
WHERE b.Age is NULL                 # bigger age not found

Cómo funciona:

Coincide con cada fila de o con todas las filas de b tener el mismo valor en la columna Group y un mayor valor en la columna Age. Cualquier fila de o que no tenga el valor máximo de su grupo en la columna Age coincidirá con una o más filas de b.

El LEFT JOIN hace que coincida con la persona más antigua del grupo (incluidas las personas que están solas en su grupo) con una fila llena de NULLs de b ('no mayor edad en el grupo").
Usar INNER JOIN hace que estas filas no coincidan y se ignoran.

La cláusula WHERE mantiene solo las filas que tienen NULL s en los campos extraídos de b. Son las personas de más edad de cada grupo.

Otras lecturas

Esta solución y muchas otras se explican en el libro SQL Antipatterns: Avoiding the Pitfalls of Database Programming

 218
Author: axiac,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-22 13:56:25

Mi solución simple para SQLite (y probablemente MySQL):

SELECT *, MAX(age) FROM mytable GROUP BY `Group`;

Sin embargo, no funciona en PostgreSQL y tal vez en otras plataformas.

En PostgreSQL puede usar DISTINCT EN la cláusula:

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
 25
Author: Igor Kulagin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-02 11:55:38

Puede unirse contra una subconsulta que tire de MAX(Group) y Age. Este método es portátil a través de la mayoría de RDBMS.

SELECT t1.*
FROM yourTable t1
INNER JOIN
(
    SELECT `Group`, MAX(Age) AS max_age
    FROM yourTable
    GROUP BY `Group`
) t2
    ON t1.`Group` = t2.`Group` AND t1.Age = t2.max_age;
 24
Author: Michael Berkowski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-06 02:20:57

Utilizando el método de clasificación.

SELECT @rn :=  CASE WHEN @prev_grp <> groupa THEN 1 ELSE @rn+1 END AS rn,  
   @prev_grp :=groupa,
   person,age,groupa  
FROM   users,(SELECT @rn := 0) r        
HAVING rn=1
ORDER  BY groupa,age DESC,person
 3
Author: sel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-24 01:52:03

La solución de Axiac es lo que funcionó mejor para mí al final. Sin embargo, tenía una complejidad adicional: un "valor máximo" calculado, derivado de dos columnas.

Usemos el mismo ejemplo: Me gustaría la persona más antigua de cada grupo. Si hay personas que son igualmente viejos, tomar la persona más alta.

Tuve que realizar el join izquierdo dos veces para obtener este comportamiento:

SELECT o1.* WHERE
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o1
LEFT JOIN
    (SELECT o.*
    FROM `Persons` o
    LEFT JOIN `Persons` b
    ON o.Group = b.Group AND o.Age < b.Age
    WHERE b.Age is NULL) o2
ON o1.Group = o2.Group AND o1.Height < o2.Height 
WHERE o2.Height is NULL;

Espero que esto ayude! Supongo que debería haber una mejor manera de hacer esto...

 2
Author: Arthur C,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-14 13:30:45

Usando CTEs-Expresiones de Tabla Comunes:

WITH MyCTE(MaxPKID, SomeColumn1)
AS(
SELECT MAX(a.MyTablePKID) AS MaxPKID, a.SomeColumn1
FROM MyTable1 a
GROUP BY a.SomeColumn1
  )
SELECT b.MyTablePKID, b.SomeColumn1, b.SomeColumn2 MAX(b.NumEstado)
FROM MyTable1 b
INNER JOIN MyCTE c ON c.MaxPKID = b.MyTablePKID
GROUP BY b.MyTablePKID, b.SomeColumn1, b.SomeColumn2

--Note: MyTablePKID is the PrimaryKey of MyTable
 1
Author: Marvin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-19 16:22:08

No estoy seguro si MySQL tiene la función row_number. Si es así, puede usarlo para obtener el resultado deseado. En SQL Server se puede hacer algo similar a:

CREATE TABLE p
(
 person NVARCHAR(10),
 gp INT,
 age INT
);
GO
INSERT  INTO p
VALUES  ('Bob', 1, 32);
INSERT  INTO p
VALUES  ('Jill', 1, 34);
INSERT  INTO p
VALUES  ('Shawn', 1, 42);
INSERT  INTO p
VALUES  ('Jake', 2, 29);
INSERT  INTO p
VALUES  ('Paul', 2, 36);
INSERT  INTO p
VALUES  ('Laura', 2, 39);
GO

SELECT  t.person, t.gp, t.age
FROM    (
         SELECT *,
                ROW_NUMBER() OVER (PARTITION BY gp ORDER BY age DESC) row
         FROM   p
        ) t
WHERE   t.row = 1;
 1
Author: user130268,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-10 21:56:46

Mi solución funciona solo si necesita recuperar solo una columna, sin embargo para mis necesidades fue la mejor solución encontrada en términos de rendimiento (utiliza solo una sola consulta!):

SELECT SUBSTRING_INDEX(GROUP_CONCAT(column_x ORDER BY column_y),',',1) AS xyz,
   column_z
FROM table_name
GROUP BY column_z;

Usa GROUP_CONCAT para crear una lista concat ordenada y luego substring a solo la primera.

 1
Author: Antonio Giovanazzi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-28 09:48:04

También puedes probar

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
 0
Author: Ritwik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-27 13:51:02

Este método tiene la ventaja de permitirle clasificar por una columna diferente, y no destrozar los otros datos. Es bastante útil en una situación en la que está tratando de enumerar los pedidos con una columna para los artículos, enumerando primero los más pesados.

Fuente: http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_group-concat

SELECT person, group,
    GROUP_CONCAT(
        DISTINCT age
        ORDER BY age DESC SEPARATOR ', follow up: '
    )
FROM sql_table
GROUP BY group;
 0
Author: Ray Foss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-13 14:30:12

Que el nombre de la tabla sea personas

select O.*              -- > O for oldest table
from people O , people T
where O.grp = T.grp and 
O.Age = 
(select max(T.age) from people T where O.grp = T.grp
  group by T.grp)
group by O.grp; 
 0
Author: user3475425,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-10 11:31:26

Si se necesita ID(y todos los coulmns) de mytable

SELECT
    *
FROM
    mytable
WHERE
    id NOT IN (
        SELECT
            A.id
        FROM
            mytable AS A
        JOIN mytable AS B ON A. GROUP = B. GROUP
        AND A.age < B.age
    )
 0
Author: mayank kumar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-10 07:39:22

Así es como obtengo las N filas máximas por grupo en mysql

SELECT co.id, co.person, co.country
FROM person co
WHERE (
SELECT COUNT(*)
FROM person ci
WHERE  co.country = ci.country AND co.id < ci.id
) < 1
;

Cómo funciona:

  • auto unirse a la tabla
  • los grupos son hechos por co.country = ci.country
  • N elementos por grupo son controlados por ) < 1 así que para 3 elementos -)
  • para obtener max o min depende de: co.id < ci.id
    • co.id
    • co.id > ci.id -min

Ejemplo completo aquí:

Mysql selecciona n valores máximos por grupo

 0
Author: Vanko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-28 07:11:26

Tengo una solución simple usando WHERE IN

SELECT a.* FROM `mytable` AS a    
WHERE a.age IN( SELECT MAX(b.age) AS age FROM `mytable` AS b GROUP BY b.group )    
ORDER BY a.group ASC, a.person ASC
 0
Author: Khalid Musa Sagar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-11 20:29:27
with CTE as 
(select Person, 
[Group], Age, RN= Row_Number() 
over(partition by [Group] 
order by Age desc) 
from yourtable)`


`select Person, Age from CTE where RN = 1`
 -1
Author: Harshad C,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-27 07:38:37

No usaría Grupo como nombre de columna ya que es palabra reservada. Sin embargo, después de SQL funcionaría.

SELECT a.Person, a.Group, a.Age FROM [TABLE_NAME] a
INNER JOIN 
(
  SELECT `Group`, MAX(Age) AS oldest FROM [TABLE_NAME] 
  GROUP BY `Group`
) b ON a.Group = b.Group AND a.Age = b.oldest
 -1
Author: Bae Cheol Shin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-30 23:26:16