Bagaimana cara menemukan "celah" dalam menjalankan counter dengan SQL?

106

Saya ingin menemukan "celah" pertama di kolom penghitung dalam tabel SQL. Misalnya, jika ada nilai 1,2,4 dan 5, saya ingin mencari 3.

Saya tentu saja bisa mendapatkan nilai secara berurutan dan melihatnya secara manual, tetapi saya ingin tahu apakah ada cara untuk melakukannya di SQL.

Selain itu, itu harus SQL yang cukup standar, bekerja dengan DBMS yang berbeda.

Touko
sumber
Di Sql server 2008 dan yang lebih baru, Anda dapat menggunakan LAG(id, 1, null)fungsi dengan OVER (ORDER BY id)klausa.
ajeh

Jawaban:

185

Dalam MySQLdan PostgreSQL:

SELECT  id + 1
FROM    mytable mo
WHERE   NOT EXISTS
        (
        SELECT  NULL
        FROM    mytable mi 
        WHERE   mi.id = mo.id + 1
        )
ORDER BY
        id
LIMIT 1

Masuk SQL Server:

SELECT  TOP 1
        id + 1
FROM    mytable mo
WHERE   NOT EXISTS
        (
        SELECT  NULL
        FROM    mytable mi 
        WHERE   mi.id = mo.id + 1
        )
ORDER BY
        id

Masuk Oracle:

SELECT  *
FROM    (
        SELECT  id + 1 AS gap
        FROM    mytable mo
        WHERE   NOT EXISTS
                (
                SELECT  NULL
                FROM    mytable mi 
                WHERE   mi.id = mo.id + 1
                )
        ORDER BY
                id
        )
WHERE   rownum = 1

ANSI (bekerja di mana saja, paling tidak efisien):

SELECT  MIN(id) + 1
FROM    mytable mo
WHERE   NOT EXISTS
        (
        SELECT  NULL
        FROM    mytable mi 
        WHERE   mi.id = mo.id + 1
        )

Sistem yang mendukung fungsi jendela geser:

SELECT  -- TOP 1
        -- Uncomment above for SQL Server 2012+
        previd
FROM    (
        SELECT  id,
                LAG(id) OVER (ORDER BY id) previd
        FROM    mytable
        ) q
WHERE   previd <> id - 1
ORDER BY
        id
-- LIMIT 1
-- Uncomment above for PostgreSQL
Quassnoi
sumber
39
@vulkanino: minta mereka untuk mempertahankan lekukan. Juga harap dicatat bahwa lisensi creative commons mengharuskan Anda untuk menato nick saya dan pertanyaannya URLjuga, meskipun saya kira mungkin kode QR itu.
Quassnoi
4
Ini bagus, tetapi jika saya punya [1, 2, 11, 12], maka ini hanya akan menemukan 3. Yang ingin saya temukan adalah 3-10 sebagai gantinya - pada dasarnya awal dan akhir dari setiap celah. Saya mengerti bahwa saya mungkin harus menulis skrip python saya sendiri yang memanfaatkan SQL (dalam kasus saya MySql), tetapi alangkah baiknya jika SQL dapat membuat saya lebih dekat dengan apa yang saya inginkan (saya memiliki tabel dengan 2 juta baris yang memiliki celah, jadi saya perlu mengirisnya menjadi potongan-potongan kecil dan menjalankan beberapa SQL di atasnya). Saya kira saya bisa menjalankan satu kueri untuk menemukan awal celah, lalu kueri lain untuk menemukan akhir celah, dan mereka "menggabungkan" dua urutan.
Hamish Grubijan
1
@HamishGrubijan: tolong posting sebagai pertanyaan lain
Quassnoi
2
@Malkocoglu: Anda akan mendapatkan NULL, tidak 0, jika tabel kosong. Ini berlaku untuk semua database.
Quassnoi
5
ini tidak akan menemukan celah awal dengan benar. jika Anda memiliki 3,4,5,6,8. kode ini akan melaporkan 7, karena memiliki NO 1 untuk diperiksa. Jadi jika Anda kehilangan nomor awal, Anda harus memeriksanya.
ttomsen
12

