Jak znaleźć luki w numeracji sekwencyjnej w mysql?


119

Mamy bazę danych z tabelą, której wartości zostały zaimportowane z innego systemu. Istnieje kolumna z automatycznym zwiększaniem wartości i nie ma zduplikowanych wartości, ale brakuje wartości. Na przykład, uruchamiając to zapytanie:

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

powinien zwrócić 100, ale zamiast tego zwraca 87. Czy jest jakieś zapytanie, które mogę uruchomić, które zwróci wartości brakujących liczb? Na przykład rekordy mogą istnieć dla identyfikatorów 1-70 i 83-100, ale nie ma rekordów o identyfikatorach 71-82. Chcę zwrócić 71, 72, 73 itd.

czy to możliwe?


To może nie działać w MySQL, ale w pracy (Oracle) potrzebowaliśmy czegoś podobnego. Napisaliśmy zapisany proces, który jako wartość maksymalną przyjmował liczbę. Następnie Stored Proc utworzył tabelę tymczasową z jedną kolumną. Tabela zawierała wszystkie liczby od 1 do Max. Następnie wykonało połączenie NOT IN między tabelą tymczasową a naszym interesującym stołem. Jeśli wywołałeś go z Max = Select max (id) z arrc_vouchers, zwróciłby wszystkie brakujące wartości.
saunderl

2
Co jest złego w lukach w numeracji? Wartość klucza zastępczego na ogół nie ma znaczenia; liczy się tylko to, że jest wyjątkowy. Jeśli Twoja aplikacja nie obsługuje nieciągłych identyfikatorów, prawdopodobnie jest to błąd w aplikacji, a nie w danych.
Wyzard

4
W tym przypadku jest to problem, ponieważ dane, które odziedziczyliśmy ze starego systemu, wykorzystywały numer automatycznego inkrementacji powiązany z rekordem jako klucz do drukowania na fizycznej karcie, która jest rozdawana ludziom. To nie był nasz pomysł. Aby dowiedzieć się, których kart brakuje, musimy wiedzieć, gdzie występują luki w numeracji sekwencyjnej.
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;

Możesz użyć generowania serii do generowania liczb od 1 do najwyższego identyfikatora Twojej tabeli. Następnie uruchom zapytanie, w którym id nie znajduje się w tej serii.
Tsvetelin Salutski

Odpowiedzi:


170

Aktualizacja

ConfexianMJS dostarczył znacznie lepszej odpowiedzi pod względem wydajności.

Odpowiedź (nie tak szybka, jak to możliwe)

Oto wersja, która działa na stole o dowolnym rozmiarze (nie tylko na 100 wierszach):

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 - pierwszy identyfikator w bieżącej luce
  • gap_ends_at - ostatni identyfikator w bieżącej luce

6
Już nawet nie pracuję dla tej firmy, ale to najlepsza odpowiedź, jaką widziałem i zdecydowanie warto o niej pamiętać, aby móc z niej skorzystać w przyszłości. Dzięki!
EmmyS

4
jedyny problem polega na tym, że nie „zgłasza” możliwej początkowej luki. np. jeśli brakuje pierwszych 5 identyfikatorów (od 1 do 5), nie oznacza to… Jak możemy pokazać dopuszczalne luki na samym początku?
DiegoDD

Uwaga: to zapytanie nie działa w przypadku tabel tymczasowych. Mój problem polegał na tym, order numberże szukałem luk w nie jest odrębny (tabela przechowuje wiersze zamówienia, więc numer zamówienia, do którego należą, powtarza się dla każdego wiersza). Pierwsze zapytanie: 2812 wierszy w zestawie (1 min 31,09 s) . Zrobiłem kolejną tabelę, wybierając różne numery zamówień. Twoje zapytanie bez moich powtórzeń: zestaw 1009 wierszy (18,04 s)
Chris K,

1
@DiegoDD Co jest nie tak z SELECT MIN(id) FROM table?
Air

8
Pracowałem, ale uruchomienie na stole z 700000 rekordami zajęło około 5 godzin
Matt

98

Pomogło mi to znaleźć luki w tabeli z ponad 80 tys. Wierszy:

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;

Wynik:

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

Zauważ, że kolejność kolumn expectedi gotjest krytyczna.

Jeśli wiesz, że YourColnie zaczyna się od 1 i to nie ma znaczenia, możesz wymienić

(SELECT @rownum:=0) AS a

