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

88

Comment faire ça?

L'ancien titre de cette question était "en utilisant le rang (@Rank: = @Rank + 1) dans une requête complexe avec des sous-requêtes - cela fonctionnera-t-il? " Parce que je cherchais une solution utilisant des rangs, mais maintenant je vois que la solution publiée par Bill est beaucoup mieux.

Question originale:

J'essaie de composer une requête qui prendrait le dernier enregistrement de chaque groupe selon un 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

L'expression @Rank := @Rank + 1est normalement utilisée pour le rang, mais pour moi, elle semble suspecte lorsqu'elle est utilisée dans 2 sous-requêtes, mais initialisée une seule fois. Cela fonctionnera-t-il de cette façon?

Et deuxièmement, fonctionnera-t-il avec une sous-requête évaluée plusieurs fois? Comme la sous-requête dans la clause where (ou ayant) (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!

TMS
la source
2
question plus avancée ici stackoverflow.com/questions/9841093/…
TMS

Réponses:

174

Vous voulez donc obtenir la ligne avec le plus élevé OrderFieldpar 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 avez besoin exactement de l'un d'entre eux, vous pouvez étendre la condition:

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 de l'édition.)

En d'autres termes, renvoie la ligne t1pour laquelle aucune autre ligne t2n'existe avec le même GroupIdet un supérieur OrderField. Lorsque la t2.*valeur est NULL, cela signifie que la jointure externe gauche n'a trouvé aucune correspondance de ce type et a donc t1la plus grande valeur de OrderFielddans 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é activé (GroupId, OrderField).


En ce qui concerne les performances, consultez ma réponse à la 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 de jointure a fonctionné 278 fois plus vite dans mon test.

Il est important que vous ayez le bon index 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 remises à 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 factices, 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 passe à 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 |
+---------+---------+

Maintenant, exécutez 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 de ce qui précède que le rang maximum par groupe est correct, mais alors 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 chevaucheront jamais du tout les rangs de la première table dérivée.

Vous devrez ajouter une autre table dérivée pour forcer @Rank à se remettre à zéro entre le traitement des deux tables (et j'espère 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. Ceci est 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 s'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 faisant des déclarations sur leurs blogs selon lesquelles «les jointures ralentissent SQL», mais cela n'a aucun sens. Une mauvaise optimisation ralentit SQL.

Bill Karwin
la source
Cela peut s'avérer très utile (pour le PO également), mais ne répond malheureusement à aucune des deux questions posées.
Andriy M
Merci Bill, c'est une bonne idée de comment éviter les rangs, mais ... la jointure ne serait-elle pas lente? La jointure (sans la limitation de la clause where) serait de taille beaucoup plus grande que dans mes requêtes. Bref, merci pour l'idée! Mais je serais également intéressant dans la question initiale, c'est-à-dire si les rangs fonctionneraient de cette façon.
TMS
Merci pour l'excellente réponse, Bill. Cependant, que se passe-t-il si j'utilise @Rank1et @Rank2, un pour chaque sous-requête? Cela réglerait-il le problème? Serait-ce plus rapide que votre solution?
TMS
Utiliser @Rank1et @Rank2ne ferait aucune différence.
Bill Karwin
2
Merci pour cette excellente solution. Je luttais depuis longtemps avec ce problème. Pour les personnes qui souhaitent ajouter des filtres pour les autres champs, par exemple "foo", vous devez les ajouter à la condition de jointure ... AND t1.foo = t2.foopour obtenir plus tard les résultats corrects pourWHERE ... AND foo='bar'
ownking