Jawaban Anda semuanya berfungsi dengan baik jika Anda memiliki nilai pertama id = 1, jika tidak, celah ini tidak akan terdeteksi. Misalnya jika nilai id tabel Anda adalah 3,4,5, kueri Anda akan menghasilkan 6.

Saya melakukan sesuatu seperti ini

SELECT MIN(ID+1) FROM (
    SELECT 0 AS ID UNION ALL 
    SELECT  
        MIN(ID + 1)
    FROM    
        TableX) AS T1
WHERE
    ID+1 NOT IN (SELECT ID FROM TableX) 
Ruben
sumber
Ini akan menemukan celah pertama. Jika Anda memiliki id 0, 2,3,4. Jawabannya adalah 1. Saya sedang mencari jawaban untuk menemukan celah terbesar. Katakanlah urutannya adalah 0,2,3,4, 100,101,102. Saya ingin menemukan celah 4-99.
Kemin Zhou
8

Sebenarnya tidak ada cara SQL yang sangat standar untuk melakukan ini, tetapi dengan beberapa bentuk klausa pembatas yang dapat Anda lakukan

SELECT `table`.`num` + 1
FROM `table`
LEFT JOIN `table` AS `alt`
ON `alt`.`num` = `table`.`num` + 1
WHERE `alt`.`num` IS NULL
LIMIT 1

(MySQL, PostgreSQL)

atau

SELECT TOP 1 `num` + 1
FROM `table`
LEFT JOIN `table` AS `alt`
ON `alt`.`num` = `table`.`num` + 1
WHERE `alt`.`num` IS NULL

(SQL Server)

atau

SELECT `num` + 1
FROM `table`
LEFT JOIN `table` AS `alt`
ON `alt`.`num` = `table`.`num` + 1
WHERE `alt`.`num` IS NULL
AND ROWNUM = 1

(Peramal)

kekacauan
sumber
jika ada jarak gap, hanya baris pertama dalam range yang akan dikembalikan untuk query postgres Anda.
John Haugeland
Ini paling masuk akal bagi saya, menggunakan gabungan juga akan memungkinkan Anda mengubah nilai TOP Anda, untuk menunjukkan lebih banyak hasil gap.
AJ_
1
Terima kasih, ini bekerja dengan sangat baik dan jika Anda ingin melihat semua titik di mana terdapat celah, Anda dapat menghapus batasnya.
mekbib.awoke
8

Hal pertama yang terlintas di kepalaku. Tidak yakin apakah itu ide yang baik untuk pergi dengan cara ini sama sekali, tetapi harus berhasil. Misalkan tabelnya tdan kolomnya adalah c:

SELECT t1.c+1 AS gap FROM t as t1 LEFT OUTER JOIN t as t2 ON (t1.c+1=t2.c) WHERE t2.c IS NULL ORDER BY gap ASC LIMIT 1

Sunting: Yang ini mungkin lebih cepat (dan lebih pendek!):

SELECT min(t1.c)+1 AS gap FROM t as t1 LEFT OUTER JOIN t as t2 ON (t1.c+1=t2.c) WHERE t2.c IS NULL

Michael Krelin - peretas
sumber
LEFT OUTER JOIN t ==> LEFT OUTER JOIN t2
Eamon Nerbonne
1
Tidak-tidak, Eamon, LEFT OUTER JOING t2akan meminta Anda untuk memiliki t2tabel, yang hanya sebuah alias.
Michael Krelin - hacker
6

Ini berfungsi di SQL Server - tidak dapat mengujinya di sistem lain tetapi tampaknya standar ...

SELECT MIN(t1.ID)+1 FROM mytable t1 WHERE NOT EXISTS (SELECT ID FROM mytable WHERE ID = (t1.ID + 1))

Anda juga bisa menambahkan titik awal ke klausa where ...

SELECT MIN(t1.ID)+1 FROM mytable t1 WHERE NOT EXISTS (SELECT ID FROM mytable WHERE ID = (t1.ID + 1)) AND ID > 2000

Jadi jika Anda memiliki 2000, 2001, 2002, dan 2005 di mana 2003 dan 2004 tidak ada, itu akan mengembalikan 2003.

