¿Cómo encuentro una "brecha" en la ejecución de un contador con SQL?

106

Me gustaría encontrar el primer "espacio" en una columna de contador en una tabla SQL. Por ejemplo, si hay valores 1, 2, 4 y 5, me gustaría averiguar 3.

Por supuesto, puedo ordenar los valores y revisarlos manualmente, pero me gustaría saber si habría una manera de hacerlo en SQL.

Además, debería ser SQL bastante estándar, trabajando con diferentes DBMS.

Touko
fuente
En Sql Server 2008 y posteriores, puede usar la LAG(id, 1, null)función con OVER (ORDER BY id)cláusula.
ajeh

Respuestas:

184

En MySQLy 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

En 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

En 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 en todas partes, 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 admiten funciones de ventana 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
Quassnoi
fuente
39
@vulkanino: pídales que conserven la sangría. También tenga en cuenta que la licencia de creative commons requiere que se tatúe mi nick y la pregunta URLtambién, aunque creo que puede estar codificada en QR.
Quassnoi
4
Esto es genial, pero si lo tuviera [1, 2, 11, 12], entonces esto solo lo encontraría 3. Lo que me encantaría encontrar es 3-10 en su lugar, básicamente el principio y el final de cada hueco. Entiendo que podría tener que escribir mi propio script de Python que aproveche SQL (en mi caso MySql), pero sería bueno si SQL pudiera acercarme a lo que quiero (tengo una tabla con 2 millones de filas que tiene espacios, así que tendré que cortarlo en trozos más pequeños y ejecutar algo de SQL en él). Supongo que podría ejecutar una consulta para encontrar el inicio de una brecha, luego otra para encontrar el final de una brecha, y luego "fusionar ordenando" las dos secuencias.
Hamish Grubijan
1
@HamishGrubijan: publíquelo como otra pregunta
Quassnoi
2
@Malkocoglu: obtendrá NULL, no 0, si la mesa está vacía. Esto es cierto para todas las bases de datos.
Quassnoi
5
esto no encontrará los huecos iniciales correctamente. si tiene 3,4,5,6,8. este código reportará 7, porque tiene NO 1 para verificar. Entonces, si le faltan números iniciales, tendrá que verificarlo.
ttomsen
12

Todas sus respuestas funcionan bien si tiene un primer valor de identificación = 1; de lo contrario, esta brecha no se detectará. Por ejemplo, si los valores de identificación de su tabla son 3,4,5, sus consultas devolverán 6.

Hice algo como esto

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
fuente
Esto encontrará el primer espacio. Si tiene id 0, 2,3,4. La respuesta es 1. Estaba buscando una respuesta para encontrar la brecha más grande. Digamos que la secuencia es 0,2,3,4, 100,101,102. Quiero encontrar una brecha de 4-99.
Kemin Zhou
8

Realmente no existe una forma SQL extremadamente estándar de hacer esto, pero con alguna forma de cláusula de limitación puede hacerlo

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)

o

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)

o

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)

caos
fuente
si hay un intervalo de intervalo, solo se devolverá la primera fila del intervalo para su consulta de postgres.
John Haugeland
Esto tiene más sentido para mí, usar una combinación también le permitirá cambiar su valor TOP, para mostrar más resultados de brecha.
AJ_
1
Gracias, esto funciona muy bien y si desea ver todos los puntos donde hay un hueco, puede eliminar el límite.
mekbib
8

Lo primero que me vino a la cabeza. No estoy seguro de si es una buena idea seguir este camino, pero debería funcionar. Suponga que la tabla es ty la columna es 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

Editar: Este puede ser un tic más rápido (¡y más corto!):

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 - hacker
fuente
UNIÓN EXTERIOR IZQUIERDA t ==> UNIÓN EXTERIOR IZQUIERDA t2
Eamon Nerbonne
1
No-no, Eamon, LEFT OUTER JOING t2requeriría que tuvieras una t2mesa, que es solo un alias.
Michael Krelin - hacker
6

Esto funciona en SQL Server; no se puede probar en otros sistemas, pero parece estándar ...

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

También puede agregar un punto de partida a la 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

Entonces, si tuviera 2000, 2001, 2002 y 2005 donde 2003 y 2004 no existían, regresaría 2003.

Mayonesa
fuente
3

La siguiente solución:

  • proporciona datos de prueba;
  • una consulta interna que produce otras lagunas; y
  • funciona en SQL Server 2012.

Numera las filas ordenadas secuencialmente en la cláusula " con " y luego reutiliza el resultado dos veces con una combinación interna en el número de fila, pero compensada por 1 para comparar la fila anterior con la fila posterior, buscando ID con un espacio mayor que 1. Más de lo solicitado pero de mayor aplicación.

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

La consulta interna produce:

gap_start   gap_end

3           3

9           11

La consulta externa produce:

gap_start

3
wwmbes
fuente
2

Unión interna a una vista o secuencia que tiene todos los valores posibles.

¿Incapaz? Hacer una mesa. Siempre mantengo una mesa simulada solo para esto.

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

Luego,

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

por PostgreSQL

Un ejemplo que hace uso de una consulta recursiva.

Esto podría ser útil si desea encontrar un espacio en un rango específico (funcionará incluso si la tabla está vacía, mientras que los otros ejemplos no)

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
fuente
1

Mi conjetura:

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
fuente
1

Este da cuenta de todo lo mencionado hasta ahora. Incluye 0 como punto de partida, que será predeterminado si tampoco existen valores. También agregué las ubicaciones apropiadas para las otras partes de una clave de varios valores. Esto solo se ha probado en 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
fuente
1

Escribí una forma rápida de hacerlo. No estoy seguro de que sea el más eficiente, pero hace el trabajo. Tenga en cuenta que no le dice el espacio, pero le dice la identificación antes y después del espacio (tenga en cuenta que el espacio puede ser de varios valores, por ejemplo, 1,2,4,7,11, etc.)

Estoy usando sqlite como ejemplo

Si esta es la estructura de tu mesa

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

y estas son tus filas

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

La consulta es

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
fuente
0
select min([ColumnName]) from [TableName]
where [ColumnName]-1 not in (select [ColumnName] from [TableName])
and [ColumnName] <> (select min([ColumnName]) from [TableName])
Behnam
fuente
0

Aquí hay una solución SQL estándar que se ejecuta en todos los servidores de bases de datos sin cambios:

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

Ver en acción para;

Mehmet Kaplan
fuente
0

También funciona para tablas vacías o con valores negativos. Solo probado en 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
fuente
0

Si usa Firebird 3, esto es más elegante y simple:

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
fuente
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
fuente
0

Encontré que la mayoría de los enfoques se ejecutan muy, muy lentamente mysql. Aquí está mi solución para mysql < 8.0. Probado en registros de 1 millón con un espacio cerca del final de ~ 1 segundo para terminar. No estoy seguro de si se ajusta a otros sabores 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
Supongo que la secuencia comienza en "1".
Max Ivanov
fuente
0

Si su contador comienza desde 1 y desea generar el primer número de secuencia (1) cuando está vacío, aquí está el fragmento de código corregido de la primera respuesta válida 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
     )  
kozo
fuente
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
fuente
0

Si los números en la columna son números enteros positivos (comenzando desde 1), aquí se explica cómo resolverlo fácilmente. (asumiendo que ID es el nombre de su columna)

    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
fuente
encontrará espacios solo hasta el número de filas en 'TABLE-NAME' como "SELECT ROW_NUMBER () OVER () AS NUM FROM 'TABLE-NAME'" dará identificadores hasta el número de filas solamente
vijay shanker