Jak znaleźć „lukę” w uruchamianiu licznika za pomocą SQL?

106

Chciałbym znaleźć pierwszą „lukę” w kolumnie licznika w tabeli SQL. Na przykład, jeśli istnieją wartości 1, 2, 4 i 5, chciałbym się dowiedzieć 3.

Mogę oczywiście uporządkować wartości i przejść przez to ręcznie, ale chciałbym wiedzieć, czy można to zrobić w języku SQL.

Ponadto powinien to być dość standardowy SQL, działający z różnymi DBMS.

Touko
źródło
W Sql Server 2008 i nowszych można używać LAG(id, 1, null)funkcji z OVER (ORDER BY id)klauzulą.
ajeh

Odpowiedzi:

185

W MySQLi 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

W 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

W 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 (działa wszędzie, najmniej wydajnie):

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

Systemy wspierające funkcje okien przesuwnych:

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
źródło
40
@vulkanino: poproś ich o zachowanie wcięcia. Pamiętaj również, że licencja Creative Commons wymaga tatuowania mojego nicka i pytania URL, chociaż myślę, że może być zakodowane QR.
Quassnoi
4
To jest super, ale gdybym miał [1, 2, 11, 12], to by się znalazł tylko 3. Zamiast tego chciałbym znaleźć 3-10 - w zasadzie początek i koniec każdej przerwy. Rozumiem, że być może będę musiał napisać własny skrypt w Pythonie, który wykorzystuje SQL (w moim przypadku MySql), ale byłoby miło, gdyby SQL przybliżył mnie do tego, czego chcę (mam tabelę z 2 milionami wierszy, która ma luki, więc będę musiał pokroić go na mniejsze kawałki i uruchomić na nim trochę SQL). Przypuszczam, że mógłbym uruchomić jedno zapytanie, aby znaleźć początek przerwy, a następnie drugie, aby znaleźć koniec przerwy, a następnie „scalają” dwie sekwencje.
Hamish Grubijan
1
@HamishGrubijan: opublikuj to jako kolejne pytanie
Quassnoi
2
@Malkocoglu: dostaniesz NULL, nie 0, jeśli stół jest pusty. Dotyczy to wszystkich baz danych.
Quassnoi,
5
nie spowoduje to prawidłowego znalezienia początkowych luk. jeśli masz 3,4,5,6,8. ten kod zgłosi 7, ponieważ ma NIE 1 do sprawdzenia. Więc jeśli brakuje ci numerów startowych, będziesz musiał to sprawdzić.
ttomsen
12

Wszystkie Twoje odpowiedzi działają poprawnie, jeśli masz pierwszą wartość id = 1, w przeciwnym razie ta luka nie zostanie wykryta. Na przykład, jeśli wartości identyfikatora tabeli wynoszą 3,4,5, zapytania zwrócą 6.

Zrobiłem coś takiego

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
źródło
To znajdzie pierwszą lukę. Jeśli masz id 0, 2,3,4. Odpowiedź brzmi 1. Szukałem odpowiedzi, aby znaleźć największą lukę. Powiedzmy, że sekwencja to 0,2,3,4, 100,101,102. Chcę znaleźć lukę 4-99.
Kemin Zhou
8

Nie ma na to wyjątkowo standardowego sposobu w języku SQL, ale można to zrobić za pomocą jakiejś formy klauzuli ograniczającej

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)

lub

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)

lub

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

(Wyrocznia)

chaos
źródło
jeśli istnieje zakres przerw, tylko pierwszy wiersz w zakresie zostanie zwrócony dla zapytania postgres.
John Haugeland,
Ma to dla mnie największy sens, użycie sprzężenia pozwoli Ci również zmienić wartość TOP, aby pokazać więcej wyników luki.
AJ_
1
Dzięki, to działa bardzo dobrze i jeśli chcesz zobaczyć wszystkie punkty, w których występuje luka, możesz usunąć limit.
mekbib.awoke
8

Pierwsza rzecz, która przyszła mi do głowy. Nie jestem pewien, czy w ogóle jest to dobry pomysł, ale powinno działać. Załóżmy, że tabela to, ta kolumna to 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

Edycja: ten może być szybszy (i krótszy!):

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 - haker
źródło
LEFT OUTER JOIN t ==> LEFT OUTER JOIN t2
Eamon Nerbonne
1
Nie, nie, Eamon, LEFT OUTER JOING t2wymagałoby od ciebie posiadania t2tabeli, która jest tylko aliasem.
Michael Krelin - haker
6

