Erhalten Sie Datensätze mit dem höchsten / kleinsten <was auch immer> pro Gruppe


88

Wie geht das?

Der frühere Titel dieser Frage lautete " Verwenden von Rang (@Rank: = @Rank + 1) in komplexen Abfragen mit Unterabfragen - funktioniert das? ", Weil ich nach einer Lösung mit Rängen gesucht habe, aber jetzt sehe ich, dass die von Bill veröffentlichte Lösung ist viel viel besser.

Ursprüngliche Frage:

Ich versuche, eine Abfrage zu erstellen, die den letzten Datensatz von jeder Gruppe in einer bestimmten Reihenfolge übernimmt:

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

Ausdruck @Rank := @Rank + 1wird normalerweise für Rang verwendet, aber für mich sieht er verdächtig aus, wenn er in 2 Unterabfragen verwendet wird, aber nur einmal initialisiert. Wird es so funktionieren?

Und zweitens, funktioniert es mit einer Unterabfrage, die mehrfach ausgewertet wird? Wie eine Unterabfrage in der where (oder have) -Klausel (eine andere Art, das Obige zu schreiben):

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

Danke im Voraus!


2
Fortgeschrittenere Frage hier stackoverflow.com/questions/9841093/…
TMS

Antworten:


172

Sie möchten also die Zeile mit der höchsten OrderFieldpro Gruppe erhalten? Ich würde es so machen:

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)

( BEARBEITEN von Tomas: Wenn sich innerhalb derselben Gruppe mehr Datensätze mit demselben OrderField befinden und Sie genau einen davon benötigen, können Sie die Bedingung erweitern:

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

Ende der Bearbeitung.)

Mit anderen Worten, geben Sie die Zeile zurück, t1für die keine andere Zeile t2mit derselben GroupIdund einer größeren vorhanden ist OrderField. Wenn t2.*NULL ist, bedeutet dies, dass der linke äußere Join keine solche Übereinstimmung gefunden hat und daher t1den größten Wert OrderFieldin der Gruppe hat.

Keine Ränge, keine Unterabfragen. Dies sollte schnell laufen und den Zugriff auf t2 mit "Using index" optimieren, wenn Sie einen zusammengesetzten Index aktiviert haben (GroupId, OrderField).


Informationen zur Leistung finden Sie in meiner Antwort zum Abrufen des letzten Datensatzes in jeder Gruppe . Ich habe eine Unterabfragemethode und die Verknüpfungsmethode mithilfe des Stapelüberlauf-Datendumps ausprobiert. Der Unterschied ist bemerkenswert: Die Join-Methode lief in meinem Test 278-mal schneller.

Es ist wichtig, dass Sie den richtigen Index haben, um die besten Ergebnisse zu erzielen!

In Bezug auf Ihre Methode, die die Variable @Rank verwendet, funktioniert sie nicht so, wie Sie sie geschrieben haben, da die Werte von @Rank nicht auf Null zurückgesetzt werden, nachdem die Abfrage die erste Tabelle verarbeitet hat. Ich zeige Ihnen ein Beispiel.

Ich habe einige Dummy-Daten eingefügt, mit einem zusätzlichen Feld, das null ist, außer in der Zeile, von der wir wissen, dass sie die größte pro Gruppe ist:

select * from `Table`;

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

Wir können zeigen, dass der Rang für die erste Gruppe auf drei und für die zweite Gruppe auf sechs steigt und die innere Abfrage diese korrekt zurückgibt:

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

Führen Sie nun die Abfrage ohne Verknüpfungsbedingung aus, um ein kartesisches Produkt aller Zeilen zu erzwingen, und rufen Sie auch alle Spalten ab:

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

Wir können aus dem Obigen ersehen, dass der maximale Rang pro Gruppe korrekt ist, aber dann steigt der @Rank weiter an, wenn er die zweite abgeleitete Tabelle auf 7 und höher verarbeitet. Die Ränge aus der zweiten abgeleiteten Tabelle überschneiden sich also niemals mit den Rängen aus der ersten abgeleiteten Tabelle.

Sie müssten eine weitere abgeleitete Tabelle hinzufügen, um zu zwingen, dass @Rank zwischen der Verarbeitung der beiden Tabellen auf Null zurückgesetzt wird (und hoffen, dass das Optimierungsprogramm die Reihenfolge, in der Tabellen ausgewertet werden, nicht ändert, oder STRAIGHT_JOIN verwenden, um dies zu verhindern):

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

Aber die Optimierung dieser Abfrage ist schrecklich. Es kann keine Indizes verwenden, es erstellt zwei temporäre Tabellen, sortiert sie auf die harte Tour und verwendet sogar einen Verknüpfungspuffer, da es auch beim Verknüpfen von temporären Tabellen keinen Index verwenden kann. Dies ist eine Beispielausgabe von 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                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Während meine Lösung mit der linken äußeren Verbindung viel besser optimiert. Es verwendet keine temporäre Tabelle und sogar Berichte, "Using index"was bedeutet, dass der Join nur mithilfe des Index aufgelöst werden kann, ohne die Daten zu berühren.

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

Sie werden wahrscheinlich Leute lesen, die in ihren Blogs behaupten, dass "Joins SQL langsam machen", aber das ist Unsinn. Eine schlechte Optimierung macht SQL langsam.


Dies mag sich als sehr nützlich erweisen (auch für das OP), beantwortet aber leider keine der beiden gestellten Fragen.
Andriy M

Danke Bill, das ist eine gute Idee, wie man die Ränge meidet, aber ... wäre der Join nicht langsam? Der Join (ohne die Einschränkung der where-Klausel) wäre viel größer als in meinen Abfragen. Trotzdem danke für die Idee! Aber ich wäre auch in der ursprünglichen Frage interessant, dh ob die Reihen so funktionieren würden.
TMS

Danke für die hervorragende Antwort, Bill. Was ist jedoch, wenn ich @Rank1und @Rank2eine für jede Unterabfrage verwendet habe? Würde das das Problem beheben? Wäre das schneller als Ihre Lösung?
TMS

Verwenden @Rank1und @Rank2würde keinen Unterschied machen.
Bill Karwin

2
Vielen Dank für diese großartige Lösung. Ich hatte lange mit diesem Problem zu kämpfen. Für die Leute, die Filter für die anderen Felder hinzufügen möchten, z. B. "foo", müssen Sie sie zur Join-Bedingung hinzufügen, um ... AND t1.foo = t2.foospäter die richtigen Ergebnisse fürWHERE ... AND foo='bar'
ownking
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.