Obter registros com valor máximo para cada grupo de resultados SQL agrupados

229

Como você obtém as linhas que contêm o valor máximo para cada conjunto agrupado?

Eu já vi algumas variações excessivamente complicadas nessa questão, e nenhuma com uma boa resposta. Eu tentei montar o exemplo mais simples possível:

Dada uma tabela como essa abaixo, com colunas de pessoa, grupo e faixa etária, como você obteria a pessoa mais velha de cada grupo? (Um empate dentro de um grupo deve dar o primeiro 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 desejado:

Shawn | 1     | 42    
Laura | 2     | 39  
Yarin
fonte
3
Cuidado: A Resposta Aceita funcionou em 2012 quando foi escrita. No entanto, ele não funciona mais por vários motivos, conforme indicado nos Comentários.
Rick James

Respostas:

132

Existe uma maneira super simples de fazer isso no mysql:

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

Isso funciona porque no mysql você está autorizado a não agregada não-grupo-por colunas, caso em que o mysql retorna apenas a primeira linha. A solução é ordenar primeiro os dados de modo que, para cada grupo, a linha que você deseja seja primeiro e, em seguida, agrupe pelas colunas para as quais deseja o valor.

Você evita subconsultas complicadas que tentam encontrar o max()etc, e também os problemas de retornar várias linhas quando há mais de uma com o mesmo valor máximo (como as outras respostas fariam)

Nota: Esta é uma solução somente para mysql . Todos os outros bancos de dados que eu conheço lançarão um erro de sintaxe SQL com a mensagem "colunas não agregadas não estão listadas no grupo por cláusula" ou similar. Como esta solução usa comportamento não documentado , o mais cauteloso pode querer incluir um teste para afirmar que continua funcionando se uma versão futura do MySQL alterar esse comportamento.

Atualização da versão 5.7:

Desde a versão 5.7, a sql-modeconfiguração inclui ONLY_FULL_GROUP_BYpor padrão, portanto, para fazer isso funcionar, você não deve ter essa opção (edite o arquivo de opções do servidor para remover essa configuração).

Boêmio
fonte
66
"o mysql apenas retorna a primeira linha." - talvez seja assim que funciona, mas não é garantido. A documentação diz: "O servidor é livre para escolher qualquer valor de cada grupo; portanto, a menos que sejam iguais, os valores escolhidos são indeterminados". . O servidor não seleciona linhas, mas valores (não necessariamente da mesma linha) para cada coluna ou expressão que aparece na SELECTcláusula e não é calculada usando uma função agregada.
axiac
16
Esse comportamento foi alterado no MySQL 5.7.5 e, por padrão, rejeita esta consulta porque as colunas na SELECTcláusula não são funcionalmente dependentes das GROUP BYcolunas. Se estiver configurado para aceitá-lo (`ONLY_FULL_GROUP_BY` está desativado), funciona como as versões anteriores (ou seja, os valores dessas colunas são indeterminados).
axiac 22/01
17
Estou surpreso que esta resposta tenha recebido muitos votos. Está errado e é ruim. Não é garantido que esta consulta funcione. Os dados em uma subconsulta são um conjunto não ordenado, apesar da cláusula order by. O MySQL pode realmente solicitar os registros agora e manter essa ordem, mas não quebraria nenhuma regra se parasse de fazê-lo em alguma versão futura. Em seguida, ele GROUP BYcondensa em um registro, mas todos os campos serão escolhidos arbitrariamente a partir dos registros. Ele pode ser que o MySQL atualmente simplesmente escolhe sempre a primeira linha, mas poderia muito bem pegar qualquer outra linha ou mesmo valores de diferentes linhas em uma versão futura.
22616 Thorsten Kettner #
9
Ok, nós discordamos aqui. Não uso recursos não documentados que funcionem atualmente e confio em alguns testes que, esperamos, abrangem isso. Você sabe que tem sorte que a implementação atual obtenha o primeiro registro completo, onde os documentos afirmam claramente que você pode obter valores indeterminados, mas ainda o usa. Algumas configurações simples de sessão ou banco de dados podem mudar isso a qualquer momento. Eu consideraria isso muito arriscado.
22616 Thorsten Kettner #
3
Esta resposta parece errada. De acordo com o documento , o servidor pode escolher qualquer valor de cada grupo ... Além disso, a seleção de valores de cada grupo não pode ser influenciada pela adição de uma cláusula ORDER BY. A classificação do conjunto de resultados ocorre após a escolha dos valores e ORDER BY não afeta qual valor em cada grupo o servidor escolhe.
Tgr
296

A solução correta é:

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

Como funciona:

Ele corresponde a cada linha ocom todas as linhas de bter o mesmo valor na coluna Groupe um valor maior na coluna Age. Qualquer linha que onão tenha o valor máximo de seu grupo na coluna Agecorresponderá a uma ou mais linhas de b.

O LEFT JOINfaz-lo coincidir com a pessoa mais velha do grupo (incluindo as pessoas que estão sozinhas em seu grupo) com uma linha completa de NULLs de b( 'não maior idade no grupo').
O uso INNER JOINfaz com que essas linhas não correspondam e são ignoradas.

A WHEREcláusula mantém apenas as linhas com NULLs nos campos extraídos b. Eles são as pessoas mais velhas de cada grupo.

Leituras adicionais

Esta solução e muitas outras são explicadas no livro Antipatterns SQL: Evitando as Armadilhas da Programação de Banco de Dados

axiac
fonte
43
BTW, isso pode retornar duas ou mais linhas para um mesmo grupo se o.Age = b.Age, por exemplo, se Paul do grupo 2 estiver em 39 como Laura. No entanto, se não queremos esse comportamento, podemos fazer: #ON o.Group = b.Group AND (o.Age < b.Age or (o.Age = b.Age and o.id < b.id))
Todor
8
Incrível! Para registros 20M é como 50 vezes mais rápido do que o algoritmo "ingênuo" (juntar-se contra uma subconsulta com max ())
user2706534
3
Funciona perfeitamente com comentários do @Todor. Eu acrescentaria que, se houver mais condições de consulta, elas deverão ser adicionadas no FROM e no LEFT JOIN. Algo parecido com: FROM (SELECIONE * DA Pessoa ONDE Idade! = 32) o LEFT JOIN (SELECIONE * DA Pessoa ONDE Idade! = 32) b - se você deseja descartar pessoas com 32 anos
Alain Zelink 7/15/10
1
@AlainZelink não são essas "condições de consulta adicionais" melhor colocadas na lista de condições final WHERE, para não introduzir subconsultas - que não eram necessárias na resposta @ axiac original?
tarilabs
5
Esta solução funcionou; no entanto, ele começou a ser relatado no log de consultas lentas quando tentado com mais de 10.000 linhas compartilhando o mesmo ID. Estava JOINing na coluna indexada. Um caso raro, mas achei que vale a pena mencionar.
27416 Chaletabelle
50

Você pode ingressar em uma subconsulta que puxa o MAX(Group)e Age. Este método é portátil na maioria dos 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 Berkowski
fonte
Michael, obrigado por isso - mas você tem uma resposta para a questão de retornar várias linhas em gravatas, de acordo com os comentários de Bohemian?
Yarin
1
@Yarin Se houvesse duas linhas, por exemplo Group = 2, Age = 20, onde , a subconsulta retornaria uma delas, mas a ONcláusula join corresponderia a ambas , portanto, você retornaria duas linhas com o mesmo grupo / idade, embora com valores diferentes para as outras colunas, ao invés de um.
Michael Berkowski
Então, estamos dizendo que é impossível limitar os resultados a um por grupo, a menos que sigamos a rota do Bohemians MySQL?
Yarin
@Yarin não, não é impossível, requer apenas mais trabalho se houver colunas adicionais - possivelmente outra subconsulta aninhada para extrair o ID máximo associado a cada par de grupo / idade semelhante, e junte-se a ele para obter o restante da linha com base no ID.
Michael Berkowski
Essa deve ser a resposta aceita (a resposta atualmente aceita falhará na maioria dos outros RDBMS e, de fato, até falhará em muitas versões do MySQL).
Tim Biegeleisen 6/08/19
28

Minha solução simples para SQLite (e provavelmente MySQL):

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

No entanto, ele não funciona no PostgreSQL e talvez em outras plataformas.

No PostgreSQL você pode usar a cláusula DISTINCT ON :

SELECT DISTINCT ON ("group") * FROM "mytable" ORDER BY "group", "age" DESC;
Igor Kulagin
fonte
@Bohemian desculpe, eu entendi sabe, este é MySQL somente uma vez que inclui colunas não agregados
Cec
2
@IgorKulagin - Não funciona no Postgres- Mensagem de erro: a coluna "mytable.id" deve aparecer na cláusula GROUP BY ou ser usada em uma função agregada
Yarin
13
A consulta MySQL pode funcionar apenas por acidente em muitas ocasiões. O "SELECT *" pode retornar informações que não correspondem ao MAX (idade) pertencente. Esta resposta está errada. Provavelmente também é o caso do SQLite.
Albert Hendriks
2
Mas isso se encaixa no caso em que precisamos selecionar a coluna agrupada e a coluna max. Isso não se encaixa o requisito acima onde ele faria resultados ( 'Bob', 1, 42), mas o resultado esperado é ( 'Shawn', 1, 42)
Ram Babu S
1
Bom para o postgres
Karol Gasienica
4

Usando o método de classificação.

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
fonte
sel - precisa de alguma explicação - eu nunca vi :=antes - o que é isso?
Yarin
1
: = é operador de atribuição. Você pode ler mais em dev.mysql.com/doc/refman/5.0/en/user-variables.html
sel
Vou ter que me aprofundar nisso - acho que a resposta complica demais o nosso cenário, mas obrigada por me ensinar algo novo.
Yarin
3

Não tenho certeza se o MySQL tem a função row_number. Nesse caso, você pode usá-lo para obter o resultado desejado. No SQL Server, você pode fazer algo semelhante 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;
user130268
fonte
1
Faz, desde 8.0.
Ilja Everilä
2

A solução da axiac é o que funcionou melhor para mim no final. No entanto, eu tinha uma complexidade adicional: um "valor máximo" calculado, derivado de duas colunas.

Vamos usar o mesmo exemplo: eu gostaria da pessoa mais velha de cada grupo. Se houver pessoas igualmente velhas, leve a pessoa mais alta.

Eu tive que executar a junção esquerda duas vezes para obter esse comportamento:

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 isto ajude! Acho que deveria haver uma maneira melhor de fazer isso ...

Arthur C
fonte
2

Minha solução funciona apenas se você precisar recuperar apenas uma coluna; no entanto, para minhas necessidades, foi a melhor solução encontrada em termos de desempenho (ela usa apenas uma única consulta!):

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

Ele usa GROUP_CONCAT para criar uma lista de concat ordenadas e, em seguida, faço a substring apenas para a primeira.

Antonio Giovanazzi
fonte
Pode confirmar que você pode obter várias colunas classificando a mesma chave dentro do group_concat, mas precisa gravar um group_concat / index / substring separado para cada coluna.
Rasika
O bônus aqui é que você pode adicionar várias colunas à classificação dentro do group_concat e isso resolveria os vínculos facilmente e garantiria apenas um registro por grupo. Bem feito na solução simples e eficiente!
Rasika
2

Eu tenho uma solução simples 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
Khalid Musa Sagar
fonte
1

Usando CTEs - expressões comuns de tabela:

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
Marvin
fonte
1

No Oracle, a consulta abaixo pode fornecer o resultado desejado.

SELECT group,person,Age,
  ROWNUMBER() OVER (PARTITION BY group ORDER BY age desc ,person asc) as rankForEachGroup
  FROM tablename where rankForEachGroup=1
kiruba
fonte
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`
Harshad
fonte
0

Você também pode tentar

SELECT * FROM mytable WHERE age IN (SELECT MAX(age) FROM mytable GROUP BY `Group`) ;
Ritwik
fonte
1
Obrigado, embora isso retorne vários registros para uma idade em que há um empate
Yarin
Além disso, essa consulta estaria incorreta no caso de haver um homem de 39 anos no grupo 1. Nesse caso, essa pessoa também seria selecionada, mesmo que a idade máxima no grupo 1 seja maior.
Joshua Richardson
0

Eu não usaria Grupo como nome da coluna, pois é uma palavra reservada. No entanto, seguir o SQL funcionaria.

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
Bae Cheol Shin
fonte
Obrigado, embora isso retorne vários registros para uma idade em que há um empate
Yarin
@Yarin, como decidiria qual é a pessoa mais velha correta? Múltiplas respostas parecem ser a resposta mais acertada caso contrário, use limite ea ordem
Duncan
0

Esse método tem o benefício de permitir que você classifique por uma coluna diferente e não descarte os outros dados. É bastante útil em uma situação em que você está tentando listar pedidos com uma coluna para itens, listando os mais pesados ​​primeiro.

Fonte: 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;
Ray Foss
fonte
0

deixe o nome da tabela ser pessoas

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; 
user3475425
fonte
0

Se o ID (e todos os coulmns) for necessário no 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
    )
mayank kumar
fonte
0

É assim que eu estou recebendo as N linhas máximas por grupo no 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
;

como funciona:

  • auto junção à mesa
  • grupos são feitos por co.country = ci.country
  • N elementos por grupo são controlados por ) < 1 isso para 3 elementos -) <3
  • para obter max ou min depende de: co.id < ci.id
    • co.id <ci.id - máx
    • co.id> ci.id - min

Exemplo completo aqui:

mysql seleciona n valores máximos por grupo

Vanko
fonte