Como posso selecionar linhas com MAX (valor da coluna), DISTINCT por outra coluna no SQL?


768

Minha mesa é:

id  home  datetime     player   resource
---|-----|------------|--------|---------
1  | 10  | 04/03/2009 | john   | 399 
2  | 11  | 04/03/2009 | juliet | 244
5  | 12  | 04/03/2009 | borat  | 555
3  | 10  | 03/03/2009 | john   | 300
4  | 11  | 03/03/2009 | juliet | 200
6  | 12  | 03/03/2009 | borat  | 500
7  | 13  | 24/12/2008 | borat  | 600
8  | 13  | 01/01/2009 | borat  | 700

Eu preciso selecionar cada distinto homesegurando o valor máximo de datetime.

O resultado seria:

id  home  datetime     player   resource 
---|-----|------------|--------|---------
1  | 10  | 04/03/2009 | john   | 399
2  | 11  | 04/03/2009 | juliet | 244
5  | 12  | 04/03/2009 | borat  | 555
8  | 13  | 01/01/2009 | borat  | 700

Eu tentei:

-- 1 ..by the MySQL manual: 

SELECT DISTINCT
  home,
  id,
  datetime AS dt,
  player,
  resource
FROM topten t1
WHERE datetime = (SELECT
  MAX(t2.datetime)
FROM topten t2
GROUP BY home)
GROUP BY datetime
ORDER BY datetime DESC

Não funciona O conjunto de resultados possui 130 linhas, embora o banco de dados contenha 187. O resultado inclui algumas duplicatas de home.

-- 2 ..join

SELECT
  s1.id,
  s1.home,
  s1.datetime,
  s1.player,
  s1.resource
FROM topten s1
JOIN (SELECT
  id,
  MAX(datetime) AS dt
FROM topten
GROUP BY id) AS s2
  ON s1.id = s2.id
ORDER BY datetime 

Não. Dá todos os registros.

-- 3 ..something exotic: 

Com vários resultados.

Respostas:


940

Você está tão perto! Tudo o que você precisa fazer é selecionar AMBOS a casa e sua data e hora máx. E depois voltar à toptentabela nos AMBOS campos:

SELECT tt.*
FROM topten tt
INNER JOIN
    (SELECT home, MAX(datetime) AS MaxDateTime
    FROM topten
    GROUP BY home) groupedtt 
ON tt.home = groupedtt.home 
AND tt.datetime = groupedtt.MaxDateTime

5
Testá-lo para distinta, se dois de data e hora no máximo igual estar na mesma casa (com jogadores diferentes)
Maksym Gontar

5
Eu acho que a maneira clássica de fazer isso é com uma junção natural: "SELECT tt. * FROM topten tt JOIN NATURAL (SELECT home, MAX (datetime) AS datetime FROM topten GROUP BY home) mostrecent;" Mesma consulta exatamente, mas, sem dúvida, mais legível
Parker

32
e se houver duas linhas que tenham os mesmos valores de campo 'home' e 'datetime'?
amigos estão dizendo sobre kemal duran

3
@Young o problema com a sua consulta é que ele pode retornar id, playere resourcede linha não-max para um determinado ie casa para casa = 10 você pode obter: 3 | 10 | 04/03/2009 | john | 300 Em outras palavras, ele não garante que todas as colunas de uma linha no conjunto de resultados pertencerão para max (datetime) para determinada casa.
sactiw

1
@ me1111 O problema com sua consulta é que ela pode ou não retornar a linha com o máximo (data e hora) para uma determinada casa. Sendo GROUP BY razão vai buscar qualquer linha aleatória para cada casa e ORDER BY será apenas uma espécie do geral todo resultado como produzido por GROUP BY
sactiw

87

A MySQLsolução mais rápida , sem consultas internas e sem GROUP BY:

SELECT m.*                    -- get the row that contains the max value
FROM topten m                 -- "m" from "max"
    LEFT JOIN topten b        -- "b" from "bigger"
        ON m.home = b.home    -- match "max" row with "bigger" row by `home`
        AND m.datetime < b.datetime           -- want "bigger" than "max"
WHERE b.datetime IS NULL      -- keep only if there is no bigger than max

Explicação :

