138

I'd like to find the first "gap" in a counter column in an SQL table. For example, if there are values 1,2,4 and 5 I'd like to find out 3.

I can of course get the values in order and go through it manually, but I'd like to know if there would be a way to do it in SQL.

In addition, it should be quite standard SQL, working with different DBMSes.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Touko
  • 11,359
  • 16
  • 75
  • 105
  • In Sql server 2008 and up you can use `LAG(id, 1, null)` function with `OVER (ORDER BY id)` clause. – ajeh Jan 23 '17 at 20:12

22 Answers22

231

In MySQL and 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 (works everywhere, least efficient):

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

Systems supporting sliding window functions:

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
  • 413,100
  • 91
  • 616
  • 614
  • 43
    @vulkanino: please ask them to preserve the indentation. Also please note that creative commons license requires you to tattoo my nick and the question `URL` as well, though it may be QR coded I think. – Quassnoi Dec 16 '11 at 17:28
  • 4
    This is great, but if I had `[1, 2, 11, 12]`, then this would find only `3`. What I'd love it to find is 3-10 instead - basically the beginning and the end of every gap. I understand that I might have to write my own python script that leverages SQL (in my case MySql), but it would be nice if SQL could get me closer to what I want (I have a table with 2 million rows that has gaps, so I will need to slice it into smaller pieces and run some SQL on it). I suppose I could run one query to find the start of a gap, then another to find the end of a gap, and them "merge sort" the two sequences. – Hamish Grubijan May 20 '13 at 21:12
  • 1
    @HamishGrubijan: please post it as another question – Quassnoi May 21 '13 at 06:00
  • @Quassnoi: Thank you for this snippet. I am curious, what happens when the table is empty ? I tried the ANSI version on PostgreSQL and I got 0. Is this true for all others ? – Malkocoglu Feb 20 '14 at 12:11
  • 2
    @Malkocoglu: you will get `NULL`, not `0`, if the table is empty. This is true for all databases. – Quassnoi Feb 20 '14 at 15:39
  • Second @Quassnoi 's comment about getting no results if the table is empty--in fact if you apply other criteria (in my case I'm looking for a free number in a subrange of numbers in the table) to where no existing rows meet the criteria, you can get no results back rather than the first free number. For my purposes I'll probably just build a special case in my non-SQL code that uses the first number in the range by default, but it's something to be aware of. – S'pht'Kr Apr 25 '14 at 11:37
  • @HamishGrubijan just drop the LIMIT condition at the end and it will return all of the gaps (just be aware that the "last" one returned is the "gap" after your last record - e.g. 13 in your example) – scunliffe Oct 12 '14 at 15:24
  • 6
    this will not find initial gaps properly. if you have 3,4,5,6,8. this code will report 7, because it has NO 1 to even check with. So if you are missing starting numbers you will have to check for that. – ttomsen Feb 11 '15 at 18:48
  • @ttomsen: not sure it would count as a gap, but good point nevertheless. – Quassnoi Feb 11 '15 at 21:16
  • this is not a correct answer. if there is no gap, it will show a record equal to lastid + 1. also it doesn't work on sequence like 1,2,4,5,8 it only finds the first gap – FLICKER Jun 10 '15 at 01:29
  • 1
    @MohammadSanati: the OP asked for the first gap – Quassnoi Jun 10 '15 at 04:33
  • The SQL Server query will return MAX(ID) + 1 which will never be part of sequence. I have edited the answer for SQL Server by adding a filter. – Reeya Oberoi Oct 10 '15 at 04:12
  • @ReeyaOberoi: 3 in OP's example is not a part of the sequence either – Quassnoi Oct 10 '15 at 10:59
  • @HamishGrubijan: See: How to find gaps in identity columns at the speed of light. https://thefirstsql.com/2011/07/08/how-to-find-gaps-in-identity-columns-at-the-speed-of-light/ – wwmbes Jun 08 '17 at 14:48
  • 1
    @wwmbes: I find it hard to argue with statements like "scales very well" and "find at the speed of light" when applied to databases, but the author's query would have to build 3 (three) temporary tables, each one proportional in size to the original one, and self-join the last of them. It does not conform with universally accepted definitions of "good scaling" or metaphorical "speed of light" – Quassnoi Jun 08 '17 at 17:22
  • The code in this answer will give you WILDLY incomplete results if your gaps are > 1 – GWR Dec 09 '17 at 23:18
  • Can anyone explain HOW this works? I don't fully understand the second `where` clause and also why the naming choice of `mo` and `mi`? I'm guessing `o` is for `original` but why `mi`? – gMale Apr 11 '19 at 14:58
  • @gMale: `o` and `i` stand for "outer" and "inner", this might not be the obvious choice but it works for me. The query basically says "look at all the records in `id` order and for each of them, see if the record with the next consecutive id exists. This means: for `id = 1` check if `id = 2` exists, for `id = 2` check if `id = 3` exists and so on. As soon as you find a record for which the next consecutive id does not exist, return its id plus one". For op's data that would mean "`id = 2` is the first record for which `id = 3` does not exist, so let's return `2 + 1 = 3`". – Quassnoi Apr 11 '19 at 16:31
  • Fiddle URL for PostgreSQL: https://www.db-fiddle.com/f/b2ujWsSsit3SAiAWpRKQYV/1 – Danton Heuer Nov 29 '22 at 19:52
14

Your answers all work fine if you have a first value id = 1, otherwise this gap will not be detected. For instance if your table id values are 3,4,5, your queries will return 6.

I did something like this

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
  • 141
  • 1
  • 2
  • This will find the first gap. If you have id 0, 2,3,4. The the answer is 1. I was look for an answer to find the largest gap. Say the sequence is 0,2,3,4, 100,101,102. I want to find 4-99 gap. – Kemin Zhou Oct 21 '19 at 03:57
10

There isn't really an extremely standard SQL way to do this, but with some form of limiting clause you can do

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)

or

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)

or

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

(Oracle)

chaos
  • 122,029
  • 33
  • 303
  • 309
  • if there's a gap range, only the first row in the range will be returned for your postgres query. – John Haugeland Jan 03 '17 at 05:09
  • This makes the most sense to me, using a join will also let you change your TOP value, to show more gap results. – AJ_ Mar 28 '19 at 13:43
  • 1
    Thanks, this works very well and if you would like to see all points where there is a gap, you can remove the limit. – mekbib.awoke Nov 14 '19 at 09:10
9

The first thing that came into my head. Not sure if it's a good idea to go this way at all, but should work. Suppose the table is t and the column is 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: This one may be a tick faster (and shorter!):

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
Peter Trcka
  • 1,279
  • 1
  • 16
  • 21
Michael Krelin - hacker
  • 138,757
  • 24
  • 193
  • 173
6

This works in SQL Server - can't test it in other systems but it seems standard...

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

You could also add a starting point to the where clause...

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

So if you had 2000, 2001, 2002, and 2005 where 2003 and 2004 didn't exist, it would return 2003.

Mayo
  • 10,544
  • 6
  • 45
  • 90
4

The following solution:

  • provides test data;
  • an inner query that produces other gaps; and
  • it works in SQL Server 2012.

Numbers the ordered rows sequentially in the "with" clause and then reuses the result twice with an inner join on the row number, but offset by 1 so as to compare the row before with the row after, looking for IDs with a gap greater than 1. More than asked for but more widely applicable.

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

The inner query produces:

gap_start   gap_end

3           3

9           11

The outer query produces:

gap_start

3
wwmbes
  • 301
  • 2
  • 4
2

Inner join to a view or sequence that has a all possible values.

No table? Make a table. I always keep a dummy table around just for this.

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

Then,

 select a.id from artificial_range a
 where not exists ( select * from your_table b
 where b.counter = a.id) ;
tpdi
  • 34,554
  • 11
  • 80
  • 120
2

This one accounts for everything mentioned so far. It includes 0 as a starting point, which it will default to if no values exist as well. I also added the appropriate locations for the other parts of a multi-value key. This has only been tested on 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
  • 11,857
  • 5
  • 62
  • 68
2

For PostgreSQL

An example that makes use of recursive query.

This might be useful if you want to find a gap in a specific range (it will work even if the table is empty, whereas the other examples will not)

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
pumbo
  • 3,646
  • 2
  • 25
  • 27
1

My guess:

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
  • 2,713
  • 1
  • 21
  • 24
1

I wrote up a quick way of doing it. Not sure this is the most efficient, but gets the job done. Note that it does not tell you the gap, but tells you the id before and after the gap (keep in mind the gap could be multiple values, so for example 1,2,4,7,11 etc)

I'm using sqlite as an example

If this is your table structure

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

and these are your rows

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

The query is

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

1

Here is an alternative to show the range of all possible gap values in portable and more compact way :

Assume your table schema looks like this :

> SELECT id FROM your_table;
+-----+
| id  |
+-----+
|  90 |
| 103 |
| 104 |
| 118 |
| 119 |
| 120 |
| 121 |
| 161 |
| 162 |
| 163 |
| 185 |
+-----+

To fetch the ranges of all possible gap values, you have the following query :

  • The subquery lists pairs of ids, each of which has the lowerbound column being smaller than upperbound column, then use GROUP BY and MIN(m2.id) to reduce number of useless records.
  • The outer query further removes the records where lowerbound is exactly upperbound - 1
  • My query doesn't (explicitly) output the 2 records (YOUR_MIN_ID_VALUE, 89) and (186, YOUR_MAX_ID_VALUE) at both ends, that implicitly means any number in both of the ranges hasn't been used in your_table so far.
> SELECT  m3.lowerbound + 1, m3.upperbound - 1 FROM
  (
    SELECT m1.id as lowerbound, MIN(m2.id) as upperbound FROM
    your_table m1 INNER JOIN your_table
    AS m2 ON m1.id < m2.id GROUP BY m1.id
  )
  m3 WHERE m3.lowerbound < m3.upperbound - 1;

+-------------------+-------------------+
| m3.lowerbound + 1 | m3.upperbound - 1 |
+-------------------+-------------------+
|                91 |               102 |
|               105 |               117 |
|               122 |               160 |
|               164 |               184 |
+-------------------+-------------------+

Ham
  • 703
  • 8
  • 17
0
select min([ColumnName]) from [TableName]
where [ColumnName]-1 not in (select [ColumnName] from [TableName])
and [ColumnName] <> (select min([ColumnName]) from [TableName])
Behnam
  • 1,039
  • 2
  • 14
  • 39
0

Here is standard a SQL solution that runs on all database servers with no change:

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

See in action for;

Mehmet Kaplan
  • 1,723
  • 2
  • 20
  • 43
0

It works for empty tables or with negatives values as well. Just tested 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
Horaciux
  • 6,322
  • 2
  • 22
  • 41
0

If You use Firebird 3 this is most elegant and 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
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
  • 86
  • 1
  • 7
0

Found most of approaches run very, very slow in mysql. Here is my solution for mysql < 8.0. Tested on 1M records with a gap near the end ~ 1sec to finish. Not sure if it fits other SQL flavours.

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
I assume that sequence starts from `1`.
Max Ivanov
  • 141
  • 2
  • 3
0

If your counter is starting from 1 and you want to generate first number of sequence (1) when empty, here is the corrected piece of code from first answer valid for 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
  • 26
  • 3
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
  • 39
  • 12
0

If the numbers in the column are positive integers (starting from 1) then here is how to solve it easily. (assuming ID is your column name)

    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
  • 1
  • 2
  • it will find gaps only till number of rows in 'TABLE-NAME' as "SELECT ROW_NUMBER() OVER () AS NUM FROM 'TABLE-NAME'" will give ids till number of rows only – vijay shanker Oct 31 '19 at 11:29
0

SELECT ID+1 FROM table WHERE ID+1 NOT IN (SELECT ID FROM table) ORDER BY 1;

khaled saleh
  • 470
  • 7
  • 18