Mayo
sumber
3

Solusi berikut ini:

  • menyediakan data uji;
  • kueri dalam yang menghasilkan celah lain; dan
  • ini berfungsi di SQL Server 2012.

Menomori baris yang diurutkan secara berurutan dalam klausa " dengan " dan kemudian menggunakan kembali hasilnya dua kali dengan gabungan dalam pada nomor baris, tetapi diimbangi dengan 1 untuk membandingkan baris sebelumnya dengan baris setelahnya, mencari ID dengan celah lebih besar dari 1. Lebih dari yang diminta tetapi lebih dapat diterapkan secara luas.

create table #ID ( id integer );

insert into #ID values (1),(2),    (4),(5),(6),(7),(8),    (12),(13),(14),(15);

with Source as (
    select
         row_number()over ( order by A.id ) as seq
        ,A.id                               as id
    from #ID as A WITH(NOLOCK)
)
Select top 1 gap_start from (
    Select 
         (J.id+1) as gap_start
        ,(K.id-1) as gap_end
    from       Source as J
    inner join Source as K
    on (J.seq+1) = K.seq
    where (J.id - (K.id-1)) <> 0
) as G

Kueri dalam menghasilkan:

gap_start   gap_end

3           3

9           11

Kueri luar menghasilkan:

gap_start

3
wwmbes.dll
sumber
2

Inner bergabung ke tampilan atau urutan yang memiliki semua kemungkinan nilai.

Tidak ada meja? Buatlah meja. Saya selalu menyimpan meja boneka hanya untuk ini.

create table artificial_range( 
  id int not null primary key auto_increment, 
  name varchar( 20 ) null ) ;

-- or whatever your database requires for an auto increment column

insert into artificial_range( name ) values ( null )
-- create one row.

insert into artificial_range( name ) select name from artificial_range;
-- you now have two rows

insert into artificial_range( name ) select name from artificial_range;
-- you now have four rows

insert into artificial_range( name ) select name from artificial_range;
-- you now have eight rows

--etc.

insert into artificial_range( name ) select name from artificial_range;
-- you now have 1024 rows, with ids 1-1024

Kemudian,

 select a.id from artificial_range a
 where not exists ( select * from your_table b
 where b.counter = a.id) ;
tpdi
sumber
2

Untuk PostgreSQL

Contoh yang memanfaatkan kueri rekursif.

Ini mungkin berguna jika Anda ingin menemukan celah dalam rentang tertentu (ini akan berfungsi bahkan jika tabel kosong, sedangkan contoh lainnya tidak)

WITH    
    RECURSIVE a(id) AS (VALUES (1) UNION ALL SELECT id + 1 FROM a WHERE id < 100), -- range 1..100  
    b AS (SELECT id FROM my_table) -- your table ID list    
SELECT a.id -- find numbers from the range that do not exist in main table
FROM a
LEFT JOIN b ON b.id = a.id
WHERE b.id IS NULL
-- LIMIT 1 -- uncomment if only the first value is needed
AlexM
sumber
1

Tebakanku:

SELECT MIN(p1.field) + 1 as gap
FROM table1 AS p1  
INNER JOIN table1 as p3 ON (p1.field = p3.field + 2)
LEFT OUTER JOIN table1 AS p2 ON (p1.field = p2.field + 1)
WHERE p2.field is null;
Leonel Martins
sumber
1

Yang ini menjelaskan semua yang disebutkan sejauh ini. Ini mencakup 0 sebagai titik awal, yang akan menjadi default jika tidak ada nilai juga. Saya juga menambahkan lokasi yang sesuai untuk bagian lain dari kunci multi-nilai. Ini hanya diuji di SQL Server.

select
    MIN(ID)
from (
    select
        0 ID
    union all
    select
        [YourIdColumn]+1
    from
        [YourTable]
    where
        --Filter the rest of your key--
    ) foo
left join
    [YourTable]
    on [YourIdColumn]=ID
    and --Filter the rest of your key--
where
    [YourIdColumn] is null
Carter Medlin
sumber
1