Junte-se à tabela usando a homecoluna O uso de LEFT JOINgarante que todas as linhas da tabela mapareçam no conjunto de resultados. Aqueles que não têm uma correspondência na tabela bterão NULLs para as colunas de b.

A outra condição nas JOINsolicitações para corresponder apenas às linhas bque possuem maior valor na datetimecoluna que a linha de m.

Usando os dados postados na pergunta, o LEFT JOINproduzirá estes pares:

+------------------------------------------+--------------------------------+
|              the row from `m`            |    the matching row from `b`   |
|------------------------------------------|--------------------------------|
| id  home  datetime     player   resource | id    home   datetime      ... |
|----|-----|------------|--------|---------|------|------|------------|-----|
| 1  | 10  | 04/03/2009 | john   | 399     | NULL | NULL | NULL       | ... | *
| 2  | 11  | 04/03/2009 | juliet | 244     | NULL | NULL | NULL       | ... | *
| 5  | 12  | 04/03/2009 | borat  | 555     | NULL | NULL | NULL       | ... | *
| 3  | 10  | 03/03/2009 | john   | 300     | 1    | 10   | 04/03/2009 | ... |
| 4  | 11  | 03/03/2009 | juliet | 200     | 2    | 11   | 04/03/2009 | ... |
| 6  | 12  | 03/03/2009 | borat  | 500     | 5    | 12   | 04/03/2009 | ... |
| 7  | 13  | 24/12/2008 | borat  | 600     | 8    | 13   | 01/01/2009 | ... |
| 8  | 13  | 01/01/2009 | borat  | 700     | NULL | NULL | NULL       | ... | *
+------------------------------------------+--------------------------------+

Por fim, a WHEREcláusula mantém apenas os pares que possuem NULLs nas colunas de b(eles são marcados com *na tabela acima); isso significa que, devido à segunda condição da JOINcláusula, a linha selecionada mpossui o maior valor na coluna datetime.

Leia o livro Antipatterns SQL: Evitando as armadilhas da programação de banco de dados para obter outras dicas sobre SQL.


Com SQLite, o primeiro é muito mais lento que a versão de La Voie, quando não há índice na coluna correspondente (ou seja, "casa"). (Testado com 24k linhas resultando em 13k linhas)
Thomas Tempelmann

10
Esta é a melhor resposta, se você mostrar o plano de execução você verá um passo menos com esta consulta
TlmaK0

o que acontecerá se 2 linhas têm o mesmo homee datetimee datetimeé o máximo para que determinado home?
Istiaque Ahmed

Ambas as linhas aparecem no conjunto de resultados. Esta resposta é uma prova de conceito. No seu código real, você provavelmente tem outro critério para selecionar apenas um deles nessa situação (talvez o primeiro ou o último ou use outra coluna para decidir). Basta adicionar esse critério como uma nova condição na ONcláusula. Fe ... ON ... AND m.id < b.idpara manter a entrada mais recente (aquela com a maior id) quando duas linhas tiverem os mesmos valores em homeedatetime colunas e é o máximo datetime.
axiac

Quais índices seriam melhor otimizados para uma consulta como essa?
AjaxLeung

73

Aqui está a versão T-SQL :

-- Test data
DECLARE @TestTable TABLE (id INT, home INT, date DATETIME, 
  player VARCHAR(20), resource INT)
INSERT INTO @TestTable
SELECT 1, 10, '2009-03-04', 'john', 399 UNION
SELECT 2, 11, '2009-03-04', 'juliet', 244 UNION
SELECT 5, 12, '2009-03-04', 'borat', 555 UNION
SELECT 3, 10, '2009-03-03', 'john', 300 UNION
SELECT 4, 11, '2009-03-03', 'juliet', 200 UNION
SELECT 6, 12, '2009-03-03', 'borat', 500 UNION
SELECT 7, 13, '2008-12-24', 'borat', 600 UNION
SELECT 8, 13, '2009-01-01', 'borat', 700

-- Answer
SELECT id, home, date, player, resource 
FROM (SELECT id, home, date, player, resource, 
    RANK() OVER (PARTITION BY home ORDER BY date DESC) N
    FROM @TestTable
)M WHERE N = 1

-- and if you really want only home with max date
SELECT T.id, T.home, T.date, T.player, T.resource 
    FROM @TestTable T
