Come posso ottenere il valore maggiore attuale e successivo in una selezione?


18

Ho una tabella InnoDB 'idtimes' (MySQL 5.0.22-log) con colonne

`id` int(11) NOT NULL,
`time` int(20) NOT NULL, [...]

con una chiave unica composta

UNIQUE KEY `id_time` (`id`,`time`)

quindi possono esserci più timestamp per ID e più ID per timestamp.

Sto cercando di impostare una query in cui ottengo tutte le voci più il tempo successivo successivo per ciascuna voce, se esiste, quindi dovrebbe restituire ad esempio:

+-----+------------+------------+
| id  | time       | nexttime   |
+-----+------------+------------+
| 155 | 1300000000 | 1311111111 |
| 155 | 1311111111 | 1322222222 |
| 155 | 1322222222 |       NULL |
| 156 | 1312345678 | 1318765432 |
| 156 | 1318765432 |       NULL |
+-----+------------+------------+

In questo momento sono così lontano:

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id
    WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

ma ovviamente questo restituisce tutte le righe con r.time> l.time e non solo il primo ...

Immagino che avrò bisogno di una sottoselezione come

SELECT outer.id, outer.time, 
    (SELECT time FROM idtimes WHERE id = outer.id AND time > outer.time 
        ORDER BY time ASC LIMIT 1)
    FROM idtimes AS outer ORDER BY outer.id ASC, outer.time ASC;

ma non so come fare riferimento all'ora corrente (so che quanto sopra non è SQL valido).

Come posso fare questo con una singola query (e preferirei non usare @variables che dipendono dall'andare nella tabella una riga alla volta e ricordare l'ultimo valore)?

Risposte:


20

Fare un JOIN è una cosa di cui potresti aver bisogno.

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id

Suppongo che il join esterno sia intenzionale e che tu voglia ottenere null. Ne parleremo più avanti.

WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

Vuoi solo la r. riga che ha il tempo più basso (MIN) che è superiore al tempo l. Questo è il posto dove hai bisogno di subquery.

WHERE r.time = (SELECT MIN(time) FROM idtimes r2 where r2.id = l.id AND r2.time > l.time)

Ora ai nulli. Se "non esiste un tempo successivo più alto", SELECT MIN () valuterà null (o peggio) e che non si confronta mai uguale a nulla, quindi la clausola WHERE non sarà mai soddisfatta e il "tempo più alto" per ogni ID, non potrebbe mai apparire nel set di risultati.

Lo risolvi eliminando il tuo JOIN e spostando la subquery scalare nell'elenco SELECT:

SELECT id, time, 
    (SELECT MIN(time) FROM idtimes sub 
        WHERE sub.id = main.id AND sub.time > main.time) as nxttime
  FROM idtimes AS main 

4

Evito sempre di usare le subquery sia in SELECTblocco che inFROM blocco, perché rende il codice "più sporco" e talvolta meno efficiente.

Penso che un modo più elegante per farlo sia:

1. Trova i tempi maggiori del tempo della riga

Puoi farlo con una tabella JOINtra idtimes con se stesso, vincolando il join allo stesso ID e a tempi maggiori del tempo della riga corrente.

Dovresti usare LEFT JOINper evitare di escludere le righe dove non ci sono volte maggiori di quella della riga corrente.

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS greater_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time

Il problema, come hai detto, è che hai più righe in cui next_time è maggiore del tempo .

+-----+------------+--------------+
| id  | time       | greater_time |
+-----+------------+--------------+
| 155 | 1300000000 | 1311111111   |
| 155 | 1300000000 | 1322222222   |
| 155 | 1311111111 | 1322222222   |
| 155 | 1322222222 |       NULL   |
| 156 | 1312345678 | 1318765432   |
| 156 | 1318765432 |       NULL   |
+-----+------------+--------------+

2. Trova le righe in cui greater_time non è solo maggiore ma next_time

Il modo migliore per filtrare tutte queste righe inutili è scoprire se ci sono tempi tra il tempo (maggiore di) e maggiore_tempo (minore di) per questo ID .

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS next_time,
    i3.time AS intrudor_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time
    LEFT JOIN idtimes AS i3 ON i2.id = i3.id AND i3.time > i1.time AND i3.time < i2.time

ops, abbiamo ancora un falso next_time !

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1300000000 | 1322222222   |    1311111111 |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Basta filtrare le righe in cui si verifica questo evento, aggiungendo il WHEREvincolo di seguito

WHERE
    i3.time IS NULL

Voilà, abbiamo quello che ci serve!

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Spero che tu abbia ancora bisogno di una risposta dopo 4 anni!


È intelligente. Non sono sicuro che sia più facile da capire però. Penso che se avessimo sostituito il is nulle il join a i3 where not exists (select 1 from itimes i3 where [same clause]), il codice rispecchierebbe più da vicino ciò che vogliamo esprimere.
Andrew Spencer,

grazie amico mi hai salvato il mio (prossimo) giorno!
Jakob,

2

Prima di presentare la soluzione, dovrei notare che non è carino. Sarebbe molto più semplice se avessi qualche AUTO_INCREMENTcolonna sul tuo tavolo (vero?)

SELECT 
  l.id, l.time, 
  SUBSTRING_INDEX(GROUP_CONCAT(r.time ORDER BY r.time), ',', 1)
FROM 
  idtimes AS l 
  LEFT JOIN idtimes AS r ON (l.id = r.id)
WHERE 
  l.time < r.time
GROUP BY
  l.id, l.time

Spiegazione:

  • Stesso join del tuo: unisci due tavoli, quello giusto ottiene solo i tempi più alti
  • Raggruppa per entrambe le colonne dalla tabella di sinistra: questo ci assicura di ottenere tutto (id, time) combinazioni (che sono anche note per essere uniche).
  • Per ognuno (l.id, l.time), ottieni il primo r.time che è maggiore di l.time. Questo accade con il primo ordine di r.times via GROUP_CONCAT(r.time ORDER BY r.time), il tagliando il primo token via SUBSTRING_INDEX.

Buona fortuna e non aspettarti buone prestazioni se questa tabella è grande.


2

Puoi anche ottenere ciò che desideri da un min()e GROUP BYsenza selezione interna:

SELECT l.id, l.time, min(r.time) 
FROM idtimes l 
LEFT JOIN idtimes r on (r.id = l.id and r.time > l.time)
GROUP BY l.id, l.time;

Vorrei quasi scommesso una grossa somma di denaro che l'ottimizzatore trasforma questo nella stessa cosa di risposta di Erwin Smout in ogni caso, ed è discutibile se è più chiaro, ma è così per completezza ...


1
Per quello che vale, SSMS e SQLServer 2016 hanno gradito la tua query molto più di quella di Erwin (tempo di esecuzione 2s rispetto al tempo di esecuzione 24s su set di risultati ~ 24k)
Nathan Lafferty,

Andrew sembra che tu abbia perso la scommessa :-)
Erwin Smout

Interessante, perché dovrebbe essere un caso generale che una sottoquery che si ricolleghi alla tabella di query esterna da una delle colonne PK sia la stessa di un gruppo di. Mi chiedo se qualsiasi altro database lo ottimizzerebbe meglio. (So ​​molto poco sugli ottimizzatori di database BTW; sono solo curioso.)
Andrew Spencer,
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.