Działa to w SQL Server - nie można tego przetestować w innych systemach, ale wydaje się standardowe ...

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

Możesz również dodać punkt początkowy do klauzuli where ...

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

Więc jeśli miałbyś 2000, 2001, 2002 i 2005, gdzie 2003 i 2004 nie istniały, zwróci 2003.

Mayo
źródło
3

Następujące rozwiązanie:

  • dostarcza dane testowe;
  • wewnętrzne zapytanie, które tworzy inne luki; i
  • działa w SQL Server 2012.

Numeruje uporządkowane wiersze sekwencyjnie w klauzuli „ with ”, a następnie dwukrotnie wykorzystuje wynik ze sprzężeniem wewnętrznym na numerze wiersza, ale z przesunięciem o 1, aby porównać wiersz poprzedni z wierszem po, wyszukując identyfikatory z przerwą większą niż 1. Więcej niż oczekiwano, ale ma szersze zastosowanie.

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

Zapytanie wewnętrzne daje:

gap_start   gap_end

3           3

9           11

Zapytanie zewnętrzne daje:

gap_start

3
wwmbes
źródło
2

Sprzężenie wewnętrzne do widoku lub sekwencji, która ma wszystkie możliwe wartości.

Brak stolika? Zrób stół. W tym celu zawsze trzymam fałszywy stolik.

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

Następnie,

 select a.id from artificial_range a
 where not exists ( select * from your_table b
 where b.counter = a.id) ;
tpdi
źródło
2

Dla PostgreSQL

Przykład wykorzystujący zapytanie rekurencyjne.

Może to być przydatne, jeśli chcesz znaleźć lukę w określonym zakresie (będzie działać, nawet jeśli tabela jest pusta, podczas gdy inne przykłady nie będą)

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
źródło
1

Zgaduję że:

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
źródło
1

To wyjaśnia wszystko, o czym do tej pory wspomniano. Zawiera 0 jako punkt początkowy, który zostanie przyjęty domyślnie, jeśli nie ma również żadnych wartości. Dodałem również odpowiednie lokalizacje dla innych części klucza wielowartościowego. Zostało to przetestowane tylko na 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
źródło
1

Napisałem szybki sposób, jak to zrobić. Nie jestem pewien, czy jest to najbardziej wydajne, ale wykonuje swoje zadanie. Zauważ, że nie mówi ci o luce, ale podaje id przed i po przerwie (pamiętaj, że przerwa może mieć wiele wartości, więc na przykład 1,2,4,7,11 itp.)

Jako przykładu używam sqlite

Jeśli to jest twoja struktura tabeli

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

a to są twoje wiersze

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

Pytanie brzmi

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
źródło
0
select min([ColumnName]) from [TableName]
where [ColumnName]-1 not in (select [ColumnName] from [TableName])
and [ColumnName] <> (select min([ColumnName]) from [TableName])
Behnam
źródło
0

Oto standardowe rozwiązanie SQL, które działa na wszystkich serwerach baz danych bez zmian:

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

Zobacz w akcji;

Mehmet Kaplan
źródło
0

Działa również w przypadku pustych tabel lub z wartościami ujemnymi. Właśnie przetestowano w 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
źródło
0

Jeśli używasz Firebird 3, jest to najbardziej eleganckie i proste:

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
źródło
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
źródło
0

Stwierdzono, że większość podejść działa bardzo, bardzo wolno mysql. Oto moje rozwiązanie dla mysql < 8.0. Testowane na rekordach 1M z przerwą pod koniec ~ 1 sekundę do zakończenia. Nie jestem pewien, czy pasuje do innych odmian SQL.

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
Zakładam, że sekwencja zaczyna się od „1”.
Max Ivanov
źródło
0

Jeśli Twój licznik zaczyna się od 1 i chcesz wygenerować pierwszą liczbę sekwencji (1), gdy jest pusta, oto poprawiony fragment kodu z pierwszej odpowiedzi ważny dla 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
źródło
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.
źródło
0

Jeśli liczby w kolumnie są dodatnimi liczbami całkowitymi (zaczynając od 1), oto jak łatwo to rozwiązać. (zakładając, że ID to nazwa Twojej kolumny)

    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
źródło
znajdzie luki tylko do liczby wierszy w „NAZWA-TABELI” jako „WYBIERZ ROW_NUMBER () OVER () AS NUM FROM„ NAZWA-TABELI ”” poda identyfikatory tylko do liczby wierszy
vijay shanker