Saya menulis cara cepat untuk melakukannya. Tidak yakin ini yang paling efisien, tetapi menyelesaikan pekerjaan. Perhatikan bahwa ini tidak memberi tahu Anda celahnya, tetapi memberi tahu Anda id sebelum dan sesudah celah (perlu diingat bahwa celah itu bisa berupa beberapa nilai, jadi misalnya 1,2,4,7,11 dll)

Saya menggunakan sqlite sebagai contoh

Jika ini adalah struktur tabel Anda

create table sequential(id int not null, name varchar(10) null);

dan ini adalah baris Anda

id|name
1|one
2|two
4|four
5|five
9|nine

Pertanyaannya adalah

select a.* from sequential a left join sequential b on a.id = b.id + 1 where b.id is null and a.id <> (select min(id) from sequential)
union
select a.* from sequential a left join sequential b on a.id = b.id - 1 where b.id is null and a.id <> (select max(id) from sequential);

https://gist.github.com/wkimeria/7787ffe84d1c54216f1b320996b17b7e

William Kimeria
sumber
0
select min([ColumnName]) from [TableName]
where [ColumnName]-1 not in (select [ColumnName] from [TableName])
and [ColumnName] <> (select min([ColumnName]) from [TableName])
Behnam
sumber
0

Berikut adalah solusi SQL standar yang berjalan di semua server database tanpa perubahan:

select min(counter + 1) FIRST_GAP
    from my_table a
    where not exists (select 'x' from my_table b where b.counter = a.counter + 1)
        and a.counter <> (select max(c.counter) from my_table c);

Lihat beraksi untuk;

Mehmet Kaplan
sumber
0

Ini berfungsi untuk tabel kosong atau dengan nilai negatif juga. Baru saja diuji di SQL Server 2012

 select min(n) from (
select  case when lead(i,1,0) over(order by i)>i+1 then i+1 else null end n from MyTable) w
Horaciux
sumber
0

Jika Anda menggunakan Firebird 3, ini yang paling elegan dan sederhana:

select RowID
  from (
    select `ID_Column`, Row_Number() over(order by `ID_Column`) as RowID
      from `Your_Table`
        order by `ID_Column`)
    where `ID_Column` <> RowID
    rows 1
Rosen Nikolov
sumber
0
            -- PUT THE TABLE NAME AND COLUMN NAME BELOW
            -- IN MY EXAMPLE, THE TABLE NAME IS = SHOW_GAPS AND COLUMN NAME IS = ID

            -- PUT THESE TWO VALUES AND EXECUTE THE QUERY

            DECLARE @TABLE_NAME VARCHAR(100) = 'SHOW_GAPS'
            DECLARE @COLUMN_NAME VARCHAR(100) = 'ID'


            DECLARE @SQL VARCHAR(MAX)
            SET @SQL = 
            'SELECT  TOP 1
                    '+@COLUMN_NAME+' + 1
            FROM    '+@TABLE_NAME+' mo
            WHERE   NOT EXISTS
                    (
                    SELECT  NULL
                    FROM    '+@TABLE_NAME+' mi 
                    WHERE   mi.'+@COLUMN_NAME+' = mo.'+@COLUMN_NAME+' + 1
                    )
            ORDER BY
                    '+@COLUMN_NAME

            -- SELECT @SQL

            DECLARE @MISSING_ID TABLE (ID INT)

            INSERT INTO @MISSING_ID
            EXEC (@SQL)

            --select * from @MISSING_ID

            declare @var_for_cursor int
            DECLARE @LOW INT
            DECLARE @HIGH INT
            DECLARE @FINAL_RANGE TABLE (LOWER_MISSING_RANGE INT, HIGHER_MISSING_RANGE INT)
            DECLARE IdentityGapCursor CURSOR FOR   
            select * from @MISSING_ID
            ORDER BY 1;  

            open IdentityGapCursor

            fetch next from IdentityGapCursor
            into @var_for_cursor

            WHILE @@FETCH_STATUS = 0  
            BEGIN
            SET @SQL = '
            DECLARE @LOW INT
            SELECT @LOW = MAX('+@COLUMN_NAME+') + 1 FROM '+@TABLE_NAME
                    +' WHERE '+@COLUMN_NAME+' < ' + cast( @var_for_cursor as VARCHAR(MAX))

            SET @SQL = @sql + '
            DECLARE @HIGH INT
            SELECT @HIGH = MIN('+@COLUMN_NAME+') - 1 FROM '+@TABLE_NAME
                    +' WHERE '+@COLUMN_NAME+' > ' + cast( @var_for_cursor as VARCHAR(MAX))

            SET @SQL = @sql + 'SELECT @LOW,@HIGH'

            INSERT INTO @FINAL_RANGE
             EXEC( @SQL)
            fetch next from IdentityGapCursor
            into @var_for_cursor
            END

            CLOSE IdentityGapCursor;  
            DEALLOCATE IdentityGapCursor;  

            SELECT ROW_NUMBER() OVER(ORDER BY LOWER_MISSING_RANGE) AS 'Gap Number',* FROM @FINAL_RANGE
