Bagaimana menemukan celah dalam penomoran berurutan di mysql?


120

Kami memiliki database dengan tabel yang nilainya diimpor dari sistem lain. Ada kolom kenaikan otomatis, dan tidak ada nilai duplikat, tetapi ada nilai yang hilang. Misalnya, menjalankan kueri ini:

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

harus mengembalikan 100, tetapi mengembalikan 87 sebagai gantinya. Apakah ada kueri yang dapat saya jalankan yang akan mengembalikan nilai dari angka yang hilang? Misalnya, catatan mungkin ada untuk id 1-70 dan 83-100, tetapi tidak ada catatan dengan id 71-82. Saya ingin mengembalikan 71, 72, 73, dll.

Apakah ini mungkin?


Ini mungkin tidak berfungsi di MySQL, tetapi di tempat kerja (Oracle) kami membutuhkan sesuatu yang serupa. Kami menulis Proc Tersimpan yang mengambil angka sebagai nilai Maks. Proc Tersimpan kemudian membuat tabel temp dengan satu kolom. Tabel berisi semua angka dari 1 hingga Max. Kemudian itu melakukan NOT IN gabungan antara tabel temp dan tabel yang kita minati. Jika Anda memanggilnya dengan Max = Select max (id) dari arrc_vouchers, itu akan mengembalikan semua nilai yang hilang.
saunderl

2
Apa salahnya memiliki celah dalam penomoran? Nilai kunci pengganti umumnya tidak bermakna; yang terpenting adalah itu unik. Jika aplikasi Anda tidak dapat menangani ID yang tidak bersebelahan, itu mungkin bug dalam aplikasi, bukan dalam datanya.
Wyzard

4
Dalam hal ini, ini menjadi masalah karena data yang kami warisi dari sistem lama menggunakan nomor penambahan otomatis yang terkait dengan catatan sebagai kunci untuk mencetak pada kartu fisik yang dibagikan kepada orang-orang. Ini BUKAN ide kami. Untuk mengetahui kartu mana yang hilang, kita perlu mengetahui letak celah dalam penomoran berurutan.
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;

Anda dapat menggunakan generate series untuk menghasilkan angka dari 1 ke id tertinggi dari tabel Anda. Kemudian jalankan kueri di mana id tidak ada dalam seri ini.
Tsvetelin Salutski

Jawaban:


170

Memperbarui

ConfexianMJS memberikan jawaban yang jauh lebih baik dalam hal kinerja.

Jawaban (tidak secepat mungkin)

Berikut versi yang berfungsi pada tabel dengan berbagai ukuran (tidak hanya pada 100 baris):

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 - id pertama di celah saat ini
  • gap_ends_at - id terakhir di celah saat ini

6
Saya bahkan tidak bekerja untuk perusahaan itu lagi, tetapi ini adalah jawaban terbaik yang pernah saya lihat dan pasti perlu diingat untuk referensi di masa mendatang. Terima kasih!
EmmyS

4
satu-satunya masalah dengan ini, adalah bahwa ia tidak "melaporkan" kemungkinan celah awal. misalnya jika 5 id pertama hilang (1 sampai 5) itu tidak menunjukkan bahwa ... Bagaimana kita bisa menunjukkan celah yang mungkin terjadi di awal?
DiegoDD

Catatan: Kueri ini tidak berfungsi pada tabel sementara. Masalah saya adalah order numbersaya sedang mencari celah di tidak berbeda (tabel menyimpan baris pesanan, jadi nomor pesanan mereka berulang untuk setiap baris). Kueri pertama: 2812 baris dalam set (1 menit 31,09 detik) . Membuat tabel lain dengan memilih nomor pesanan yang berbeda. Kueri Anda tanpa pengulangan saya: 1009 baris dalam set (18,04 detik)
Chris K

1
@DiegoDD Apa yang salah SELECT MIN(id) FROM table?
Air

8
Bekerja tetapi membutuhkan waktu sekitar 5 jam untuk dijalankan di atas meja dengan 700.000 rekaman
Matt

98

Ini hanya berhasil bagi saya untuk menemukan celah di tabel dengan lebih dari 80 ribu baris:

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;

Hasil:

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

Perhatikan bahwa urutan kolom expecteddan gotsangat penting.

Jika Anda tahu itu YourColtidak dimulai dari 1 dan itu tidak masalah, Anda dapat menggantinya

(SELECT @rownum:=0) AS a

dengan

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

Hasil baru:

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

