Kawałek Kodu

Jeśli zmierzyłeś się kiedyś z duplikatami rekordów w bazie MySQL, to zapewne doświadczyłeś również zjawiska przeciwnego - ich braku. W tym wpisie zajmiemy się sposobami znalezienia dziur w... nie, nie, nie tam... w autonumeracji.

Jak podążać króliczą norą?

Zaczniemy od najprostszego przykładu. Pozwoli on znaleźć miejca, w których numeracja jest nieciągła, ale nie wskaże wszystkich numerów, które przynależą do tej króliczej nory (jeśli jest ich więcej niż 1).

Za bazowe dane niech posłuży prosta lista rekordów:

id imię
1 Janek
2 Marek
3 Stefan
6 Waldek
8 Mirek

Z prostej analizy tabeli widać, że przerwy zaczynają się po numerach 3 oraz 6, czyli dokładnie w miejscu o wartości 4 oraz 7. Czyli... czyli jeśli dziura jest, to jest w miejscu o numerze X+1. To już może nasunąć Ci jakiś pomysł na rozwiązanie - sprawdzanie czy w tabeli występuje wartość o jeden większa od wartości aktualnie przeszukiwanego rekordu.
Wykorzystamy tu sprzęgnięcie tabeli z samej sobą, ale warunkiem nie będzie równość wartości id z obydwu tabel, ale równość oparta na wyrażeniu X=Y+1.

SELECT t1.id
FROM tabela AS t1
LEFT JOIN tabela AS t2 ON t2.id=t1.id+1

Złączenie LEFT JOIN pozwala nam wybrać wszystkie rekordy z tabeli t1 oraz wszystkie rekordy z tabeli t2, a jeśli brak jest ich odpowiedników w tabeli t1, to otrzymamy tu wartość NULL.
Dla powyższego zapytania zostaną wybrane 4 rekordy zgodnie z poniższą tabelą:

t1.id t2.id t2.id=t1.id+1
1 1  
1 2 true
1 3  
1 6  
1 8  
2 1  
2 2  
2 3 true
2 6  
2 8  
3 1  
3 2  
3 3 brak 4 w t2 (NULL)
3 6  
3 8  
6 1  
6 2  
6 3  
6 6 brak 7 w t2 (NULL)
6 8  
8 1  
8 2  
8 3  
8 6  
8 8 brak 9 w t2 (NULL)

Ponieważ nie interesują nas "niedziury" w tabeli, zmieniamy zapytanie na:

SELECT t1.id
FROM tabela AS t1
LEFT JOIN tabela AS t2 ON t2.id=t1.id+1
WHERE t2.id IS NULL

To ograniczy nam wyniki z powyższej tabeli do wartości 3, 6 i 8. I właśnie po tych miejscach zaczynają się nasze przerwy. No... może oprócz wartości 8, bo ta jest po prostu ostatnią wartością w tabeli. Aby i jej się pozbyć, rozbudowujemy zapytanie do postaci:

SELECT t1.id
FROM tabela AS t1
LEFT JOIN tabela AS t2 ON t2.id=t1.id+1
WHERE t2.id IS NULL AND t1.id<>(SELECT MAX(id) FROM tabela)

Jeśli chcesz wiedzieć jakie wartości tworzą początek dziury (a nie po jakich zaczyna się dziura), wystarczy zmienić pierwszą linijkę na:

SELECT t1.id+1
...

3.2.1. Bum!

W ten sposób otrzymamy wartości 4 oraz 7.

Czy można znaleźć całą zawartość dziury?

Można. Co prawda nie zrobimy tego sprzęgając tabelę z samą sobą, bo musielibyśmy utworzyć zapytanie UNION łącząc przykładowe zapytanie z warunkiem +1, +2, +3, itd. (aby znaleźć elementy z początku dziury i kolejne do niej należące). A ponieważ nie wiemy jak szeroka jest dziura, to nie wiemy również ile trzeba takich zmodyfikowanych zapytań dodać do tego wielkiego UNION.

Trzeba źródłową tabelę sprzegnąć z tabelą, która posiada ciągłość numeracji. A skąd wziąć taką tabelę? No, jak to skąd, stąd!

Nasze zapytanie wyglądać będzie następująco:

SELECT tmp.id FROM (
 SELECT a.a+b.a*10+1 AS id FROM 
 (SELECT 0 AS a UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) AS a
   CROSS JOIN
 (SELECT 0 AS a UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) AS b
) AS tmp
LEFT JOIN tabela AS t1 ON t1.id=tmp.id
WHERE t1.id IS NULL and tmp.id<=(SELECT MAX(id) FROM tabela)

Dla uproszczenia pokażmy je tak:

SELECT tmp.id FROM (
  zapytanie_generujace liczby od 1 do 101
 ) AS tmp
LEFT JOIN tabela AS t1 ON t1.id=tmp.id
WHERE t1.id IS NULL and tmp.id<=(SELECT MAX(id) FROM tabela)

Sprzęgamy poprzez LEFT JOIN nasze zapytanie, które "tworzy" rekordy od 1 do 101 (możesz je rozbudować, aby generowało tysiące, dziesiątki tysięcy, itd. - szczegóły w wyżej wspomnianym wpisie) z tabelą źródłową. Jeśli po sprzęgnięciu brak jest wartości z tabeli źródłowej w zakresie wartości 1-101, to otrzymujemy wartość NULL. Aby wychwycić tylko te braki, dodajemy odpowiedni warunek do klauzuli WHERE. Dodatkowo ograniczamy zakres 1-101 do 1-8, czyli tak naprawdę sprzęgamy z tą pulą, a nie pulą ograniczoną z góry wartością 101.

Wynik jaki otrzymamy wygląda następująco:

tmp.id
4
5
7

 

Mam nadzieję, że nie będziesz szukał dziury w całym i jednak wpis określisz jako fajny.

 

Przydatne linki:
Plaga mrówek, czyli jak zmultiplikować rekordy zapytania.