KoP
sumber
0

Menemukan sebagian besar pendekatan berjalan sangat, sangat lambat mysql. Inilah solusi saya untuk mysql < 8.0. Diuji pada catatan 1 juta dengan jeda mendekati akhir ~ 1 detik hingga selesai. Tidak yakin apakah itu cocok dengan citarasa SQL lainnya.

SELECT cardNumber - 1
FROM
    (SELECT @row_number := 0) as t,
    (
        SELECT (@row_number:=@row_number+1), cardNumber, cardNumber-@row_number AS diff
        FROM cards
        ORDER BY cardNumber
    ) as x
WHERE diff >= 1
LIMIT 0,1
Saya berasumsi bahwa urutan dimulai dari `1`.
Max Ivanov
sumber
0

Jika penghitung Anda mulai dari 1 dan Anda ingin menghasilkan urutan nomor pertama (1) saat kosong, berikut adalah bagian kode yang dikoreksi dari jawaban pertama yang valid untuk Oracle:

SELECT
  NVL(MIN(id + 1),1) AS gap
FROM
  mytable mo  
WHERE 1=1
  AND NOT EXISTS
      (
       SELECT  NULL
       FROM    mytable mi 
       WHERE   mi.id = mo.id + 1
      )
  AND EXISTS
     (
       SELECT  NULL
       FROM    mytable mi 
       WHERE   mi.id = 1
     )  
kozo
sumber
0
DECLARE @Table AS TABLE(
[Value] int
)

INSERT INTO @Table ([Value])
VALUES
 (1),(2),(4),(5),(6),(10),(20),(21),(22),(50),(51),(52),(53),(54),(55)
 --Gaps
 --Start    End     Size
 --3        3       1
 --7        9       3
 --11       19      9
 --23       49      27


SELECT [startTable].[Value]+1 [Start]
     ,[EndTable].[Value]-1 [End]
     ,([EndTable].[Value]-1) - ([startTable].[Value]) Size 
 FROM 
    (
SELECT [Value]
    ,ROW_NUMBER() OVER(PARTITION BY 1 ORDER BY [Value]) Record
FROM @Table
)AS startTable
JOIN 
(
SELECT [Value]
,ROW_NUMBER() OVER(PARTITION BY 1 ORDER BY [Value]) Record
FROM @Table
)AS EndTable
ON [EndTable].Record = [startTable].Record+1
WHERE [startTable].[Value]+1 <>[EndTable].[Value]
Dominic H.
sumber
0

Jika angka-angka di kolom adalah bilangan bulat positif (dimulai dari 1) maka berikut adalah cara menyelesaikannya dengan mudah. (dengan asumsi ID adalah nama kolom Anda)

    SELECT TEMP.ID 
    FROM (SELECT ROW_NUMBER() OVER () AS NUM FROM 'TABLE-NAME') AS TEMP 
    WHERE ID NOT IN (SELECT ID FROM 'TABLE-NAME')
    ORDER BY 1 ASC LIMIT 1
Abrhalei
sumber
itu akan menemukan celah hanya sampai jumlah baris di 'TABLE-NAME' sebagai "SELECT ROW_NUMBER () OVER () AS NUM FROM 'TABLE-NAME'" akan memberikan id sampai jumlah baris saja
vijay shanker