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


229

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

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

Dada una tabla como la siguiente, con columnas de persona, grupo y edad, ¿cómo obtendría la persona más vieja 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  

3
Precaución: la respuesta aceptada funcionó en 2012 cuando se escribió. Sin embargo, ya no funciona por múltiples razones, como se indica en los Comentarios.
Rick James

Respuestas:


132

Hay una manera súper simple 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 que no sean de grupo, en cuyo caso mysql solo devuelve la primera fila. La solución es primero ordenar los datos de manera que para cada grupo la fila que desee sea primero, luego agrupe por las columnas para las que desea el valor.

Evita las subconsultas complicadas que intentan encontrar el max()etc., y también los problemas de devolver varias filas cuando hay más de una 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 arrojarán un error de sintaxis SQL con el mensaje "las columnas no agregadas no se enumeran en el grupo por cláusula" o similar. Debido a que esta solución utiliza un comportamiento indocumentado , los más cautelosos pueden incluir una prueba para afirmar que sigue funcionando si una versión futura de MySQL cambia este comportamiento.

Actualización de la versión 5.7:

Desde la versión 5.7, la sql-modeconfiguración incluye ONLY_FULL_GROUP_BYpor defecto, por lo que para que esto funcione se debe no tener esta opción (editar el archivo de opciones del servidor para eliminar esta configuración).


66
"mysql solo devuelve la primera fila". - tal vez así es como funciona, pero no está garantizado. La documentación dice: "El servidor es libre de elegir cualquier valor de cada grupo, por lo que, a menos que sean iguales, los valores elegidos son indeterminados". . El servidor no selecciona filas sino valores (no necesariamente de la misma fila) para cada columna o expresión que aparece en la SELECTcláusula y no se calcula utilizando una función de agregado.
axiac

16
Este comportamiento cambió en MySQL 5.7.5 y, de forma predeterminada, rechaza esta consulta porque las columnas de la SELECTcláusula no dependen funcionalmente de las GROUP BYcolumnas. Si está configurado para aceptarlo (`ONLY_FULL_GROUP_BY` está desactivado), funciona como las versiones anteriores (es decir, los valores de esas columnas son indeterminados).
axiac

17
Me sorprende que esta respuesta haya recibido tantos votos positivos. Está mal y es malo. No se garantiza que esta consulta funcione. Los datos en una subconsulta son un conjunto desordenado a pesar del orden por cláusula. MySQL puede realmente ordenar los registros ahora y mantener ese orden, pero no rompería ninguna regla si dejara de hacerlo en alguna versión futura. Luego se GROUP BYcondensa en un registro, pero todos los campos se seleccionarán arbitrariamente de los registros. Se puede ser que actualmente MySQL simplemente siempre recoge la primera fila, pero podría también elegir cualquier otra fila o valores pares de diferentes filas en una versión futura.
Thorsten Kettner

99
De acuerdo, no estamos de acuerdo aquí. No uso características no documentadas que funcionan actualmente y confío en algunas pruebas que con suerte cubrirán esto. Sabes que eres afortunado de que la implementación actual te dé el primer registro completo donde los documentos indican claramente que podrías obtener valores indeterminados, pero aún así lo usas. Alguna sesión simple o configuración de la base de datos puede cambiar esto en cualquier momento. Consideraría esto demasiado arriesgado.
Thorsten Kettner

3
Esta respuesta parece incorrecta. Según el documento , el servidor es libre de elegir cualquier valor de cada grupo ... Además, la selección de valores de cada grupo no puede verse influenciada al agregar una cláusula ORDER BY. La ordenación del conjunto de resultados se produce después de que se han elegido los valores, y ORDER BY no afecta qué valor dentro de cada grupo elige el servidor.
Tgr

298

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:

Hace coincidir cada fila ocon todas las filas que btienen el mismo valor en la columna Groupy un valor mayor en la columna Age. Cualquier fila que ono tenga el valor máximo de su grupo en la columna Agecoincidirá con una o más filas de b.

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

La WHEREcláusula mantiene solo las filas que tienen NULLs en los campos extraídos de b. Son las personas más viejas de cada grupo.

Lecturas adicionales

Esta solución y muchas otras se explican en el libro Antipatterns de SQL: cómo evitar las trampas de la programación de bases de datos.


43
Por cierto, esto puede devolver dos o más filas para un mismo grupo si o.Age = b.Age, por ejemplo, si Paul del grupo 2 está en 39 como Laura. Sin embargo, si no queremos ese comportamiento, podemos hacerlo:ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor

8
¡Increíble! Para registros de 20M es como 50 veces más rápido que el algoritmo "ingenuo" (únete contra una subconsulta con max ())
user2706534

3
Funciona perfectamente con los comentarios de @Todor. Agregaría que si hay más condiciones de consulta, deben agregarse en FROM y en LEFT JOIN. Algo COMO: DESDE (SELECCIONAR * DESDE Persona DONDE Edad! = 32) o UNIR A LA IZQUIERDA (SELECCIONAR * DESDE Persona DONDE Edad! = 32) b - si desea despedir a las personas que tienen 32 años
Alain Zelink

1
@AlainZelink ¿no es mejor poner estas "condiciones de consulta adicionales" en la lista final de condiciones WHERE, para no introducir subconsultas, que no eran necesarias en la respuesta @ axiac original?
tarilabs