INNER JOIN 
(   SELECT TI.id, TI.home, TI.date, 
        RANK() OVER (PARTITION BY TI.home ORDER BY TI.date) N
    FROM @TestTable TI
    WHERE TI.date IN (SELECT MAX(TM.date) FROM @TestTable TM)
)TJ ON TJ.N = 1 AND T.id = TJ.id

EDITAR
Infelizmente, não há função RANK () OVER no MySQL.
Mas pode ser emulado, consulte Emulando funções analíticas (Ranking AKA) com o MySQL .
Então esta é a versão do MySQL :

SELECT id, home, date, player, resource 
FROM TestTable AS t1 
WHERE 
    (SELECT COUNT(*) 
            FROM TestTable AS t2 
            WHERE t2.home = t1.home AND t2.date > t1.date
    ) = 0

desculpe cara, # 1064 - Você tem um erro na sua sintaxe SQL; verifique o manual que corresponde à versão do servidor MySQL para a sintaxe correta a ser usada perto de '() OVER (PARTITION BY krd ORDER BY daytime DESC) N FROM @rapsa) M WHERE N =' na linha 1
Kaptah

2
ah, então você está usando o MySQL. É por isso que você deve começar! Vou atualizar a resposta em breve.
Maksym Gontar

@ MaxGontar, sua solução mysql arrasa, thx. e se na sua @_TestTable você remover a linha # 1>: SELECT 1, 10, '2009-03-04', 'john', 399, isto é, e se você tiver uma única linha para um determinado valor inicial? valeu.
egidiocs 11/11/11

2
Erro: Substitua "RANK ()" por "ROW_NUMBER ()". Se você tiver um empate (causado por um valor duplicado da data), terá dois registros com "1" para N.
MikeTeeVee

29

Isso funcionará mesmo se você tiver duas ou mais linhas para cada uma homecom iguais DATETIME:

SELECT id, home, datetime, player, resource
FROM   (
       SELECT (
              SELECT  id
              FROM    topten ti
              WHERE   ti.home = t1.home
              ORDER BY
                      ti.datetime DESC
              LIMIT 1
              ) lid
       FROM   (
              SELECT  DISTINCT home
              FROM    topten
              ) t1
       ) ro, topten t2
WHERE  t2.id = ro.lid

campo tampa adicionado na tabela, No Good
Kaptah

1
Este não foi executado no PHPMyAdmin. A página é atualizada, mas não há resultado nem erro ..?
Kaptah 24/03/09

WHERE ti.home = t1.home- você pode explicar a sintaxe?
Istiaque Ahmed

@IstiaqueAhmed: o que exatamente você não entende aqui? É uma consulta correlacionada e a expressão mencionada é uma condição de correlação.
Quassnoi

@Quassnoi, A selectconsulta que tem a linha WHERE ti.home = t1.home não precisa da FROMcláusula que define t1. Então, como é usado?
Istiaque Ahmed

26

Eu acho que isso lhe dará o resultado desejado:

SELECT   home, MAX(datetime)
FROM     my_table
GROUP BY home

MAS, se você precisar de outras colunas, faça uma junção com a tabela original (verifique a Michael La Voieresposta)

Cumprimentos.


8
Ele precisa de outras colunas também.
Quassnoi

4
id, casa, datetime, jogador, de recursos
Quassnoi

17

Como as pessoas parecem continuar seguindo esse segmento (a data dos comentários varia de 1,5 ano) não é muito mais simples:

SELECT * FROM (SELECT * FROM topten ORDER BY datetime DESC) tmp GROUP BY home

Nenhuma função de agregação é necessária ...

Felicidades.


6
Isso não parece funcionar. Mensagem de erro: A coluna 'x' é inválida na lista de seleção porque não está contida em uma função agregada ou na cláusula GROUP BY.
Fowl

Definitivamente, isso não funcionará no SQL Server ou Oracle, embora pareça que possa funcionar no MySQL.
ErikE

Isso é realmente lindo! Como é que isso funciona? Usando DESC e a coluna de retorno do grupo padrão? Então, se eu o alterasse para ASC de data e hora, ele retornaria a linha mais antiga para cada casa?
Wayofthefuture 15/05