Jika Anda perlu melakukan beberapa jenis tugas skrip shell pada ID yang hilang, Anda juga dapat menggunakan varian ini untuk secara langsung menghasilkan ekspresi yang dapat Anda iterasi di 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;

Ini menghasilkan keluaran seperti itu

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

Anda kemudian dapat menyalin dan menempelkannya ke loop for di terminal bash untuk menjalankan perintah untuk setiap ID

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

Ini sama seperti di atas, hanya saja itu dapat dibaca dan dieksekusi. Dengan mengubah perintah "CONCAT" di atas, sintaks dapat dibuat untuk bahasa pemrograman lain. Atau bahkan mungkin SQL.


8
solusi yang bagus, bagi saya itu lebih baik daripada jawaban yang disukai - terima kasih
Wee Zel

6
Ini jauh lebih efisien daripada jawaban yang diterima.
symcbean

1
jauh lebih cepat dari jawaban yang diterima. Satu-satunya hal yang saya tambahkan adalah yang CONVERT( YourCol, UNSIGNED )akan memberikan hasil yang lebih baik jika YourCol belum menjadi integer.
Barton Chittenden

1
@AlexandreCassagne: Jika saya memahami pertanyaan Anda dengan benar, saya hanya akan melakukan kueri terpisah seperti yang tersemat untuk menemukan min:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

1
@temuri Beralih ke varian GROUPONCAT jika diperlukan: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

Kueri Cepat dan Kotor yang seharusnya melakukan trik:

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

Ini akan memberi Anda tabel yang menunjukkan id dengan id yang hilang di atasnya, dan next_id yang ada, dan berapa banyak yang hilang di antara ... mis.

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

1
Ini bekerja dengan baik untuk saya. Terima kasih.! Saya dapat dengan mudah memodifikasi ini untuk tujuan saya.
Rahim Khoja

Sepertinya ini adalah jawaban terbaik saat mencari 'id berikutnya' di celah. Sayangnya, ini SANGAT lambat untuk tabel dengan 10K baris. Saya telah menunggu lebih dari 10 menit di meja ~ 46K sedangkan dengan @ConfexianMJS saya mendapatkan hasil dalam waktu kurang dari satu detik!
BringBackCommodore64

5

Jika Anda menggunakan, MariaDBAnda memiliki opsi yang lebih cepat (800%) menggunakan mesin penyimpanan urutan :

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

2
untuk memperluas ide ini, urutan maksimum dapat ditetapkan menggunakan "SELECT MAX(column) FROM table"dan mengatur variabel dari hasil katakanlah $ MAX ... pernyataan sql kemudian dapat ditulis "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" sintaks saya berbasis php
me_

atau Anda dapat menggunakan SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;variabel MySQL.
Moshe L

2

Buat tabel sementara dengan 100 baris dan satu kolom berisi nilai 1-100.

Luar Gabungkan tabel ini ke tabel arrc_vouchers Anda dan pilih nilai kolom tunggal di mana id arrc_vouchers adalah null.

Coding ini buta, tetapi harus bekerja.

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

Oke, 1 - 100 hanyalah cara mudah untuk memberi contoh. Dalam kasus ini, kami melihat 20.000 - 85.000. Jadi apakah saya membuat tabel temp dengan 65.000 baris bernomor 20000 - 85000? Dan bagaimana cara saya melakukannya? Saya menggunakan phpMyAdmin; jika saya menetapkan nilai default kolom menjadi 25000 dan membuatnya bertambah otomatis, dapatkah saya memasukkan 65.000 baris dan itu akan memulai kenaikan otomatis dengan 25000?
EmmyS

Saya mengalami situasi serupa (saya memiliki 100 item dalam urutan dan perlu menemukan item yang hilang dalam 100). Untuk melakukan ini, saya membuat tabel lain 1-100, lalu menjalankan pernyataan ini di atasnya dan berfungsi dengan baik. Ini menggantikan fungsi yang sangat kompleks untuk membuat tabel sementara. Hanya saran untuk seseorang dalam situasi serupa, terkadang lebih cepat membuat tabel daripada tabel temp.
newshorts

2

Solusi alternatif yang memerlukan kueri + beberapa kode yang melakukan beberapa pemrosesan adalah:

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;

Perhatikan bahwa kueri tidak berisi subpilihan yang kita tahu tidak ditangani secara baik oleh perencana MySQL.

Itu akan mengembalikan satu entri per centralValue (cValue) yang tidak memiliki nilai lebih kecil (lValue) atau nilai lebih besar (rValue), yaitu:

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