55
Esta solución funcionó; sin embargo, comenzó a aparecer en el registro de consultas lentas cuando se intentó con más de 10,000 filas compartiendo la misma ID. SE UNÍA a la columna indexada. Un caso raro, pero pensé que vale la pena mencionarlo.
chaseisabelle

50

Puede unirse contra una subconsulta que extrae el MAX(Group)y Age. Este método es portátil en la mayoría de los 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;

Michael, gracias por esto, pero ¿tienes una respuesta para el problema de devolver varias filas en empates, según los comentarios de Bohemian?
Yarin

1
@Yarin Si hubiera 2 filas, por ejemplo Group = 2, Age = 20, dónde , la subconsulta devolvería una de ellas, pero la ONcláusula de combinación coincidiría con ambas , por lo que obtendría 2 filas con el mismo grupo / edad a través de diferentes valores para las otras columnas, en lugar de uno
Michael Berkowski

Entonces, ¿estamos diciendo que es imposible limitar los resultados a uno por grupo a menos que tomemos la ruta Bohemians MySQL-only?
Yarin

@Yarin no es imposible, solo requiere más trabajo si hay columnas adicionales, posiblemente otra subconsulta anidada para extraer la identificación máxima asociada para cada par similar de grupo / edad, luego únete para obtener el resto de la fila en función de la identificación.
Michael Berkowski

Esta debería ser la respuesta aceptada (la respuesta actualmente aceptada fallará en la mayoría de los demás RDBMS, y de hecho incluso fallará en muchas versiones de MySQL).
Tim Biegeleisen el

28

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

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

Sin embargo, no funciona en PostgreSQL y quizás en otras plataformas.

En PostgreSQL puede usar la cláusula DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;

@Bohemian lo siento, lo sé, esto es solo para MySQL ya que incluye columnas no agregadas
Cec

2
@IgorKulagin - No funciona en Postgres- Mensaje de error: la columna "mytable.id" debe aparecer en la cláusula GROUP BY o usarse en una función agregada
Yarin

13
La consulta MySQL solo puede funcionar por accidente en muchas ocasiones. El "SELECCIONAR *" puede devolver información que no corresponde al MAX (edad) correspondiente. Esta respuesta es incorrecta. Este es probablemente también el caso de SQLite.
Albert Hendriks

2
Pero esto se ajusta al caso en el que necesitamos seleccionar la columna agrupada y la columna máxima. Esto no se ajusta al requisito anterior donde resultaría ('Bob', 1, 42) pero el resultado esperado es ('Shawn', 1, 42)
Ram Babu S

1
Bueno para postgres
Karol Gasienica

4

Usando 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

sel - necesito alguna explicación - nunca he visto :=antes - ¿qué es eso?
Yarin

1
: = es operador de asignación. Puede leer más en dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel

Tendré que profundizar en esto. Creo que la respuesta complica demasiado nuestro escenario, pero gracias por enseñarme algo nuevo ...
Yarin

3

No estoy seguro si MySQL tiene la función row_number. Si es así, puede usarlo para obtener el resultado deseado. En SQL Server 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
Lo hace, desde 8.0.
Ilja Everilä

2

La solución de axiac es lo que mejor me funcionó 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 de más edad en cada grupo. Si hay personas que son igualmente viejas, tome la persona más alta.

Tuve que realizar la unión izquierda 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! Sin embargo, creo que debería haber una mejor manera de hacer esto ...


2

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 (¡usa solo una consulta!):

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

Utiliza GROUP_CONCAT para crear una lista de concat ordenada y luego subcadena solo a la primera.


Puede confirmar que puede obtener varias columnas ordenando en la misma clave dentro de group_concat, pero necesita escribir un group_concat / index / substring por separado para cada columna.
Rasika

La ventaja aquí es que puede agregar varias columnas a la clasificación dentro de group_concat y resolvería los lazos fácilmente y garantizaría solo un registro por grupo. ¡Bien hecho por la solución simple y eficiente!
Rasika

2

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

1

Uso de CTE: 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

En Oracle a continuación, la consulta puede dar el resultado deseado.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1

0
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`

0

También puedes probar

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

1
Gracias, aunque esto devuelve múltiples registros para una época en la que hay un empate
Yarin

Además, esta consulta sería incorrecta en el caso de que haya una persona de 39 años en el grupo 1. En ese caso, esa persona también sería seleccionada, aunque la edad máxima en el grupo 1 sea mayor.
Joshua Richardson

0

No usaría Grupo como nombre de columna ya que es una palabra reservada. Sin embargo, seguir 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

Gracias, aunque esto devuelve múltiples registros para una época en la que hay un empate
Yarin

@Yarin, ¿cómo decidiría cuál es la persona mayor correcta? Las respuestas múltiples parecen ser la respuesta más adecuada; de lo contrario, utilice el límite y el orden
Duncan


0

deja que el nombre de la mesa sea gente

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

Si se necesita ID (y todas las 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

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 mesa
  • los grupos son realizados por co.country = ci.country
  • N elementos por grupo están controlados por ) < 13 elementos -) <3
  • obtener max o min depende de: co.id < ci.id
    • co.id <ci.id - max
    • co.id> ci.id - min

Ejemplo completo aquí:

mysql selecciona n valores máximos por grupo

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.