z

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

Nowy wynik:

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

Jeśli potrzebujesz wykonać jakieś zadanie skryptu powłoki na brakujących identyfikatorach, możesz również użyć tego wariantu, aby bezpośrednio utworzyć wyrażenie, które możesz iterować w 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;

Daje to taki wynik

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

Następnie możesz skopiować i wkleić go do pętli for w terminalu bash, aby wykonać polecenie dla każdego identyfikatora

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

Jest to to samo, co powyżej, tyle że jest zarówno czytelne, jak i wykonywalne. Zmieniając powyższe polecenie „CONCAT”, można wygenerować składnię dla innych języków programowania. A może nawet SQL.


8
fajne rozwiązanie, jak dla mnie lepsze niż preferowana odpowiedź - dzięki
Wee Zel

6
Jest to znacznie bardziej wydajne niż zaakceptowana odpowiedź.
symcbean

1
znacznie szybciej niż zaakceptowana odpowiedź. Jedyne, co chciałbym dodać, to to, że CONVERT( YourCol, UNSIGNED )da lepsze wyniki, jeśli YourCol nie jest już liczbą całkowitą.
Barton Chittenden,

1
@AlexandreCassagne: Jeśli dobrze rozumiem twoje pytanie, po prostu wykonałbym osobne zapytanie, takie jak osadzone, aby znaleźć min:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

1
@temuri W razie potrzeby przełącz na wariant 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

Szybkie i brudne zapytanie, które powinno załatwić sprawę:

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

Spowoduje to wyświetlenie tabeli pokazującej identyfikator, który ma brakujące identyfikatory nad nim, oraz następny_id, który istnieje i ile brakuje między ... np.

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

1
To działało świetnie dla mnie. Dzięki.! Udało mi się to łatwo zmodyfikować do moich celów.
Rahim Khoja

Wydaje się, że to najlepsza odpowiedź, jeśli szukasz „następnego identyfikatora” w lukach. Niestety jest BARDZO wolny dla tabel z 10 tys. Wierszy. Czekałem ponad 10 minut na stole ~ 46K, podczas gdy z @ConfexianMJS otrzymałem wyniki w mniej niż sekundę!
BringBackCommodore64

5

Jeśli korzystasz MariaDBz szybszej (800%) opcji korzystającej z mechanizmu przechowywania sekwencji :

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

2
aby rozwinąć ten pomysł, maksimum sekwencji można ustalić używając "SELECT MAX(column) FROM table"i ustawiając zmienną z wyniku, powiedzmy $ MAX ... następnie można zapisać instrukcję sql "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" moja składnia jest oparta na php
me_

lub możesz użyć SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;ze zmiennymi MySQL.
Moshe L

2

Utwórz tymczasową tabelę ze 100 wierszami i jedną kolumną zawierającą wartości 1-100.

Outer Dołącz tę tabelę do swojej tabeli arrc_vouchers i wybierz wartości w jednej kolumnie, w których identyfikator arrc_vouchers ma wartość null.

Kodowanie tego ślepego, ale powinno działać.

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

OK, 1 - 100 to prosty sposób na podanie przykładu. W tym przypadku mamy do czynienia z 20 000 - 85 000. Czy mogę więc utworzyć tabelę tymczasową z 65 000 wierszami o numerach od 20000 do 85000? Jak mam to zrobić? Używam phpMyAdmin; jeśli ustawię domyślną wartość kolumny na 25000 i ustawię ją na automatyczną inkrementację, czy mogę po prostu wstawić 65 000 wierszy i rozpocznie automatyczną inkrementację od 25000?
EmmyS

Miałem podobną sytuację (mam w porządku 100 pozycji i muszę znaleźć brakujące pozycje w 100). Aby to zrobić, utworzyłem kolejną tabelę 1-100, a następnie wykonuję na niej tę instrukcję i działa pięknie. Zastępuje to bardzo złożoną funkcję tworzenia tabel tymczasowych. Tylko rada dla kogoś w podobnej sytuacji, czasami tworzenie tabeli jest szybsze niż tabele tymczasowe.
newshorts

2

Alternatywnym rozwiązaniem, które wymaga zapytania + trochę kodu wykonującego pewne przetwarzanie, byłoby:

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;

Zauważ, że zapytanie nie zawiera żadnej podselekcji, o której wiemy, że nie jest obsługiwana wydajnie przez planner MySQL.