Tanpa menjelaskan lebih lanjut (kita akan melihatnya di paragraf berikutnya) output ini berarti:

  • Tidak ada nilai antara 0 dan 2
  • Tidak ada nilai antara 9 dan 22
  • Tidak ada nilai antara 24 dan 29
  • Tidak ada nilai antara 29 dan 33
  • Tidak ada nilai antara 33 dan MAX VALUE

Jadi ide dasarnya adalah melakukan gabungan KANAN dan KIRI dengan tabel yang sama melihat apakah kita memiliki nilai adjacents per nilai (yaitu: jika nilai pusat adalah '3' maka kita periksa 3-1 = 2 di kiri dan 3 + 1 di kanan), dan ketika ROW memiliki nilai NULL di RIGHT atau LEFT maka kita tahu tidak ada nilai yang berdekatan.

Output mentah lengkap dari tabel saya adalah:

select * from arrc_vouchers order by id asc;

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

Beberapa catatan:

  1. Pernyataan SQL IF dalam kondisi gabungan diperlukan jika Anda mendefinisikan bidang 'id' sebagai UNSIGNED, oleh karena itu tidak akan memungkinkan Anda untuk menurunkannya di bawah nol. Ini tidak benar-benar diperlukan jika Anda menjaga c.value> 0 seperti yang dinyatakan di catatan berikutnya, tetapi saya memasukkannya hanya sebagai doc.
  2. Saya memfilter nilai pusat nol karena kami tidak tertarik dengan nilai sebelumnya dan kami dapat memperoleh nilai posting dari baris berikutnya.

2

Jika ada urutan yang memiliki gap maksimal satu antara dua angka (seperti 1,3,5,6) maka query yang dapat digunakan adalah:

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);
  • nama_tabel - source1
  • nama kolom - id

1

berdasarkan jawaban yang diberikan di atas oleh Lucek, prosedur tersimpan ini memungkinkan Anda menentukan nama tabel dan kolom yang ingin Anda uji untuk menemukan rekaman yang tidak bersebelahan - dengan demikian menjawab pertanyaan asli dan juga mendemonstrasikan bagaimana seseorang dapat menggunakan @var untuk merepresentasikan tabel & / atau kolom dalam prosedur tersimpan.

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

Saya mencobanya dengan cara yang berbeda dan kinerja terbaik yang saya temukan adalah kueri sederhana ini:

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
;

... satu left join untuk memeriksa apakah id berikutnya ada, hanya jika next jika tidak ditemukan, maka subquery mencari id berikutnya yang ada untuk mencari akhir celah. Saya melakukannya karena query dengan equal (=) adalah kinerja yang lebih baik daripada operator (>).

Menggunakan sqlfiddle itu tidak menunjukkan kinerja yang sangat berbeda dari kueri lain tetapi dalam database nyata, kueri di atas ini menghasilkan 3 kali lebih cepat daripada yang lain.

Skema:

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

Ikuti di bawah semua kueri yang saya buat untuk membandingkan kinerja:

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
;

Mungkin itu membantu seseorang dan bermanfaat.

Anda dapat melihat dan menguji kueri saya menggunakan sqlfiddle ini :

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


0

Meskipun ini semua tampaknya berhasil, set hasil kembali dalam waktu yang sangat lama ketika ada 50.000 rekaman.

Saya menggunakan ini, dan menemukan celah atau berikutnya yang tersedia (terakhir digunakan + 1) dengan pengembalian yang jauh lebih cepat dari kueri.

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;

ini menemukan celah pertama yang bukan merupakan pertanyaan yang ditanyakan.
menarik

0

Mungkin tidak relevan, tetapi saya sedang mencari sesuatu seperti ini untuk membuat daftar celah dalam urutan angka dan menemukan posting ini, yang memiliki beberapa solusi berbeda tergantung pada apa yang Anda cari. Saya sedang mencari celah pertama yang tersedia dalam urutan (yaitu nomor berikutnya yang tersedia), dan ini tampaknya berfungsi dengan baik.

SELECT MIN (l.number_sequence + 1) sebagai nextavabile dari pasien sebagai l LEFT OUTER JOIN patient as r on l.number_sequence + 1 = r.number_sequence DI MANA r.number_sequence adalah NULL. Beberapa skenario dan solusi lain dibahas di sana, dari tahun 2005!

Bagaimana Menemukan Nilai yang Hilang dalam Urutan Dengan SQL

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.