Como faço para encontrar uma “lacuna” no contador de execução com SQL?


106

Gostaria de encontrar a primeira "lacuna" em uma coluna de contador em uma tabela SQL. Por exemplo, se houver valores 1,2,4 e 5, gostaria de descobrir 3.

Claro que posso colocar os valores em ordem e analisá-los manualmente, mas gostaria de saber se haveria uma maneira de fazer isso em SQL.

Além disso, deve ser um SQL padrão, trabalhando com diferentes SGBDs.


No Sql Server 2008 e superior, você pode usar a LAG(id, 1, null)função com OVER (ORDER BY id)cláusula.
ajeh

Respostas:


185

Em MySQLe 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

Em 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

Em 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 (funciona em qualquer lugar, menos eficiente):

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

Sistemas que suportam funções de janela deslizante:

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

39
@vulkanino: peça que preservem o recuo. Observe também que a licença do Creative Commons exige que você tatuar meu nick e a pergunta URLtambém, embora possa ser codificado por QR, eu acho.
Quassnoi

4
Isso é ótimo, mas se tivesse [1, 2, 11, 12], isso iria encontrar apenas 3. O que eu adoraria encontrar é 3-10 em vez disso - basicamente o início e o fim de cada lacuna. Eu entendo que posso ter que escrever meu próprio script python que aproveita SQL (no meu caso MySql), mas seria bom se SQL pudesse me aproximar do que eu quero (eu tenho uma tabela com 2 milhões de linhas que tem lacunas, então vou precisar dividi-lo em pedaços menores e executar algum SQL nele). Suponho que eu poderia executar uma consulta para encontrar o início de uma lacuna, depois outra para encontrar o final de uma lacuna, e eles "mesclaram classificação" as duas sequências.
Hamish Grubijan

1
@HamishGrubijan: por favor, poste como outra pergunta
Quassnoi

2
@Malkocoglu: você obterá NULL, não 0, se a mesa estiver vazia. Isso é verdade para todos os bancos de dados.
Quassnoi de

5
isso não encontrará as lacunas iniciais corretamente. se você tiver 3,4,5,6,8. este código reportará 7, porque não tem 1 para verificar. Portanto, se faltarem números iniciais, você terá que verificar isso.
ttomsen

12

Todas as suas respostas funcionam bem se você tiver um primeiro valor id = 1, caso contrário, essa lacuna não será detectada. Por exemplo, se os valores de id de sua tabela são 3,4,5, suas consultas retornarão 6.

Eu fiz algo assim

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) 

Isso encontrará a primeira lacuna. Se você tiver id 0, 2,3,4. A resposta é 1. Eu estava procurando uma resposta para encontrar a maior lacuna. Digamos que a sequência seja 0,2,3,4, 100,101,102. Eu quero encontrar a lacuna 4-99.
Kemin Zhou

8

Não há realmente uma maneira SQL extremamente padrão de fazer isso, mas com alguma forma de cláusula limitadora você pode fazer

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)

ou

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

(Servidor SQL)

ou

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

(Oráculo)


se houver um intervalo de lacuna, apenas a primeira linha no intervalo será retornada para sua consulta postgres.
John Haugeland

Isso faz mais sentido para mim, pois usar uma junção também permitirá que você altere seu valor TOP, para mostrar mais resultados de lacunas.
AJ_

1
Obrigado, isso funciona muito bem e se você gostaria de ver todos os pontos onde há uma lacuna, você pode remover o limite.
mekbib.awoke

8

A primeira coisa que me veio à cabeça. Não tenho certeza se é uma boa ideia seguir esse caminho, mas deve funcionar. Suponha que a tabela seja te a coluna seja 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

Edit: Este pode ser um carrapato mais rápido (e mais curto!):

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


LEFT OUTER JOIN t ==> LEFT OUTER JOIN t2
Eamon Nerbonne

1
Não, não, Eamon, LEFT OUTER JOING t2exigiria que você tivesse uma t2mesa, que é apenas um apelido.
Michael Krelin - hacker de

6

Isso funciona no SQL Server - não posso testá-lo em outros sistemas, mas parece padrão ...

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

Você também pode adicionar um ponto de partida para a cláusula where ...

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

Portanto, se você tivesse 2000, 2001, 2002 e 2005, onde 2003 e 2004 não existiam, ele retornaria 2003.


3

A seguinte solução:

  • fornece dados de teste;
  • uma consulta interna que produz outras lacunas; e
  • ele funciona no SQL Server 2012.

Numera as linhas ordenadas sequencialmente na cláusula " with " e, em seguida, reutiliza o resultado duas vezes com uma junção interna no número da linha, mas deslocado em 1 para comparar a linha anterior com a linha posterior, procurando IDs com uma lacuna maior que 1. Mais do que solicitado, mas mais amplamente aplicável.

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

A consulta interna produz:

gap_start   gap_end

3           3

9           11

A consulta externa produz:

gap_start

3

2

Associação interna a uma vista ou sequência que possui todos os valores possíveis.

Sem mesa? Faça uma mesa. Sempre mantenho uma mesa fictícia só para isso.

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

Então,

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

2

Para PostgreSQL

Um exemplo que faz uso de consulta recursiva.

Isso pode ser útil se você quiser encontrar uma lacuna em um intervalo específico (funcionará mesmo se a tabela estiver vazia, enquanto os outros exemplos não)

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

1

Meu palpite:

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;

1

Este é responsável por tudo o que foi mencionado até agora. Inclui 0 como ponto de partida, para o qual será padronizado se nenhum valor também existir. Eu também adicionei os locais apropriados para as outras partes de uma chave de vários valores. Isso só foi testado no 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

1

Eu escrevi uma maneira rápida de fazer isso. Não tenho certeza se isso é o mais eficiente, mas dá conta do recado. Observe que ele não informa a lacuna, mas informa o id antes e depois da lacuna (tenha em mente que a lacuna pode ter vários valores, por exemplo 1,2,4,7,11 etc)

Estou usando sqlite como exemplo

Se esta for a sua estrutura de tabela

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

e estas são suas linhas

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

A consulta é

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


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

0

Aqui está uma solução SQL padrão que é executada em todos os servidores de banco de dados sem alterações:

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

Veja em ação para;


0

Funciona com tabelas vazias ou com valores negativos. Acabei de testar no 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

0

Se você usar o Firebird 3, isso é mais elegante e simples:

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

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

0

Descobri que a maioria das abordagens funciona muito, muito lentamente mysql. Aqui está minha solução para mysql < 8.0. Testado em registros de 1 milhão com uma lacuna perto do final ~ 1 segundo para terminar. Não tenho certeza se ele se encaixa em outros tipos de 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
Presumo que a sequência comece em `1`.

0

Se o seu contador está começando em 1 e você deseja gerar o primeiro número da sequência (1) quando vazio, aqui está o trecho corrigido do código da primeira resposta válido para 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
     )  

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]

0

Se os números na coluna forem inteiros positivos (começando em 1), aqui está como resolvê-lo facilmente. (assumindo que ID é o nome da sua coluna)

    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

ele encontrará lacunas apenas até o número de linhas em 'TABLE-NAME' como "SELECT ROW_NUMBER () OVER () AS NUM FROM 'TABLE-NAME'" dará ids até o número de linhas apenas
vijay shanker
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.