Come trovare le lacune nella numerazione sequenziale in mysql?


120

Abbiamo un database con una tabella i cui valori sono stati importati da un altro sistema. C'è una colonna di incremento automatico e non ci sono valori duplicati, ma mancano valori. Ad esempio, eseguendo questa query:

select count(id) from arrc_vouchers where id between 1 and 100

dovrebbe restituire 100, ma restituisce 87 invece. C'è qualche query che posso eseguire che restituirà i valori dei numeri mancanti? Ad esempio, i record possono esistere per ID 1-70 e 83-100, ma non ci sono record con ID 71-82. Voglio restituire 71, 72, 73, ecc.

È possibile?


Questo potrebbe non funzionare in MySQL, ma al lavoro (Oracle) avevamo bisogno di qualcosa di simile. Abbiamo scritto uno Stored Proc che ha preso un numero come valore massimo. Lo Stored Proc ha quindi creato una tabella temporanea con una singola colonna. La tabella conteneva tutti i numeri da 1 a Max. Quindi ha fatto un collegamento NOT IN tra il tavolo temporaneo e il nostro tavolo di interesse. Se lo chiami con Max = Seleziona max (id) da arrc_vouchers, restituirà tutti i valori mancanti.
saunderl

2
Cosa c'è di sbagliato nell'avere lacune nella numerazione? Il valore di una chiave surrogata generalmente non è significativo; tutto ciò che conta è che sia unico. Se la tua applicazione non è in grado di gestire ID non contigui, probabilmente è un bug nell'applicazione, non nei dati.
Wyzard

4
In questo caso è un problema perché i dati che abbiamo ereditato dal vecchio sistema utilizzavano il numero di incremento automatico associato a un record come chiave per stampare su una carta fisica che viene distribuita alle persone. Questa NON era la nostra idea. Per scoprire quali carte mancano, dobbiamo sapere dove si trovano gli spazi vuoti nella numerazione sequenziale.
EmmyS

xaprb.com/blog/2005/12/06/… select l.id + 1 as start from sequence as l left outer join sequence as r on l.id + 1 = r.id where r.id is null;

Puoi utilizzare genera serie per generare numeri da 1 all'ID più alto della tua tabella. Quindi esegui una query dove id non è in questa serie.
Tsvetelin Salutski

Risposte:


170

Aggiornare

ConfexianMJS ha fornito una risposta molto migliore in termini di prestazioni.

La risposta (non il più velocemente possibile)

Ecco la versione che funziona su tabelle di qualsiasi dimensione (non solo su 100 righe):

SELECT (t1.id + 1) as gap_starts_at, 
       (SELECT MIN(t3.id) -1 FROM arrc_vouchers t3 WHERE t3.id > t1.id) as gap_ends_at
FROM arrc_vouchers t1
WHERE NOT EXISTS (SELECT t2.id FROM arrc_vouchers t2 WHERE t2.id = t1.id + 1)
HAVING gap_ends_at IS NOT NULL
  • gap_starts_at - primo id nel gap attuale
  • gap_ends_at - ultimo id nel gap attuale

6
Non lavoro più nemmeno per quella società, ma questa è la migliore risposta che ho visto e vale sicuramente la pena ricordarla per riferimento futuro. Grazie!
EmmyS

4
l'unico problema è che non "segnala" un possibile gap iniziale. ad esempio, se mancano i primi 5 ID (da 1 a 5) non mostra che ... Come potremmo mostrare spazi vuoti all'inizio?
DiegoDD

Nota: questa query non funziona su tabelle temporanee. Il mio problema era order numberche stavo cercando spazi vuoti non è distinto (la tabella memorizza le righe dell'ordine, quindi il numero dell'ordine a cui appartengono si ripete per ogni riga). 1a query: 2812 righe nel set (1 min 31,09 sec) . Crea un'altra tabella selezionando numeri d'ordine distinti. La tua query senza le mie ripetizioni: 1009 righe nel set (18.04 sec)
Chris K

1
@DiegoDD Cosa c'è che non va SELECT MIN(id) FROM table?
Aria

8
Ha funzionato ma ci sono volute circa 5 ore per eseguire su un tavolo con 700000 record
Matt

98

Questo ha funzionato per me per trovare gli spazi vuoti in una tabella con più di 80k righe:

SELECT
 CONCAT(z.expected, IF(z.got-1>z.expected, CONCAT(' thru ',z.got-1), '')) AS missing
FROM (
 SELECT
  @rownum:=@rownum+1 AS expected,
  IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got
 FROM
  (SELECT @rownum:=0) AS a
  JOIN YourTable
  ORDER BY YourCol
 ) AS z
WHERE z.got!=0;

Risultato:

+------------------+
| missing          |
+------------------+
| 1 thru 99        |
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
4 rows in set (0.06 sec)

Notare che l'ordine delle colonne expectede gotè fondamentale.

Se sai che YourColnon inizia da 1 e non importa, puoi sostituire

(SELECT @rownum:=0) AS a

con

(SELECT @rownum:=(SELECT MIN(YourCol)-1 FROM YourTable)) AS a

Nuovo risultato:

+------------------+
| missing          |
+------------------+
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
3 rows in set (0.06 sec)

Se è necessario eseguire una sorta di attività di script di shell sugli ID mancanti, è anche possibile utilizzare questa variante per produrre direttamente un'espressione su cui è possibile iterare in bash.

SELECT GROUP_CONCAT(IF(z.got-1>z.expected, CONCAT('$(',z.expected,' ',z.got-1,')'), z.expected) SEPARATOR " ") AS missing
FROM (  SELECT   @rownum:=@rownum+1 AS expected,   IF(@rownum=height, 0, @rownum:=height) AS got  FROM   (SELECT @rownum:=0) AS a   JOIN block   ORDER BY height  ) AS z WHERE z.got!=0;

Questo produce un output simile

$(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456)

Puoi quindi copiarlo e incollarlo in un ciclo for in un terminale bash per eseguire un comando per ogni ID

for ID in $(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456); do
  echo $ID
  # fill the gaps
done

È la stessa cosa di sopra, solo che è sia leggibile che eseguibile. Modificando il comando "CONCAT" sopra, è possibile generare la sintassi per altri linguaggi di programmazione. O forse anche SQL.


8
bella soluzione, per me è meglio della risposta preferita - grazie
Wee Zel

6
È molto più efficiente della risposta accettata.
symcbean

1
molto più veloce della risposta accettata. L'unica cosa che aggiungerei è che CONVERT( YourCol, UNSIGNED )darà risultati migliori se YourCol non è già un numero intero.
Barton Chittenden

1
@AlexandreCassagne: se capisco correttamente la tua domanda, farei semplicemente una query separata come quella incorporata per trovare il minimo:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

1
@temuri Passa alla variante GROUP_CONCAT se necessario: SELECT IF((z.got-IF(z.over>0, z.over, 0)-1)>z.expected, CONCAT(z.expected,' thru ',(z.got-IF(z.over>0, z.over, 0)-1)), z.expected) AS missing FROM ( SELECT @rownum:=@rownum+1 AS expected, @target-@missing AS under, (@missing:=@missing+IF(@rownum=YourCol, 0, YourCol-@rownum))-@target AS over, IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got FROM (SELECT @rownum:=0, @missing:=0, @target:=10) AS a JOIN YourTable ORDER BY YourCol ) AS z WHERE z.got!=0 AND z.under>0;
ConfexianMJS

11

Query veloce e sporca che dovrebbe fare il trucco:

SELECT a AS id, b AS next_id, (b - a) -1 AS missing_inbetween
FROM 
 (
SELECT a1.id AS a , MIN(a2.id) AS b 
FROM arrc_vouchers  AS a1
LEFT JOIN arrc_vouchers AS a2 ON a2.id > a1.id
WHERE a1.id <= 100
GROUP BY a1.id
) AS tab

WHERE 
b > a + 1

Questo ti darà una tabella che mostra l'id che ha ID mancanti sopra di esso, e next_id che esiste e quanti mancano tra ... ad es.

 
id next_id missing_inbetween
 1 4 2
68 70 1
75 87 11

1
Questo ha funzionato benissimo per me. Grazie.! Sono stato in grado di modificarlo facilmente per i miei scopi.
Rahim Khoja

Sembra che questa sia la risposta migliore quando si cerca "ID successivo" negli spazi vuoti. Purtroppo è ESTREMAMENTE lento per tabelle con 10K di righe. Ho aspettato più di 10 minuti su un tavolo da ~ 46K mentre con @ConfexianMJS ho ottenuto risultati in meno di un secondo!
BringBackCustomore64