To zwróci jeden wpis na centralValue (cValue), który nie ma mniejszej wartości (lValue) ani większej wartości (rValue), tj .:

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


Bez wchodzenia w dalsze szczegóły (zobaczymy je w następnych akapitach) ten wynik oznacza, że:

  • Brak wartości od 0 do 2
  • Brak wartości między 9 a 22
  • Brak wartości między 24 a 29
  • Brak wartości między 29 a 33
  • Brak wartości między 33 a MAX VALUE

Więc podstawową ideą jest wykonanie łączenia w PRAWO i LEWO z tą samą tabelą, sprawdzając, czy mamy wartości sąsiadujące na wartość (tj .: jeśli wartość środkowa to '3', to sprawdzamy 3-1 = 2 po lewej i 3 + 1 po po prawej), a kiedy ROW ma wartość NULL po prawej lub lewej stronie, wtedy wiemy, że nie ma przyległej wartości.

Kompletne surowe dane wyjściowe mojej tabeli to:

select * from arrc_vouchers order by id asc;

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

Kilka uwag:

  1. Instrukcja SQL IF w warunku łączenia jest potrzebna, jeśli zdefiniujesz pole „id” jako UNSIGNED, dlatego nie pozwoli ci to zmniejszyć poniżej zera. Nie jest to absolutnie konieczne, jeśli utrzymujesz c.value> 0, jak określono w następnej notatce, ale dołączam ją tak, jak doc.
  2. Filtruję zerową wartość centralną, ponieważ nie interesuje nas żadna poprzednia wartość i możemy wyprowadzić wartość postu z następnego wiersza.

2

Jeśli istnieje ciąg z maksymalnie jedną przerwą między dwiema liczbami (np. 1,3,5,6), wówczas zapytanie, które można zastosować, to:

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);
  • Nazwa tabeli - source1
  • Nazwa kolumny - id

1

na podstawie odpowiedzi udzielonej powyżej przez Lucka ta procedura składowana pozwala określić nazwy tabel i kolumn, które chcesz przetestować, aby znaleźć nieciągłe rekordy - w ten sposób odpowiadając na oryginalne pytanie, a także demonstrując, jak można użyć @var do reprezentowania tabel & / lub kolumny w procedurze składowanej.

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

I próbował go w różny sposób i najlepszej wydajności, które znalazłem było to proste zapytanie:

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
;

... jedno lewe sprzężenie, aby sprawdzić, czy następny identyfikator istnieje, tylko jeśli następne, jeśli nie zostanie znalezione, wówczas podzapytanie znajduje następny identyfikator, który istnieje, aby znaleźć koniec przerwy. Zrobiłem to, ponieważ zapytanie z równością (=) ma lepszą wydajność niż operator większy niż (>).

Korzystanie z sqlfiddle nie pokazuje tak różnej wydajności innych zapytań, ale w prawdziwej bazie danych powyższe zapytanie daje 3 razy szybszy wynik niż inne.

Schemat:

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

Wykonaj poniżej wszystkie zapytania, które wykonałem, aby porównać wydajność:

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
;

Może to komuś pomoże i przydatne.

Możesz zobaczyć i przetestować moje zapytanie za pomocą tego sqlfiddle :

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


0

Chociaż wszystko wydaje się działać, zestaw wyników powraca po bardzo długim czasie, gdy jest 50 000 rekordów.

Skorzystałem z tego i znalazłem lukę lub następną dostępną (ostatnio używaną + 1) z dużo szybszym powrotem z zapytania.

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;

znajduje to pierwszą lukę, o którą nie chodziło w pytaniu.
drewish

0

Prawdopodobnie nieistotne, ale szukałem czegoś takiego, aby wymienić luki w sekwencji liczb i znalazłem ten post, który ma wiele różnych rozwiązań w zależności od dokładnie tego, czego szukasz. Szukałem pierwszej dostępnej przerwy w sekwencji (tj. Następnej dostępnej liczby) i wydaje się, że działa dobrze.

SELECT MIN (l.number_sequence + 1) jako nextavabile od pacjentów jako l LEFT OUTER JOIN pacjentów jako r na l.number_sequence + 1 = r.number_sequence WHERE r.number_sequence wynosi NULL. Kilka innych scenariuszy i omówionych tam rozwiązań, od 2005 roku!

Jak znaleźć brakujące wartości w sekwencji za pomocą SQL

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.