Ottieni record con <qualunque> più alto / più piccolo per gruppo


88

Come farlo?

L'ex titolo di questa domanda era " utilizzo del rango (@Rank: = @Rank + 1) in query complesse con sottoquery - funzionerà? " Perché stavo cercando una soluzione utilizzando i ranghi, ma ora vedo che la soluzione pubblicata da Bill è molto molto meglio.

Domanda originale:

Sto cercando di comporre una query che prenderebbe l'ultimo record da ogni gruppo dato un ordine definito:

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'espressione @Rank := @Rank + 1è normalmente usata per il rango, ma per me sembra sospetta se usata in 2 sottoquery, ma inizializzata solo una volta. Funzionerà in questo modo?

In secondo luogo, funzionerà con una sottoquery valutata più volte? Come la sottoquery nella clausola where (o having) (un altro modo per scrivere quanto sopra):

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

Grazie in anticipo!


2
domanda più avanzata qui stackoverflow.com/questions/9841093/…
TMS

Risposte:


174

Quindi vuoi ottenere la riga con il più alto OrderFieldper gruppo? Lo farei in questo modo:

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 di Tomas: se ci sono più record con lo stesso OrderField all'interno dello stesso gruppo e ne hai bisogno esattamente uno, potresti voler estendere la condizione:

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

fine della modifica.)

In altre parole, restituisce la riga t1per la quale non esistono altre righe t2con la stessa GroupIde una maggiore OrderField. Quando t2.*è NULL, significa che il join esterno sinistro non ha trovato alcuna corrispondenza di questo tipo e quindi t1ha il valore maggiore di OrderFieldnel gruppo.

Niente ranghi, niente sottoquery. Questo dovrebbe essere veloce e ottimizzare l'accesso a t2 con "Using index" se hai un indice composto attivo (GroupId, OrderField).


Per quanto riguarda le prestazioni, vedere la mia risposta a Recupero dell'ultimo record in ogni gruppo . Ho provato un metodo di subquery e il metodo di join utilizzando il dump dei dati di Stack Overflow. La differenza è notevole: il metodo di unione è stato eseguito 278 volte più velocemente nel mio test.

È importante che tu abbia l'indice giusto per ottenere i migliori risultati!

Per quanto riguarda il tuo metodo che utilizza la variabile @Rank, non funzionerà come l'hai scritta, perché i valori di @Rank non verranno ripristinati a zero dopo che la query ha elaborato la prima tabella. Ti faccio vedere un esempio.

Ho inserito alcuni dati fittizi, con un campo in più che è nullo tranne che sulla riga che sappiamo essere la più grande per gruppo:

select * from `Table`;

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

Possiamo mostrare che il rango aumenta a tre per il primo gruppo e sei per il secondo gruppo, e la query interna li restituisce correttamente:

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

Ora esegui la query senza condizione di join, per forzare un prodotto cartesiano di tutte le righe e recuperiamo anche tutte le colonne:

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

Possiamo vedere da quanto sopra che il ranking massimo per gruppo è corretto, ma poi il @Rank continua ad aumentare mentre elabora la seconda tabella derivata, fino a 7 e oltre. Quindi i ranghi della seconda tabella derivata non si sovrapporranno mai ai ranghi della prima tabella derivata.

Dovresti aggiungere un'altra tabella derivata per forzare @Rank a reimpostare a zero tra l'elaborazione delle due tabelle (e sperare che l'ottimizzatore non cambi l'ordine in cui valuta le tabelle, oppure usa STRAIGHT_JOIN per impedirlo):

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

Ma l'ottimizzazione di questa query è terribile. Non può utilizzare alcun indice, crea due tabelle temporanee, le ordina nel modo più duro e utilizza persino un buffer di join perché non può utilizzare un indice nemmeno quando si uniscono tabelle temporanee. Questo è un esempio di output da 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 che la mia soluzione utilizzando il join esterno sinistro ottimizza molto meglio. Non utilizza tabelle temporanee e persino rapporti, il "Using index"che significa che può risolvere il join utilizzando solo l'indice, senza toccare i dati.

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

Probabilmente leggerai persone che affermano sui loro blog che "i join rendono SQL lento", ma non ha senso. Una scarsa ottimizzazione rende l'SQL lento.


Questo può rivelarsi abbastanza utile (anche per l'OP), ma, purtroppo, non risponde a nessuna delle due domande poste.
Andriy M

Grazie Bill, è una buona idea come evitare i ranghi, ma ... l'unione non sarebbe lenta? Il join (senza la limitazione della clausola where) sarebbe di dimensioni molto maggiori rispetto alle mie query. Comunque grazie per l'idea! Ma sarei interessante anche nella domanda originale, cioè se i ranghi funzionassero in questo modo.
TMS

Grazie per l'eccellente risposta, Bill. Tuttavia, cosa succede se ho usato @Rank1e @Rank2, uno per ogni sottoquery? Questo risolverebbe il problema? Sarebbe più veloce della tua soluzione?
TMS

Usare @Rank1e @Rank2non farebbe differenza.
Bill Karwin

2
Grazie per l'ottima soluzione. Ho lottato a lungo con quel problema. Per le persone che desiderano aggiungere filtri per gli altri campi, ad esempio "pippo", è necessario aggiungerli alla condizione di join ... AND t1.foo = t2.fooper ottenere in seguito i risultati corretti perWHERE ... AND foo='bar'
possedere il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.