5

Se stai usando un, MariaDBhai un'opzione più veloce (800%) usando il motore di memorizzazione delle sequenze :

SELECT * FROM seq_1_to_50000 WHERE SEQ NOT IN (SELECT COL FROM TABLE);

2
per espandere questa idea, il massimo della sequenza può essere stabilito utilizzando "SELECT MAX(column) FROM table"e impostando una variabile dal risultato diciamo $ MAX ... l'istruzione sql può quindi essere scritta la "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" mia sintassi è basata su php
me_

oppure puoi usarlo SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;con le variabili MySQL.
Moshe L

2

Crea una tabella temporanea con 100 righe e una singola colonna contenente i valori 1-100.

Esterno Unisci questa tabella alla tua tabella arrc_vouchers e seleziona i valori della singola colonna in cui l'id arrc_vouchers è nullo.

Codificare questo cieco, ma dovrebbe funzionare.

select tempid from temptable 
left join arrc_vouchers on temptable.tempid = arrc_vouchers.id 
where arrc_vouchers.id is null

OK, 1 - 100 era solo un modo semplice per fare un esempio. In questo caso, stiamo guardando 20.000 - 85.000. Quindi creo una tabella temporanea con 65.000 righe numerate da 20000 a 85000? E come posso farlo? Sto usando phpMyAdmin; se imposto il valore predefinito della colonna a 25000 e lo faccio aumentare automaticamente, posso semplicemente inserire 65.000 righe e inizierà l'incremento automatico con 25000?
EmmyS

Ho avuto una situazione simile (ho 100 articoli in ordine e devo trovare articoli mancanti in 100). Per fare ciò, ho creato un'altra tabella 1-100, quindi ho eseguito questa istruzione su di essa e funziona magnificamente. Questo sostituisce una funzione molto complessa per creare tabelle temporanee. Solo un consiglio per qualcuno in una situazione simile, a volte è più veloce creare una tabella rispetto alle tabelle temporanee.
newshorts

2

Una soluzione alternativa che richiede una query + un po 'di codice che esegue alcune elaborazioni sarebbe:

select l.id lValue, c.id cValue, r.id rValue 
  from 
  arrc_vouchers l 
  right join arrc_vouchers c on l.id=IF(c.id > 0, c.id-1, null)
  left  join arrc_vouchers r on r.id=c.id+1
where 1=1
  and c.id > 0 
  and (l.id is null or r.id is null)
order by c.id asc;

Si noti che la query non contiene alcuna sottoselezione che sappiamo che non è gestita in modo efficiente dal pianificatore di MySQL.

Ciò restituirà una voce per centralValue (cValue) che non ha un valore inferiore (lValue) o un valore maggiore (rValue), ovvero:

lValue |cValue|rValue
-------+------+-------
{null} | 2    | 3      
8      | 9    | {null} 
{null} | 22   | 23     
23     | 24   | {null} 
{null} | 29   | {null} 
{null} | 33   | {null} 


Senza entrare in ulteriori dettagli (li vedremo nei prossimi paragrafi) questo output significa che:

  • Nessun valore compreso tra 0 e 2
  • Nessun valore compreso tra 9 e 22
  • Nessun valore compreso tra 24 e 29
  • Nessun valore compreso tra 29 e 33
  • Nessun valore compreso tra 33 e MAX VALUE

Quindi l'idea di base è di fare un join RIGHT e LEFT con la stessa tabella vedendo se abbiamo valori adiacenti per valore (cioè: se il valore centrale è '3', controlliamo 3-1 = 2 a sinistra e 3 + 1 a destra), e quando una RIGA ha un valore NULL a DESTRA o SINISTRA, allora sappiamo che non c'è alcun valore adiacente.

L'output grezzo completo della mia tabella è:

select * from arrc_vouchers order by id asc;

0  
2  
3  
4  
5  
6  
7  
8  
9  
22 
23 
24 
29 
33 

Alcune note:

  1. L'istruzione SQL IF nella condizione di join è necessaria se si definisce il campo "id" come UNSIGNED, quindi non consentirà di diminuirlo sotto zero. Questo non è strettamente necessario se mantieni il valore c> 0 come indicato nella nota successiva, ma lo includo proprio come doc.
  2. Sto filtrando il valore centrale zero poiché non siamo interessati a nessun valore precedente e possiamo derivare il valore del post dalla riga successiva.

