web-dev-qa-db-fra.com

Obtenez des enregistrements avec le plus grand / le plus petit <quel que soit> par groupe

Comment faire ça?

L'ancien titre de cette question était "en utilisant rank (@Rank: = @Rank + 1) dans une requête complexe avec des sous-requêtes - cela fonctionnera-t-il?" parce que je cherchais une solution en utilisant ranks, mais maintenant je vois que la solution proposée par Bill est bien meilleure.

Question d'origine:

J'essaie de composer une requête qui prendrait le dernier enregistrement de chaque groupe étant donné un certain ordre défini:

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 est normalement utilisé pour le classement, mais pour moi, il semble suspect lorsqu'il est utilisé dans 2 sous-requêtes, mais initialisé une seule fois. Cela fonctionnera-t-il de cette façon?

Et deuxièmement, cela fonctionnera-t-il avec une sous-requête évaluée plusieurs fois? Comme sous-requête dans la clause where (ou having) (une autre façon d'écrire ce qui précède):

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

Merci d'avance!

84
TMS

Vous voulez donc obtenir la ligne avec le plus haut OrderField par groupe? Je le ferais de cette façon:

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 by Tomas: S'il y a plus d'enregistrements avec le même OrderField dans le même groupe et que vous en avez besoin exactement d'un, vous souhaiterez peut-être étendre état:

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

fin du montage.)

En d'autres termes, renvoyez la ligne t1 pour lequel aucune autre ligne t2 existe avec le même GroupId et un OrderField supérieur. Quand t2.* est NULL, cela signifie que la jointure externe gauche n'a trouvé aucune correspondance, et donc t1 a la plus grande valeur de OrderField dans le groupe.

Pas de rangs, pas de sous-requêtes. Cela devrait fonctionner rapidement et optimiser l'accès à t2 avec "Utilisation de l'index" si vous avez un index composé sur (GroupId, OrderField).


En ce qui concerne les performances, voir ma réponse à Récupération du dernier enregistrement de chaque groupe . J'ai essayé une méthode de sous-requête et la méthode de jointure en utilisant le vidage de données Stack Overflow. La différence est remarquable: la méthode join a fonctionné 278 fois plus vite dans mon test.

Il est important que vous ayez le bon indice pour obtenir les meilleurs résultats!

En ce qui concerne votre méthode utilisant la variable @Rank, elle ne fonctionnera pas comme vous l'avez écrite, car les valeurs de @Rank ne seront pas réinitialisées à zéro une fois que la requête aura traité la première table. Je vais vous montrer un exemple.

J'ai inséré des données fictives, avec un champ supplémentaire qui est nul, sauf sur la ligne que nous savons être la plus grande par groupe:

select * from `Table`;

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

Nous pouvons montrer que le rang augmente à trois pour le premier groupe et à six pour le deuxième groupe, et la requête interne les renvoie correctement:

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 |
+---------+---------+

Exécutez maintenant la requête sans condition de jointure, pour forcer un produit cartésien de toutes les lignes, et nous récupérons également toutes les colonnes:

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 |
+---------+---------+---------+------------+------+------+

Nous pouvons voir à partir de ce qui précède que le rang maximum par groupe est correct, mais ensuite le @Rank continue d'augmenter à mesure qu'il traite la deuxième table dérivée, à 7 et plus. Ainsi, les rangs de la deuxième table dérivée ne se chevaucheront jamais avec les rangs de la première table dérivée.

Vous devez ajouter une autre table dérivée pour forcer @Rank à se remettre à zéro entre les deux tables (et espérer que l'optimiseur ne change pas l'ordre dans lequel il évalue les tables, ou bien utilisez STRAIGHT_JOIN pour éviter cela):

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 |
+---------+------------+------+------+

Mais l'optimisation de cette requête est terrible. Il ne peut utiliser aucun index, il crée deux tables temporaires, les trie à la dure et utilise même un tampon de jointure car il ne peut pas non plus utiliser d'index lors de la jonction de tables temporaires. Voici un exemple de sortie 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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Alors que ma solution utilisant la jointure externe gauche optimise beaucoup mieux. Il n'utilise aucune table temporaire et même des rapports "Using index" ce qui signifie qu'il peut résoudre la jointure en utilisant uniquement l'index, sans toucher aux données.

+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
| 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 |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+

Vous lirez probablement des personnes affirmant sur leurs blogs que "les jointures rendent SQL lent", mais cela n'a aucun sens. Une mauvaise optimisation rend SQL lent.

164
Bill Karwin