Wie finde ich Lücken in der fortlaufenden Nummerierung in MySQL?


119

Wir haben eine Datenbank mit einer Tabelle, deren Werte aus einem anderen System importiert wurden. Es gibt eine automatische Inkrementierungsspalte und es gibt keine doppelten Werte, aber es fehlen Werte. Beispiel: Ausführen dieser Abfrage:

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

sollte 100 zurückgeben, aber stattdessen 87. Kann ich eine Abfrage ausführen, die die Werte der fehlenden Zahlen zurückgibt? Beispielsweise können die Datensätze für die ID 1-70 und 83-100 vorhanden sein, es gibt jedoch keine Datensätze mit den IDs 71-82. Ich möchte 71, 72, 73 usw. zurückgeben.

Ist das möglich?


Dies funktioniert möglicherweise nicht in MySQL, aber bei der Arbeit (Oracle) brauchten wir etwas Ähnliches. Wir haben einen gespeicherten Prozess geschrieben, der eine Zahl als Maximalwert verwendet. Der gespeicherte Prozess erstellte dann eine temporäre Tabelle mit einer einzelnen Spalte. Die Tabelle enthielt alle Zahlen von 1 bis max. Dann wurde eine NOT IN-Verknüpfung zwischen der temporären Tabelle und unserer Tabelle von Interesse durchgeführt. Wenn Sie es mit Max = Select max (id) aus arrc_vouchers aufrufen, werden alle fehlenden Werte zurückgegeben.
Saunderl

2
Was ist falsch daran, Lücken in der Nummerierung zu haben? Der Wert eines Ersatzschlüssels ist im Allgemeinen nicht aussagekräftig. Alles was zählt ist, dass es einzigartig ist. Wenn Ihre Anwendung nicht zusammenhängende IDs nicht verarbeiten kann, ist dies wahrscheinlich ein Fehler in der Anwendung und nicht in den Daten.
Wyzard

4
In diesem Fall handelt es sich um ein Problem, da die Daten, die wir vom alten System geerbt haben, die einem Datensatz zugeordnete automatische Inkrementierungsnummer als Schlüssel zum Drucken auf einer physischen Karte verwendeten, die an Personen ausgegeben wird. Das war NICHT unsere Idee. Um herauszufinden, welche Karten fehlen, müssen wir wissen, wo die Lücken in der fortlaufenden Nummerierung sind.
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;

Sie können "Serie generieren" verwenden, um Zahlen von 1 bis zur höchsten ID Ihrer Tabelle zu generieren. Führen Sie dann eine Abfrage aus, bei der die ID nicht in dieser Reihe enthalten ist.
Tsvetelin Salutski

Antworten:


170

Aktualisieren

ConfexianMJS lieferte eine viel bessere Antwort in Bezug auf die Leistung.

Die (nicht so schnelle wie möglich) Antwort

Hier ist die Version, die für Tabellen beliebiger Größe geeignet ist (nicht nur für 100 Zeilen):

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 - erste ID in der aktuellen Lücke
  • gap_ends_at - letzte ID in der aktuellen Lücke

6
Ich arbeite nicht einmal mehr für dieses Unternehmen, aber dies ist die beste Antwort, die ich je gesehen habe, und es lohnt sich auf jeden Fall, sich daran zu erinnern, um später darauf zurückgreifen zu können. Vielen Dank!
EmmyS

4
Das einzige Problem dabei ist, dass es keine mögliche anfängliche Lücke "meldet". zB wenn die ersten 5 IDs fehlen (1 bis 5), zeigt das nicht ... Wie können wir am Anfang pissible Lücken zeigen?
DiegoDD

Hinweis: Diese Abfrage funktioniert nicht für temporäre Tabellen. Mein Problem war, dass die order numberSuche nach Lücken nicht eindeutig ist (in der Tabelle werden die Bestellpositionen gespeichert, sodass sich die Bestellnummer, zu der sie gehören, für jede Zeile wiederholt). 1. Abfrage: 2812 Zeilen im Satz (1 Min. 31,09 Sek.) . Erstellen Sie eine weitere Tabelle, indem Sie unterschiedliche Bestellnummern auswählen. Ihre Anfrage ohne meine Wiederholungen: 1009 Zeilen im Satz (18.04 Sek.)
Chris K

1
@DiegoDD Was ist los mit SELECT MIN(id) FROM table?
Air

8
Arbeitete, aber es dauerte ungefähr 5 Stunden, um auf einem Tisch mit 700000 Datensätzen zu laufen
Matt

98

Dies hat nur funktioniert, um die Lücken in einer Tabelle mit mehr als 80.000 Zeilen zu finden:

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;

Ergebnis:

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

Beachten Sie, dass die Reihenfolge der Spalten expectedund gotkritisch ist.