Isto é brilhante!
Dog Lover

Essa correção não funciona se você tiver colunas não agregadas (no MySQL).
user3562927

11

Você também pode tentar este e para tabelas grandes o desempenho da consulta será melhor. Funciona quando não há mais de dois registros para cada casa e suas datas são diferentes. Melhor consulta geral do MySQL é de Michael La Voie acima.

SELECT t1.id, t1.home, t1.date, t1.player, t1.resource
FROM   t_scores_1 t1 
INNER JOIN t_scores_1 t2
   ON t1.home = t2.home
WHERE t1.date > t2.date

Ou, no caso do Postgres ou dos dbs que fornecem funções analíticas, tente

SELECT t.* FROM 
(SELECT t1.id, t1.home, t1.date, t1.player, t1.resource
  , row_number() over (partition by t1.home order by t1.date desc) rw
 FROM   topten t1 
 INNER JOIN topten t2
   ON t1.home = t2.home
 WHERE t1.date > t2.date 
) t
WHERE t.rw = 1

Esta resposta está correta? Tentei usá-lo, mas parece não selecionar o registro com a data mais recente para 'casa', mas apenas remove o registro com a data mais antiga. Aqui está um exemplo: SQLfiddle
marcin93w

1
@kidOfDeath - Atualizado minha resposta com o contexto e consulta Postgres
Shiva

Com SQLite, o primeiro é muito mais lento que a versão de La Voie, quando não há índice na coluna correspondente (ou seja, "casa").
Thomas Tempelmann

8

Isso funciona no Oracle:

with table_max as(
  select id
       , home
       , datetime
       , player
       , resource
       , max(home) over (partition by home) maxhome
    from table  
)
select id
     , home
     , datetime
     , player
     , resource
  from table_max
 where home = maxhome

1
como isso seleciona o máximo de data e hora? ele pediu para agrupar por casa e selecione data e hora máx. Não vejo como isso faz isso.
N00b

8
SELECT  tt.*
FROM    TestTable tt 
INNER JOIN 
        (
        SELECT  coord, MAX(datetime) AS MaxDateTime 
        FROM    rapsa 
        GROUP BY
                krd 
        ) groupedtt
ON      tt.coord = groupedtt.coord
        AND tt.datetime = groupedtt.MaxDateTime

8

Tente isso para o SQL Server:

WITH cte AS (
   SELECT home, MAX(year) AS year FROM Table1 GROUP BY home
)
SELECT * FROM Table1 a INNER JOIN cte ON a.home = cte.home AND a.year = cte.year

5
SELECT c1, c2, c3, c4, c5 FROM table1 WHERE c3 = (select max(c3) from table)

SELECT * FROM table1 WHERE c3 = (select max(c3) from table1)

5

Aqui está a versão do MySQL que imprime apenas uma entrada em que há duplicatas MAX (datetime) em um grupo.

Você poderia testar aqui http://www.sqlfiddle.com/#!2/0a4ae/1

Dados de amostra

mysql> SELECT * from topten;
+------+------+---------------------+--------+----------+
| id   | home | datetime            | player | resource |
+------+------+---------------------+--------+----------+
|    1 |   10 | 2009-04-03 00:00:00 | john   |      399 |
|    2 |   11 | 2009-04-03 00:00:00 | juliet |      244 |
|    3 |   10 | 2009-03-03 00:00:00 | john   |      300 |
|    4 |   11 | 2009-03-03 00:00:00 | juliet |      200 |
|    5 |   12 | 2009-04-03 00:00:00 | borat  |      555 |
|    6 |   12 | 2009-03-03 00:00:00 | borat  |      500 |
|    7 |   13 | 2008-12-24 00:00:00 | borat  |      600 |
|    8 |   13 | 2009-01-01 00:00:00 | borat  |      700 |
|    9 |   10 | 2009-04-03 00:00:00 | borat  |      700 |
|   10 |   11 | 2009-04-03 00:00:00 | borat  |      700 |
|   12 |   12 | 2009-04-03 00:00:00 | borat  |      700 |
+------+------+---------------------+--------+----------+

Versão MySQL com variável User