2

Se è presente una sequenza con uno spazio massimo di uno tra due numeri (come 1,3,5,6), la query che può essere utilizzata è:

select s.id+1 from source1 s where s.id+1 not in(select id from source1) and s.id+1<(select max(id) from source1);
  • table_name - source1
  • nome_colonna - id

1

in base alla risposta data sopra da Lucek, questa procedura memorizzata consente di specificare i nomi di tabelle e colonne che si desidera testare per trovare record non contigui, rispondendo così alla domanda originale e dimostrando anche come si potrebbe usare @var per rappresentare tabelle e / o colonne in una stored procedure.

create definer=`root`@`localhost` procedure `spfindnoncontiguous`(in `param_tbl` varchar(64), in `param_col` varchar(64))
language sql
not deterministic
contains sql
sql security definer
comment ''
begin
declare strsql varchar(1000);
declare tbl varchar(64);
declare col varchar(64);

set @tbl=cast(param_tbl as char character set utf8);
set @col=cast(param_col as char character set utf8);

set @strsql=concat("select 
    ( t1.",@col," + 1 ) as starts_at, 
  ( select min(t3.",@col,") -1 from ",@tbl," t3 where t3.",@col," > t1.",@col," ) as ends_at
    from ",@tbl," t1
        where not exists ( select t2.",@col," from ",@tbl," t2 where t2.",@col," = t1.",@col," + 1 )
        having ends_at is not null");

prepare stmt from @strsql;
execute stmt;
deallocate prepare stmt;
end

1

L'ho provato in modi diversi e la migliore prestazione che ho trovato è stata questa semplice query:

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;

... un join sinistro per verificare se esiste l' id successivo , solo se next se non viene trovato, la sottoquery trova l'id successivo esistente per trovare la fine del gap. L'ho fatto perché la query con uguale (=) offre prestazioni migliori rispetto all'operatore maggiore di (>).

Usando sqlfiddle non mostra prestazioni così diverse di altre query ma in un database reale questa query sopra risulta 3 volte più veloce di altre.

Lo schema:

CREATE TABLE arrc_vouchers (id int primary key)
;
INSERT INTO `arrc_vouchers` (`id`) VALUES (1),(4),(5),(7),(8),(9),(10),(11),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)
;

Segui di seguito tutte le query che ho fatto per confrontare le prestazioni:

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;
select *, (gapEnd-gapIni) qt
    from (
        select id+1 gapIni
        ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
        from arrc_vouchers a
        order by id
    ) a where gapEnd <> gapIni
;
select id+1 gapIni
    ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
    #,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    where id+1 <> (select x.id from arrc_vouchers x where x.id>a.id limit 1)
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),concat('*** GAT *** ',(select x.id from arrc_vouchers x where x.id>a.id limit 1))) gapEnd
    from arrc_vouchers a
    order by id
;

Forse aiuta qualcuno e utile.

Puoi vedere e testare la mia query usando questo sqlfiddle :

http://sqlfiddle.com/#!9/6bdca7/1


0

Sebbene tutto ciò sembri funzionare, il set di risultati ritorna in un tempo molto lungo quando ci sono 50.000 record.

L'ho usato e trova il divario o il successivo disponibile (ultimo usato + 1) con un ritorno molto più veloce dalla query.

SELECT a.id as beforegap, a.id+1 as avail
FROM table_name a
where (select b.id from table_name b where b.id=a.id+1) is null
limit 1;

questo trova il primo vuoto che non è quello che la domanda stava chiedendo.
pareggiò il

0

Probabilmente non è rilevante, ma stavo cercando qualcosa del genere per elencare le lacune in una sequenza di numeri e ho trovato questo post, che ha più soluzioni diverse a seconda di ciò che stai cercando. Stavo cercando il primo spazio disponibile nella sequenza (cioè il prossimo numero disponibile), e questo sembra funzionare bene.

SELEZIONA MIN (l.number_sequence + 1) come nextavabile dai pazienti come l LEFT OUTER JOIN pazienti come r su l.number_sequence + 1 = r.number_sequence WHERE r.number_sequence is NULL. Diversi altri scenari e soluzioni discussi lì, dal 2005!

Come trovare i valori mancanti in una sequenza con SQL

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.