Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Obtenha registros com o maior/menor por grupo


Então você quer obter a linha com o maior OrderField por 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, talvez queira 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, retorne a linha t1 para a qual nenhuma outra linha t2 existe com o mesmo GroupId e um OrderField maior . Quando t2.* for NULL, significa que a junção externa esquerda não encontrou tal correspondência e, portanto, t1 tem o maior valor de OrderField no 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 em (GroupId, OrderField) .

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

É importante que você tenha o índice certo 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 redefinidos para zero após a consulta processar a primeira tabela. Eu vou te mostrar um exemplo.

Eu 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 os retorna 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 buscamos 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 sobrepõem às classificações da primeira tabela derivada.

Você teria que adicionar outra tabela derivada para forçar @Rank a redefinir para zero 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 unir tabelas temporárias. Este é um exemplo de saída 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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Considerando que minha solução usando a junção externa esquerda otimiza muito melhor. Ele não usa tabela temporária e até relata "Using index" o que significa que ele 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 lerá pessoas fazendo afirmações em seus blogs que "junções tornam o SQL lento", mas isso não faz sentido. A otimização ruim torna o SQL lento.