SELECT *
FROM (
    SELECT ord.*,
        IF (@prev_home = ord.home, 0, 1) AS is_first_appear,
        @prev_home := ord.home
    FROM (
        SELECT t1.id, t1.home, t1.player, t1.resource
        FROM topten t1
        INNER JOIN (
            SELECT home, MAX(datetime) AS mx_dt
            FROM topten
            GROUP BY home
          ) x ON t1.home = x.home AND t1.datetime = x.mx_dt
        ORDER BY home
    ) ord, (SELECT @prev_home := 0, @seq := 0) init
) y
WHERE is_first_appear = 1;
+------+------+--------+----------+-----------------+------------------------+
| id   | home | player | resource | is_first_appear | @prev_home := ord.home |
+------+------+--------+----------+-----------------+------------------------+
|    9 |   10 | borat  |      700 |               1 |                     10 |
|   10 |   11 | borat  |      700 |               1 |                     11 |
|   12 |   12 | borat  |      700 |               1 |                     12 |
|    8 |   13 | borat  |      700 |               1 |                     13 |
+------+------+--------+----------+-----------------+------------------------+
4 rows in set (0.00 sec)

Saída das respostas aceitas

SELECT tt.*
FROM topten tt
INNER JOIN
    (
    SELECT home, MAX(datetime) AS MaxDateTime
    FROM topten
    GROUP BY home
) groupedtt ON tt.home = groupedtt.home AND tt.datetime = groupedtt.MaxDateTime
+------+------+---------------------+--------+----------+
| id   | home | datetime            | player | resource |
+------+------+---------------------+--------+----------+
|    1 |   10 | 2009-04-03 00:00:00 | john   |      399 |
|    2 |   11 | 2009-04-03 00:00:00 | juliet |      244 |
|    5 |   12 | 2009-04-03 00:00:00 | borat  |      555 |
|    8 |   13 | 2009-01-01 00:00:00 | borat  |      700 |
|    9 |   10 | 2009-04-03 00:00:00 | borat  |      700 |
|   10 |   11 | 2009-04-03 00:00:00 | borat  |      700 |
|   12 |   12 | 2009-04-03 00:00:00 | borat  |      700 |
+------+------+---------------------+--------+----------+
7 rows in set (0.00 sec)

Embora eu goste desta resposta, como isso está me ajudando muito, tenho que apontar para uma falha importante, que depende do sistema mysql usado. Basicamente, esta solução depende da cláusula ORDER BY na subseleção. Isso pode ou não funcionar em vários ambientes mysql. Eu não tentei no MySQL puro, mas com certeza isso não funciona DE CONFIANÇA no MariaDB 10.1, conforme explicado aqui stackoverflow.com/questions/26372511/… mas o mesmo código funciona bem no Percona Server. Para ser preciso, você PODE, ou NÃO PODE obter os mesmos resultados, dependendo da quantidade de colunas t1.
Radek

O exemplo dessa declaração é que, no MariaDB 10.1, funcionou, quando usei 5 colunas da tabela t1. Assim que adicionei a sexta coluna, obviamente mexendo com o tipo de dados "natural" na tabela original, ele parou de funcionar. O motivo é que os dados na subseleção ficaram sem ordem e, portanto, eu tive a condição "is_first_appear = 1" atendida várias vezes. O mesmo código, com os mesmos dados, funcionou no Percona ok.
Radek

5

Outra maneira de obter a linha mais recente por grupo usando uma subconsulta que basicamente calcula uma classificação para cada linha por grupo e depois filtra as linhas mais recentes como com rank = 1

select a.*
from topten a
where (
  select count(*)
  from topten b
  where a.home = b.home
  and a.`datetime` < b.`datetime`
) +1 = 1

DEMO

Aqui está a demonstração visual da classificação não para cada linha para melhor compreensão

Ao ler alguns comentários, o que dizer se houver duas linhas que tenham os mesmos valores de campo 'home' e 'datetime'?

A consulta acima falhará e retornará mais de 1 linha para a situação acima. Para encobrir esta situação, haverá a necessidade de outro critério / parâmetro / coluna para decidir qual linha deve ser tomada e qual se enquadra na situação acima. Ao visualizar o conjunto de dados de amostra, presumo que exista uma coluna de chave primária idque deve ser configurada para incremento automático. Portanto, podemos usar esta coluna para escolher a linha mais recente, ajustando a mesma consulta com a ajuda de CASEinstruções como

