简体   繁体   English

如何在使用 SQL 运行计数器时找到“差距”?

[英]How do I find a "gap" in running counter with SQL?

I'd like to find the first "gap" in a counter column in an SQL table.我想在 SQL 表的计数器列中找到第一个“间隙”。 For example, if there are values 1,2,4 and 5 I'd like to find out 3.例如,如果有值 1、2、4 和 5,我想找出 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.我当然可以按顺序获取值并手动进行检查,但我想知道是否有办法在 SQL 中执行此操作。

In addition, it should be quite standard SQL, working with different DBMSes.此外,它应该是非常标准的 SQL,可以使用不同的 DBMS。

In MySQL and PostgreSQL :MySQLPostgreSQL

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 :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 :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): ANSI (适用于任何地方,效率最低):

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

Your answers all work fine if you have a first value id = 1, otherwise this gap will not be detected.如果您的第一个值 id = 1,则您的答案一切正常,否则将无法检测到此差距。 For instance if your table id values are 3,4,5, your queries will return 6.例如,如果您的表 ID 值为 3、4、5,则您的查询将返回 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) 

There isn't really an extremely standard SQL way to do this, but with some form of limiting clause you can do确实没有一种非常标准的 SQL 方法可以做到这一点,但是您可以使用某种形式的限制子句

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) (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) (SQL 服务器)

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) (甲骨文)

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 :假设表是t列是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

This works in SQL Server - can't test it in other systems but it seems standard...这适用于 SQL Server - 无法在其他系统中测试它,但它似乎是标准的......

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...您还可以在 where 子句中添加一个起点...

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.因此,如果您有 2000、2001、2002 和 2005,而 2003 和 2004 不存在,则它将返回 2003。

The following solution:以下解决方案:

  • provides test data;提供测试数据;
  • an inner query that produces other gaps;产生其他差距的内部查询; and
  • it works in SQL Server 2012.它适用于 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.在 " with " 子句中按顺序对有序行进行编号,然后使用行号的内部联接重用结果两次,但偏移 1 以便比较之前的行和之后的行,查找间隔大于的 ID 1. 要求更高,但适用范围更广。

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

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

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.它包括 0 作为起点,如果不存在任何值,它将默认为 0。 I also added the appropriate locations for the other parts of a multi-value key.我还为多值键的其他部分添加了适当的位置。 This has only been tested on SQL Server.这仅在 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

For PostgreSQL对于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

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;

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)请注意,它不会告诉您差距,而是告诉您差距前后的 id(请记住,差距可能是多个值,例如 1,2,4,7,11 等)

I'm using sqlite as an example我以 sqlite 为例

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 https://gist.github.com/wkimeria/7787ffe84d1c54216f1b320996b17b7e

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

Here is standard a SQL solution that runs on all database servers with no change:这是一个标准的 SQL 解决方案,它可以在所有数据库服务器上运行而无需更改:

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;见于行动;

It works for empty tables or with negatives values as well.它适用于空表或负值。 Just tested in SQL Server 2012刚刚在 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

If You use Firebird 3 this is most elegant and simple:如果您使用 Firebird 3,这是最优雅和简单的:

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

Found most of approaches run very, very slow in mysql .发现大多数方法在mysql运行得非常非常慢。 Here is my solution for mysql < 8.0 .这是我对mysql < 8.0解决方案。 Tested on 1M records with a gap near the end ~ 1sec to finish.在 100 万条记录上进行了测试,在接近结束时有一个间隙 ~ 1 秒完成。 Not sure if it fits other SQL flavours.不确定它是否适合其他 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
I assume that sequence starts from `1`. 我假设序列从“1”开始。

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:如果您的计数器从 1 开始,并且您想在为空时生成第一个序列号 (1),这里是第一个对 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
     )  
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]

If the numbers in the column are positive integers (starting from 1) then here is how to solve it easily.如果列中的数字是正整数(从 1 开始),那么这里是如何轻松解决它。 (assuming ID is your column name) (假设 ID 是您的列名)

    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

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.子查询列出了 id 对,每个 id 的lowerbound列都小于upperbound列,然后使用GROUP BYMIN(m2.id)来减少无用记录的数量。
  • The outer query further removes the records where lowerbound is exactly upperbound - 1外部查询进一步删除lowerbound正好是upperbound - 1的记录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.我的查询没有(显式地)输出(YOUR_MIN_ID_VALUE, 89)的 2 条记录(YOUR_MIN_ID_VALUE, 89)(186, YOUR_MAX_ID_VALUE) ,这隐含地意味着到目前为止your_table尚未使用这两个范围中的任何数字。
> 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 |
+-------------------+-------------------+

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM