Come trovo un "gap" nell'esecuzione del contatore con SQL?


106

Vorrei trovare il primo "gap" in una colonna contatore in una tabella SQL. Ad esempio, se ci sono valori 1,2,4 e 5, vorrei scoprirlo 3.

Ovviamente posso ottenere i valori in ordine e esaminarli manualmente, ma mi piacerebbe sapere se ci sarebbe un modo per farlo in SQL.

Inoltre, dovrebbe essere SQL abbastanza standard, funzionante con diversi DBMS.


In SQL Server 2008 e versioni successive è possibile utilizzare la clausola LAG(id, 1, null)function with OVER (ORDER BY id).
ajeh

Risposte:


185

In 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

In 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

In 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 (funziona ovunque, meno efficiente):

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

Sistemi che supportano le funzioni di finestre scorrevoli:

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: chiedi loro di conservare il rientro. Inoltre, tieni presente che la licenza di Creative Commons richiede di tatuare il mio nick e anche la domanda URL, sebbene possa essere codificata QR, credo.
Quassnoi

4
Questo è fantastico, ma se lo avessi fatto [1, 2, 11, 12], allora lo troverei solo 3. Quello che mi piacerebbe che trovasse invece è 3-10, praticamente l'inizio e la fine di ogni intervallo. Capisco che potrei dover scrivere il mio script Python che sfrutta SQL (nel mio caso MySql), ma sarebbe bello se SQL potesse avvicinarmi a ciò che voglio (ho una tabella con 2 milioni di righe che ha degli spazi, quindi dovrò tagliarlo in pezzi più piccoli ed eseguire un po 'di SQL su di esso). Suppongo di poter eseguire una query per trovare l'inizio di uno spazio vuoto, poi un altro per trovare la fine di uno spazio vuoto e poi "unire l'ordinamento" le due sequenze.
Hamish Grubijan

1
@HamishGrubijan: per favore pubblicala come un'altra domanda
Quassnoi

2
@ Malkocoglu: otterrai NULL, no 0, se la tabella è vuota. Questo è vero per tutti i database.
Quassnoi

5
questo non troverà correttamente le lacune iniziali. se hai 3,4,5,6,8. questo codice riporterà 7, perché non ha nemmeno 1 con cui controllare. Quindi, se ti mancano i numeri di partenza, dovrai verificarlo.
ttomsen

12

Le tue risposte funzionano tutte bene se hai un primo valore id = 1, altrimenti questo divario non verrà rilevato. Ad esempio, se i valori dell'ID della tabella sono 3,4,5, le query restituiranno 6.

Ho fatto qualcosa di simile

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) 

Questo troverà il primo spazio vuoto. Se hai id 0, 2,3,4. La risposta è 1. Stavo cercando una risposta per trovare il divario più grande. Supponiamo che la sequenza sia 0,2,3,4, 100,101,102. Voglio trovare un intervallo di 4-99.
Kemin Zhou

8

Non esiste davvero un modo SQL estremamente standard per farlo, ma con una qualche forma di clausola limitante puoi farlo

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

(Server 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

(Oracolo)


se c'è un intervallo di spazi vuoti, verrà restituita solo la prima riga dell'intervallo per la query postgres.
John Haugeland

Questo ha più senso per me, l'uso di un join ti permetterà anche di cambiare il tuo valore TOP, per mostrare più risultati di gap.
AJ_

1
Grazie, funziona molto bene e se desideri vedere tutti i punti in cui c'è uno spazio vuoto, puoi rimuovere il limite.
mekbib

8

La prima cosa che mi è venuta in mente. Non sono sicuro che sia una buona idea andare in questo modo, ma dovrebbe funzionare. Supponiamo che la tabella sia te la colonna sia 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

Modifica: questo potrebbe essere un ticchettio più veloce (e più 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


ESTERNO SINISTRO UNISCI t ==> ESTERNO SINISTRO ISCRIVITI t2
Eamon Nerbonne

1
No-no, Eamon, LEFT OUTER JOING t2richiederebbe che tu abbia un t2tavolo, che è solo un alias.
Michael Krelin - hacker

6

Funziona in SQL Server: non è possibile testarlo su altri sistemi ma sembra standard ...

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

Puoi anche aggiungere un punto di partenza alla clausola where ...

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

Quindi se avessi 2000, 2001, 2002 e 2005 dove il 2003 e il 2004 non esistevano, tornerebbe il 2003.


3

La seguente soluzione:

  • fornisce dati di test;
  • una query interna che produce altre lacune; e
  • funziona in SQL Server 2012.

Numera le righe ordinate in modo sequenziale nella clausola " with " e quindi riutilizza il risultato due volte con un join interno sul numero di riga, ma sfalsato di 1 in modo da confrontare la riga prima con la riga dopo, cercando ID con uno spazio maggiore di 1. Più di quanto richiesto ma più ampiamente applicabile.

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 query interna produce:

gap_start   gap_end

3           3

9           11

La query esterna produce:

gap_start

3

2

Join interno a una vista o sequenza che ha tutti i valori possibili.

Nessun tavolo? Crea un tavolo. Tengo sempre un tavolo fittizio in giro solo per questo.

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

Poi,

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

2

Per PostgreSQL

Un esempio che fa uso di query ricorsive.

Questo potrebbe essere utile se vuoi trovare uno spazio in un intervallo specifico (funzionerà anche se la tabella è vuota, mentre gli altri esempi 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

1

La mia ipotesi:

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

Questo spiega tutto ciò che è stato menzionato finora. Include 0 come punto di partenza, che verrà impostato per impostazione predefinita se non esistono anche valori. Ho anche aggiunto le posizioni appropriate per le altre parti di una chiave multivalore. Questo è stato testato solo su 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

Ho scritto un modo veloce per farlo. Non sono sicuro che questo sia il più efficiente, ma porta a termine il lavoro. Nota che non ti dice il divario, ma ti dice l'id prima e dopo il divario (tieni presente che il divario potrebbe essere più valori, quindi ad esempio 1,2,4,7,11 ecc.)

Sto usando sqlite come esempio

Se questa è la struttura del tuo tavolo

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

e queste sono le tue righe

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

La domanda è

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

Ecco una soluzione SQL standard che viene eseguita su tutti i server di database senza modifiche:

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

Vedi in azione per;


0

Funziona anche per tabelle vuote o con valori negativi. Appena testato in 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 usi Firebird 3 questo è molto elegante e semplice:

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

Ho riscontrato che la maggior parte degli approcci funziona molto, molto lentamente mysql. Ecco la mia soluzione per mysql < 8.0. Testato su 1 milione di record con uno spazio vuoto verso la fine ~ 1 secondo al termine. Non sono sicuro che si adatti ad altri gusti 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 che la sequenza inizi da "1".

0

Se il tuo contatore parte da 1 e vuoi generare il primo numero di sequenza (1) quando è vuoto, ecco il pezzo di codice corretto dalla prima risposta valido per 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 i numeri nella colonna sono numeri interi positivi (a partire da 1), ecco come risolverlo facilmente. (supponendo che ID sia il nome della colonna)

    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

troverà spazi vuoti solo fino al numero di righe in "TABLE-NAME" come "SELECT ROW_NUMBER () OVER () AS NUM FROM" TABLE-NAME "" fornirà gli ID fino al numero di righe solo
vijay shanker
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.