select a.*
from topten a
where (
  select count(*)
  from topten b
  where a.home = b.home
  and  case 
       when a.`datetime` = b.`datetime`
       then a.id < b.id
       else a.`datetime` < b.`datetime`
       end
) + 1 = 1

DEMO

A consulta acima selecionará a linha com o ID mais alto entre os mesmos datetime valores

demonstração visual para a classificação não para cada linha


2

Por que não usar: SELECT home, MAX (datetime) AS MaxDateTime, player, resource FROM topten GROUP BY home Perdi alguma coisa?


4
Isso seria válido apenas com o MySQL e apenas versões anteriores à 5.7 (?) Ou após a 5.7 com o ONLY_FULL_GROUP_BY desabilitado, uma vez que SELECT colunas não foram agregadas / GROUPed (player, resource), o que significa que o MySQL fornecerá valores escolhidos aleatoriamente para aqueles dois campos de resultado. Não seria um problema para a coluna do player, pois ela se correlaciona com a coluna inicial, mas a coluna de recursos não se correlaciona com a coluna inicial ou com a data e hora e você não pode garantir qual valor de recurso você receberá.
simpleuser

+1 para a explicação, mas na pergunta feita, essa consulta não retornará a expectedsaída no MySQL versão 5.6 beforee eu duvido que ele se comporte de outra maneira no MySQL versão 5.7 e after.
sactiw

@simpleuser, `Não seria um problema para a coluna do player, pois isso se correlaciona com a coluna inicial` - você pode explicar mais?
Istiaque Ahmed

@IstiaqueAhmed, quando olho para ele novamente, essa afirmação está incorreta. Eu tinha pensado que cada jogador sempre teve o mesmo valor em casa, mas agora vejo que não fazer, então o mesmo selecione questão aleatória irá ocorrer para essa coluna bem
simpleuser

1

Tente isto

select * from mytable a join
(select home, max(datetime) datetime
from mytable
group by home) b
 on a.home = b.home and a.datetime = b.datetime

Atenciosamente K


5
Testá-lo para distinta, se dois de data e hora no máximo igual estar na mesma casa (com jogadores diferentes)
Maksym Gontar

o alias para max(datetime) é datetime. Não vai causar nenhum problema?
Istiaque Ahmed

Como a mais alta é datetimeselecionada?
Istiaque Ahmed

1

esta é a consulta que você precisa:

 SELECT b.id, a.home,b.[datetime],b.player,a.resource FROM
 (SELECT home,MAX(resource) AS resource FROM tbl_1 GROUP BY home) AS a

 LEFT JOIN

 (SELECT id,home,[datetime],player,resource FROM tbl_1) AS b
 ON  a.resource = b.resource WHERE a.home =b.home;

você pode explicar sua resposta?
Istiaque Ahmed

1

@ Michae A resposta aceita funcionará bem na maioria dos casos, mas falha em uma, conforme abaixo.

Caso existam 2 linhas com HomeID e Datetime iguais, a consulta retornará ambas as linhas, não HomeID distintas, conforme necessário, para que adicione Distinct na consulta, como abaixo.

SELECT DISTINCT tt.home  , tt.MaxDateTime
FROM topten tt
INNER JOIN
    (SELECT home, MAX(datetime) AS MaxDateTime
    FROM topten
    GROUP BY home) groupedtt 
ON tt.home = groupedtt.home 
AND tt.datetime = groupedtt.MaxDateTime

resultado mostra - "# 1054 - Coluna desconhecida 'tt.MaxDateTime' na 'lista de campos'"
Istiaque Ahmed

@IstiaqueAhmed você tem o MaxDatetime arquivado, ou seja, qualquer nome de coluna como esse ..?
Manoj Kargeti 08/11

Não, a tabela no OP não possui essa coluna.
Istiaque Ahmed

o erro também dizendo o mesmo por favor..o que exatamente você quer fazer? você pode enviar a estrutura da tabela e sua consulta.
Manoj Kargeti

1

Espero que a consulta abaixo dê a saída desejada:

Select id, home,datetime,player,resource, row_number() over (Partition by home ORDER by datetime desc) as rownum from tablename where rownum=1
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.