Obtenha registros com o maior / menor <qualquer> por grupo


88

Como fazer isso?

O título anterior desta questão era " usando classificação (@Rank: = @Rank + 1) em consulta complexa com subconsultas - funcionará? " Porque eu estava procurando uma solução usando classificações, mas agora vejo que a solução postada por Bill é muito, muito melhor.

Questão original:

Estou tentando compor uma consulta que levaria o último registro de cada grupo dada alguma ordem definida:

SET @Rank=0;

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from Table
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from Table
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField

Expression @Rank := @Rank + 1é normalmente usado para classificação, mas para mim parece suspeito quando usado em 2 subconsultas, mas inicializado apenas uma vez. Vai funcionar assim?

Em segundo lugar, funcionará com uma subconsulta avaliada várias vezes? Como subconsulta na cláusula where (ou having) (outra forma de escrever acima):

SET @Rank=0;

select Table.*, @Rank := @Rank + 1 AS Rank
from Table
having Rank = (select max(Rank) AS MaxRank
              from (select GroupId, @Rank := @Rank + 1 AS Rank 
                    from Table as t0
                    order by OrderField
                    ) as t
              where t.GroupId = table.GroupId
             )
order by OrderField

Desde já, obrigado!


2
questão mais avançada aqui stackoverflow.com/questions/9841093/…
TMS

Respostas:


174

Então você deseja obter a linha com o maior número OrderFieldpor grupo? Eu faria assim:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId AND t1.OrderField < t2.OrderField
WHERE t2.GroupId IS NULL
ORDER BY t1.OrderField; // not needed! (note by Tomas)

( EDIT por Tomas: Se houver mais registros com o mesmo OrderField dentro do mesmo grupo e você precisar exatamente de um deles, você pode querer estender a condição:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId 
        AND (t1.OrderField < t2.OrderField 
         OR (t1.OrderField = t2.OrderField AND t1.Id < t2.Id))
WHERE t2.GroupId IS NULL

fim da edição.)

Em outras palavras, retorna a linha t1para a qual nenhuma outra linhat2 existe com o mesmo GroupIde um maior OrderField. Quando t2.*é NULL, significa que a junção externa esquerda não encontrou tal correspondência e, portanto, t1tem o maior valor de OrderFieldno grupo.

Sem classificações, sem subconsultas. Isso deve ser executado rapidamente e otimizar o acesso a t2 com "Usando índice" se você tiver um índice composto ativado (GroupId, OrderField).


Com relação ao desempenho, veja minha resposta em Recuperando o último registro de cada grupo . Tentei um método de subconsulta e o método de junção usando o despejo de dados Stack Overflow. A diferença é notável: o método join foi executado 278 vezes mais rápido em meu teste.

É importante que você tenha o índice correto para obter os melhores resultados!

Com relação ao seu método usando a variável @Rank, ele não funcionará como você o escreveu, porque os valores de @Rank não serão zerados após a consulta ter processado a primeira tabela. Vou te mostrar um exemplo.

Inseri alguns dados fictícios, com um campo extra que é nulo, exceto na linha que sabemos ser a maior por grupo:

select * from `Table`;

+---------+------------+------+
| GroupId | OrderField | foo  |
+---------+------------+------+
|      10 |         10 | NULL |
|      10 |         20 | NULL |
|      10 |         30 | foo  |
|      20 |         40 | NULL |
|      20 |         50 | NULL |
|      20 |         60 | foo  |
+---------+------------+------+

Podemos mostrar que a classificação aumenta para três para o primeiro grupo e seis para o segundo grupo, e a consulta interna retorna estes corretamente:

select GroupId, max(Rank) AS MaxRank
from (
  select GroupId, @Rank := @Rank + 1 AS Rank
  from `Table`
  order by OrderField) as t
group by GroupId

+---------+---------+
| GroupId | MaxRank |
+---------+---------+
|      10 |       3 |
|      20 |       6 |
+---------+---------+

Agora execute a consulta sem condição de junção, para forçar um produto cartesiano de todas as linhas, e também buscaremos todas as colunas:

select s.*, t.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  -- on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+---------+---------+------------+------+------+
| GroupId | MaxRank | GroupId | OrderField | foo  | Rank |
+---------+---------+---------+------------+------+------+
|      10 |       3 |      10 |         10 | NULL |    7 |
|      20 |       6 |      10 |         10 | NULL |    7 |
|      10 |       3 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         30 | foo  |    9 |
|      10 |       3 |      10 |         30 | foo  |    9 |
|      10 |       3 |      20 |         40 | NULL |   10 |
|      20 |       6 |      20 |         40 | NULL |   10 |
|      10 |       3 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         60 | foo  |   12 |
|      10 |       3 |      20 |         60 | foo  |   12 |
+---------+---------+---------+------------+------+------+

Podemos ver acima que a classificação máxima por grupo está correta, mas o @Rank continua a aumentar à medida que processa a segunda tabela derivada, para 7 e acima. Portanto, as classificações da segunda tabela derivada nunca se sobreporão às classificações da primeira tabela derivada.

Você teria que adicionar outra tabela derivada para forçar o @Rank a zerar entre o processamento das duas tabelas (e esperar que o otimizador não altere a ordem em que avalia as tabelas, ou então use STRAIGHT_JOIN para evitar isso):

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (select @Rank := 0) r -- RESET @Rank TO ZERO HERE
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+------------+------+------+
| GroupId | OrderField | foo  | Rank |
+---------+------------+------+------+
|      10 |         30 | foo  |    3 |
|      20 |         60 | foo  |    6 |
+---------+------------+------+------+

Mas a otimização dessa consulta é terrível. Ele não pode usar nenhum índice, cria duas tabelas temporárias, classifica-as da maneira mais difícil e até usa um buffer de junção porque também não pode usar um índice ao juntar tabelas temporárias. Este é um exemplo de resultado de EXPLAIN:

+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table      | type   | possible_keys | key  | key_len | ref  | rows | Extra                           |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
|  1 | PRIMARY     | <derived4> | system | NULL          | NULL | NULL    | NULL |    1 | Using temporary; Using filesort |
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL | NULL    | NULL |    2 |                                 |
|  1 | PRIMARY     | <derived5> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using where; Using join buffer  |
|  5 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
|  4 | DERIVED     | NULL       | NULL   | NULL          | NULL | NULL    | NULL | NULL | No tables used                  |
|  2 | DERIVED     | <derived3> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using temporary; Using filesort |
|  3 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Enquanto minha solução usando a junção externa esquerda otimiza muito melhor. Ele não usa nenhuma tabela temporária e até mesmo relatórios, o "Using index"que significa que pode resolver a junção usando apenas o índice, sem tocar nos dados.

+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref             | rows | Extra                    |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
|  1 | SIMPLE      | t1    | ALL  | NULL          | NULL    | NULL    | NULL            |    6 | Using filesort           |
|  1 | SIMPLE      | t2    | ref  | GroupId       | GroupId | 5       | test.t1.GroupId |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+

Você provavelmente vai ler pessoas fazendo declarações em seus blogs que "as junções tornam o SQL lento", mas isso é um absurdo. A otimização deficiente torna o SQL lento.


Isso pode ser muito útil (para o OP também), mas, infelizmente, não responde a nenhuma das duas perguntas feitas.
Andriy M

Obrigado Bill, é uma boa ideia evitar as classificações, mas ... a adesão não seria lenta? A junção (sem a limitação da cláusula where) seria de tamanho muito maior do que em minhas consultas. De qualquer forma, obrigado pela ideia! Mas eu também ficaria interessante na questão original, ou seja, se as classificações funcionariam dessa forma.
TMS

Obrigado pela excelente resposta, Bill. No entanto, e se eu usasse @Rank1e @Rank2, um para cada subconsulta? Isso resolveria o problema? Isso seria mais rápido do que sua solução?
TMS de

Usar @Rank1e @Rank2não faria diferença.
Bill Karwin

2
Obrigado por essa ótima solução. Eu estava lutando muito tempo com esse problema. Para as pessoas que desejam adicionar filtros para os outros campos, por exemplo, "foo", você precisa adicioná-los à condição de junção ... AND t1.foo = t2.foopara mais tarde obter os resultados corretos paraWHERE ... AND foo='bar'
propriedade
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.