Wenn Sie wissen, dass YourColdies nicht bei 1 beginnt und das keine Rolle spielt, können Sie es ersetzen

(SELECT @rownum:=0) AS a

mit

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

Neues Ergebnis:

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

Wenn Sie eine Art Shell-Skriptaufgabe für die fehlenden IDs ausführen müssen, können Sie diese Variante auch verwenden, um direkt einen Ausdruck zu erstellen, über den Sie in bash iterieren können.

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;

Dies erzeugt eine Ausgabe wie diese

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

Sie können es dann kopieren und in eine for-Schleife in einem Bash-Terminal einfügen, um einen Befehl für jede ID auszuführen

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

Es ist dasselbe wie oben, nur dass es sowohl lesbar als auch ausführbar ist. Durch Ändern des obigen Befehls "CONCAT" kann die Syntax für andere Programmiersprachen generiert werden. Oder vielleicht sogar SQL.


8
schöne Lösung, für mich ist es besser als die bevorzugte Antwort - danke
Wee Zel

6
Es ist viel effizienter als die akzeptierte Antwort.
Symcbean

1
viel schneller als die akzeptierte Antwort. Das einzige, was ich hinzufügen möchte, ist, dass CONVERT( YourCol, UNSIGNED )es bessere Ergebnisse liefert, wenn YourCol noch keine Ganzzahl ist.
Barton Chittenden

1
@AlexandreCassagne: Wenn ich Ihre Frage richtig verstehe, würde ich einfach eine separate Abfrage wie die eingebettete durchführen, um die min zu finden:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

1
@temuri Wechseln Sie bei Bedarf zur Variante GROUP_CONCAT: 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

Schnelle und schmutzige Abfrage, die den Trick machen sollte:

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

Auf diese Weise erhalten Sie eine Tabelle mit der ID, über der IDs fehlen, und der vorhandenen next_id sowie der Anzahl der zwischen ... z

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

1
Das hat bei mir super funktioniert. Vielen Dank.! Ich konnte dies leicht für meine Zwecke ändern.
Rahim Khoja

Es scheint, dass dies die beste Antwort ist, wenn Sie nach "nächster ID" in Lücken suchen. Leider ist es für Tabellen mit 10 KB Zeilen EXTREM langsam. Ich habe mehr als 10 Minuten auf einem ~ 46K-Tisch gewartet, während ich mit @ConfexianMJS in weniger als einer Sekunde Ergebnisse erzielt habe!
BringBackCommodore64

5

Wenn Sie eine verwenden MariaDB, haben Sie eine schnellere (800%) Option mit der Sequenzspeicher-Engine :

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

2
Um diese Idee zu erweitern, kann "SELECT MAX(column) FROM table"das Maximum der Sequenz mithilfe einer Variablen aus dem Ergebnis festgelegt und festgelegt werden, z. B. $ MAX ... die SQL-Anweisung kann dann geschrieben werden. "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" Meine Syntax basiert auf PHP
me_

oder Sie können SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;mit MySQL-Variablen verwenden.
Moshe L

2

Erstellen Sie eine temporäre Tabelle mit 100 Zeilen und einer einzelnen Spalte mit den Werten 1-100.

Außen Verbinden Sie diese Tabelle mit Ihrer arrc_vouchers-Tabelle und wählen Sie die einzelnen Spaltenwerte aus, bei denen die arrc_vouchers-ID null ist.

Codierung dieses Blind, sollte aber funktionieren.

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

OK, 1 - 100 war nur eine einfache Möglichkeit, ein Beispiel zu geben. In diesem Fall betrachten wir 20.000 - 85.000. Also erstelle ich eine temporäre Tabelle mit 65.000 Zeilen mit den Nummern 20000 - 85000? Und wie mache ich das? Ich benutze phpMyAdmin; Wenn ich den Standardwert der Spalte auf 25000 setze und sie automatisch inkrementiere, kann ich dann einfach 65.000 Zeilen einfügen und die automatische Inkrementierung wird mit 25000 gestartet?
EmmyS

Ich hatte eine ähnliche Situation (ich habe 100 Artikel in der Reihenfolge und muss fehlende Artikel in 100 finden). Zu diesem Zweck habe ich eine weitere Tabelle 1-100 erstellt, diese Anweisung dann ausgeführt und sie funktioniert hervorragend. Dies ersetzt eine sehr komplexe Funktion zum Erstellen temporärer Tabellen. Nur Ratschläge für jemanden in einer ähnlichen Situation. Manchmal ist es schneller, eine Tabelle zu erstellen als temporäre Tabellen.
Newshorts

2

Eine alternative Lösung, die eine Abfrage + einen Code erfordert, der eine Verarbeitung ausführt, wäre:

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;

Beachten Sie, dass die Abfrage keine Unterauswahl enthält, von der wir wissen, dass sie vom MySQL-Planer nicht performant verarbeitet wird.

