Seleziona la sequenza continua più lunga


12

Sto cercando di costruire una query in PostgreSQL 9.0 che ottenga la sequenza più lunga di righe continue per una colonna specifica.

Considera la seguente tabella:

lap_id (serial), lap_no (int), car_type (enum), race_id (int FK)

Dove lap_noè unico per ciascuno (race_id, car_type).

Vorrei che la query producesse la sequenza più lunga per un dato race_ide car_type, quindi, restituirebbe int(o long) il più alto.

Con i seguenti dati:

1, 1, red, 1
2, 2, red, 1
3, 3, red, 1
4, 4, red, 1
5, 1, blue, 1
6, 5, red, 1
7, 2, blue, 1
8, 1, green, 1

Per car_type = red and race_id = 1la query tornerebbe 5come la sequenza più lunga del lap_nocampo.

Ho trovato una domanda simile qui, tuttavia la mia situazione è un po 'più semplice.

(Vorrei anche sapere la sequenza più lunga per un dato car_typeper tutte le gare, ma stavo pianificando di risolverlo da solo.)

Risposte:


20

La tua descrizione risulta in una definizione di tabella come questa:

CREATE TABLE tbl (
   lap_id   serial PRIMARY KEY
 , lap_no   int NOT NULL
 , car_type enum NOT NULL
 , race_id  int NOT NULL  -- REFERENCES ...
 , UNIQUE(race_id, car_type, lap_no)
);

Soluzione generale per questa classe di problemi

Per ottenere la sequenza più lunga (1 risultato, il più lungo di tutti, scelta arbitraria in caso di legami):

SELECT race_id, car_type, count(*) AS seq_len
FROM  (
   SELECT *, count(*) FILTER (WHERE step)
                      OVER (ORDER BY race_id, car_type, lap_no) AS grp
   FROM  (
      SELECT *, (lag(lap_no) OVER (PARTITION BY race_id, car_type ORDER BY lap_no) + 1)
                 IS DISTINCT FROM lap_no AS step
      FROM   tbl
      ) x
   ) y
GROUP  BY race_id, car_type, grp
ORDER  BY seq_len DESC
LIMIT  1;

count(*) FILTER (WHERE step)conta solo TRUE(= passaggio al gruppo successivo), che risulta in un nuovo numero per ogni nuovo gruppo.

Domanda correlata su SO, una risposta con una soluzione procedurale con plpgsql :

Se il requisito principale è la prestazione, la funzione plpgsql è in genere più veloce in questo caso particolare perché può calcolare il risultato in una singola scansione.

Più veloce per numeri consecutivi

Possiamo capitalizzare sul fatto che consecutivamente lap_no definire una sequenza, per una versione molto più semplice e veloce :

SELECT race_id, car_type, count(*) AS seq_len
FROM  (
   SELECT race_id, car_type
        , row_number() OVER (PARTITION BY race_id, car_type ORDER BY lap_no) - lap_no AS grp
   FROM   tbl
   ) x
GROUP  BY race_id, car_type, grp
ORDER  BY seq_len DESC
LIMIT  1;

I giri consecutivi finiscono nello stesso grp. Ogni giro mancante comporta una riduzione grpper partizione.

Questo si basa (race_id, car_type, lap_no)sull'essere UNIQUE NOT NULL. Valori NULL o duplicati potrebbero violare la logica.

Discussione sull'alternativa più semplice di Jack

La versione di @ Jack conta effettivamente tutti i giri (righe) in cui il precedente lap_noin questo race_idaveva lo stesso car_type. È più semplice, veloce e corretto, purché ciascuno car_typepossa avere solo una sequenza per race_id.

Ma per un'attività così semplice la query potrebbe essere ancora più semplice. Seguirebbe logicamente che tutto lap_noper (car_type, race_id)deve essere in sequenza e potremmo semplicemente contare i giri:

SELECT race_id, car_type, count(*) AS seq_len
FROM   tbl
GROUP  BY race_id, car_type
ORDER  BY seq_len DESC
LIMIT  1;

Se, d'altra parte, si car_typepossono avere più sequenze separate per race_id (e la domanda non specifica diversamente), la versione di Jack fallirà.

Più veloce per un determinato tipo di gara / auto

In risposta al commento / chiarimenti nella domanda: limitare la query a un dato (race_id, car_type) renderà molto più veloce , ovviamente:

SELECT count(*) AS seq_len
FROM  (
   SELECT row_number() OVER (ORDER BY lap_no) - lap_no AS grp
   FROM   tbl
   WHERE  race_id = 1
   AND    car_type = 'red'
   ) x
GROUP  BY grp
ORDER  BY seq_len DESC
LIMIT  1;

db <> violino qui
Vecchio SQL Fiddle

Indice

La chiave per le massime prestazioni è un indice adeguato (ad eccezione della soluzione procedurale menzionata che funziona con una singola scansione sequenziale). Un indice a più colonne come questo serve al meglio:

CREATE INDEX tbl_mult_idx ON tbl (race_id, car_type, lap_no);

Se la tua tabella ha il UNIQUEvincolo che ho assunto in alto, questo è implementato con questo indice (unico) internamente e non è necessario creare un altro indice.


Ciao Erwin, grazie che fa il lavoro, tuttavia ci vogliono circa 17 secondi sul mio database! Supponiamo che tu non possa fornire una modifica in modo da prendere race_id e car_type come parametri piuttosto che confrontare l'intera tabella? (Ho provato a riscriverlo e continuo a incorrere in errori)
DaveB,

7

create table tbl (lap_no int, car_type text, race_id int);
insert into tbl values (1,'red',1),(2,'red',1),(3,'red',1),(4,'red',1),
                       (1,'blue',1),(5,'red',1),(2,'blue',1),(1,'green',1);
select car_type, race_id, sum(case when lap_no=(prev+1) then 1 else 0 end)+1 seq_len
from ( select *, lag(lap_no) over (partition by car_type, race_id order by lap_no) prev 
       from tbl ) z
group by car_type, race_id
order by seq_len desc limit 1;
/*
|car_type|race_id|seq_len|
|:-------|------:|------:|
|red     |      1|      5|
*/

o forse sum((lap_no=(prev+1))::integer)+1ma non sono sicuro che sia più facile da leggere
Jack dice di provare topanswers.xyz 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.