Dies gibt einen Eintrag pro centralValue (cValue) zurück, der keinen kleineren Wert (lValue) oder größeren Wert (rValue) hat, dh:

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


Ohne auf weitere Details einzugehen (wir werden sie in den nächsten Absätzen sehen), bedeutet diese Ausgabe Folgendes:

  • Keine Werte zwischen 0 und 2
  • Keine Werte zwischen 9 und 22
  • Keine Werte zwischen 24 und 29
  • Keine Werte zwischen 29 und 33
  • Keine Werte zwischen 33 und MAX VALUE

Die Grundidee ist also, eine RECHTS- und eine LINKS-Verknüpfung mit derselben Tabelle durchzuführen, um festzustellen, ob wir benachbarte Werte pro Wert haben (dh wenn der zentrale Wert '3' ist, prüfen wir links auf 3-1 = 2 und links auf 3 + 1 rechts), und wenn eine REIHE einen NULL-Wert bei RECHTS oder LINKS hat, wissen wir, dass es keinen benachbarten Wert gibt.

Die vollständige Rohausgabe meiner Tabelle lautet:

select * from arrc_vouchers order by id asc;

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

Einige Notizen:

  1. Die SQL IF-Anweisung in der Join-Bedingung wird benötigt, wenn Sie das Feld 'id' als UNSIGNED definieren. Daher können Sie es nicht unter Null verringern. Dies ist nicht unbedingt erforderlich, wenn Sie den c.-Wert> 0 beibehalten, wie in der nächsten Anmerkung angegeben, aber ich füge ihn nur als Dokument hinzu.
  2. Ich filtere den zentralen Wert Null, da wir an keinem vorherigen Wert interessiert sind und den Post-Wert aus der nächsten Zeile ableiten können.

2

Wenn es eine Sequenz mit einer Lücke von maximal eins zwischen zwei Zahlen gibt (wie 1,3,5,6), kann folgende Abfrage verwendet werden:

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);
  • Tabellenname - source1
  • Spaltenname - id

1

Basierend auf der oben von Lucek gegebenen Antwort können Sie mit dieser gespeicherten Prozedur die Tabellen- und Spaltennamen angeben, die Sie testen möchten, um nicht zusammenhängende Datensätze zu finden. Auf diese Weise können Sie die ursprüngliche Frage beantworten und auch demonstrieren, wie Sie @var zur Darstellung von Tabellen & verwenden können. / oder Spalten in einer gespeicherten Prozedur.

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

Ich habe es auf verschiedene Arten versucht und die beste Leistung, die ich gefunden habe, war diese einfache Abfrage:

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
;

... eine linke Verknüpfung, um zu überprüfen, ob die nächste ID vorhanden ist, nur wenn die nächste nicht gefunden wird, findet die Unterabfrage die nächste vorhandene ID, um das Ende der Lücke zu finden. Ich habe es getan, weil eine Abfrage mit gleichem (=) eine bessere Leistung hat als ein Operator mit mehr als (>).

Bei Verwendung der SQL-Geige wird die Leistung anderer Abfragen nicht so unterschiedlich angezeigt , aber in einer realen Datenbank führt diese obige Abfrage dreimal schneller als andere.

Das 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)
;

Befolgen Sie die folgenden Abfragen, um die Leistung zu vergleichen:

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
;

Vielleicht hilft es jemandem und nützlich.

Sie können meine Abfrage mit dieser SQL-Geige sehen und testen :

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


0

Obwohl diese alle zu funktionieren scheinen, kehrt die Ergebnismenge bei 50.000 Datensätzen in sehr langer Zeit zurück.

Ich habe dies verwendet und es wird die Lücke oder die nächste verfügbare (zuletzt verwendete + 1) mit einer viel schnelleren Rückkehr von der Abfrage gefunden.

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;

Dies findet die erste Lücke, nach der die Frage nicht gefragt hat.
Zeichnen

0

Wahrscheinlich nicht relevant, aber ich habe nach so etwas gesucht, um die Lücken in einer Folge von Zahlen aufzulisten, und diesen Beitrag gefunden, der mehrere unterschiedliche Lösungen bietet, je nachdem, was genau Sie suchen. Ich habe nach der ersten verfügbaren Lücke in der Sequenz gesucht (dh nach der nächsten verfügbaren Nummer), und dies scheint gut zu funktionieren.

SELECT MIN (l.number_sequence + 1) als nextavabile von Patienten als l LEFT OUTER JOIN Patienten als r on l.number_sequence + 1 = r.number_sequence WHERE r.number_sequence ist NULL. Mehrere andere dort diskutierte Szenarien und Lösungen ab 2005!

So finden Sie fehlende Werte in einer Sequenz mit SQL

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.