Làm cách nào để tìm “lỗ hổng” trong việc chạy bộ đếm với SQL?


106

Tôi muốn tìm "khoảng trống" đầu tiên trong cột bộ đếm trong bảng SQL. Ví dụ, nếu có các giá trị 1,2,4 và 5, tôi muốn tìm ra 3.

Tất nhiên tôi có thể lấy các giá trị theo thứ tự và thực hiện theo cách thủ công, nhưng tôi muốn biết liệu có cách nào để làm điều đó trong SQL hay không.

Ngoài ra, nó phải là SQL khá chuẩn, hoạt động với các DBMS khác nhau.


Trong Sql server 2008 trở lên, bạn có thể sử dụng LAG(id, 1, null)hàm với OVER (ORDER BY id)mệnh đề.
ajeh

Câu trả lời:


184

Trong 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

Trong 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

Trong 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 (hoạt động ở mọi nơi, kém hiệu quả nhất):

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

Hệ thống hỗ trợ chức năng cửa sổ trượt:

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: làm ơn yêu cầu họ giữ lại vết lõm. Cũng xin lưu ý rằng giấy phép commons sáng tạo yêu cầu bạn xăm nick của tôi và cả câu hỏi URL, mặc dù tôi nghĩ nó có thể được mã hóa QR.
Quassnoi

4
Điều này thật tuyệt, nhưng nếu tôi có [1, 2, 11, 12], thì điều này sẽ chỉ tìm thấy 3. Thay vào đó, những gì tôi muốn tìm là 3-10 - về cơ bản là điểm bắt đầu và kết thúc của mọi khoảng cách. Tôi hiểu rằng tôi có thể phải viết tập lệnh python của riêng mình để sử dụng SQL (trong trường hợp của tôi là MySql), nhưng sẽ rất tuyệt nếu SQL có thể đưa tôi đến gần hơn những gì tôi muốn (tôi có một bảng với 2 triệu hàng có khoảng trống, vì vậy tôi sẽ cần phải cắt nó thành nhiều phần nhỏ hơn và chạy một số SQL trên đó). Tôi cho rằng tôi có thể chạy một truy vấn để tìm điểm bắt đầu của một khoảng trống, sau đó một truy vấn khác để tìm điểm kết thúc của một khoảng trống và chúng "sắp xếp hợp nhất" hai chuỗi.
Hamish Grubijan

1
@HamishGrubijan: vui lòng đăng nó như một câu hỏi khác
Quassnoi

2
@Malkocoglu: bạn sẽ nhận được NULL, không phải 0, nếu bảng trống. Điều này đúng cho tất cả các cơ sở dữ liệu.
Quassnoi

5
điều này sẽ không tìm thấy khoảng trống ban đầu đúng cách. nếu bạn có 3,4,5,6,8. mã này sẽ báo cáo 7, bởi vì nó KHÔNG CÓ 1 để kiểm tra. Vì vậy, nếu bạn thiếu số bắt đầu, bạn sẽ phải kiểm tra điều đó.
ttomsen

12

Tất cả các câu trả lời của bạn đều hoạt động tốt nếu bạn có giá trị đầu tiên id = 1, nếu không khoảng cách này sẽ không được phát hiện. Ví dụ: nếu giá trị id bảng của bạn là 3,4,5, các truy vấn của bạn sẽ trả về 6.

Tôi đã làm một cái gì đó như thế này

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) 

Điều này sẽ tìm ra khoảng trống đầu tiên. Nếu bạn có id 0, 2,3,4. Câu trả lời là 1. Tôi đã tìm kiếm một câu trả lời để tìm ra khoảng cách lớn nhất. Giả sử dãy số là 0,2,3,4, 100,101,102. Tôi muốn tìm khoảng cách 4-99.
Kemin Zhou

8

Không thực sự có một cách SQL cực kỳ chuẩn để làm điều này, nhưng với một số dạng điều khoản giới hạn bạn có thể làm

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)

hoặc là

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

(Máy chủ SQL)

hoặc là

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)


nếu có phạm vi khoảng cách, chỉ hàng đầu tiên trong phạm vi sẽ được trả lại cho truy vấn postgres của bạn.
John Haugeland

Điều này có ý nghĩa nhất đối với tôi, sử dụng phép nối cũng sẽ cho phép bạn thay đổi giá trị TOP của mình, để hiển thị nhiều kết quả chênh lệch hơn.
AJ_ Ngày

1
Cảm ơn, điều này hoạt động rất tốt và nếu bạn muốn xem tất cả các điểm có khoảng cách, bạn có thể xóa giới hạn.
mekbib.awoke

8

Điều đầu tiên xuất hiện trong đầu tôi. Không chắc có nên đi theo cách này hay không, nhưng sẽ hiệu quả. Giả sử bảng là tvà cột là 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

Chỉnh sửa: Cái này có thể nhanh hơn (và ngắn hơn!):

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


THAM GIA
BÊN NGOÀI

1
Không-không, Eamon, LEFT OUTER JOING t2sẽ yêu cầu bạn phải có t2bảng, đó chỉ là một bí danh.
Michael Krelin - hacker

6

Điều này hoạt động trong SQL Server - không thể kiểm tra nó trong các hệ thống khác nhưng nó có vẻ chuẩn ...

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

Bạn cũng có thể thêm một điểm bắt đầu vào mệnh đề where ...

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

Vì vậy, nếu bạn có 2000, 2001, 2002 và 2005 mà 2003 và 2004 không tồn tại, nó sẽ trả về 2003.


3

Giải pháp sau:

  • cung cấp dữ liệu thử nghiệm;
  • một truy vấn bên trong tạo ra các khoảng trống khác; và
  • nó hoạt động trong SQL Server 2012.

Đánh số thứ tự các hàng được sắp xếp theo thứ tự trong mệnh đề " with " và sau đó sử dụng lại kết quả hai lần với một phép nối bên trong trên số hàng, nhưng bù lại bằng 1 để so sánh hàng trước với hàng sau, tìm kiếm các ID có khoảng cách lớn hơn 1. Nhiều hơn yêu cầu nhưng áp dụng rộng rãi hơ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

Truy vấn bên trong tạo ra:

gap_start   gap_end

3           3

9           11

Truy vấn bên ngoài tạo ra:

gap_start

3

2

Nối bên trong với một dạng xem hoặc chuỗi có tất cả các giá trị có thể.

Không có bàn? Làm một cái bàn. Tôi luôn giữ một chiếc bàn giả chỉ để làm việc này.

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

Sau đó,

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

2

Đối với PostgreSQL

Một ví dụ sử dụng truy vấn đệ quy.

Điều này có thể hữu ích nếu bạn muốn tìm một khoảng trống trong một phạm vi cụ thể (nó sẽ hoạt động ngay cả khi bảng trống, trong khi các ví dụ khác thì không)

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

Tôi đoán:

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

Điều này giải thích cho mọi thứ được đề cập cho đến nay. Nó bao gồm 0 như một điểm bắt đầu, nó sẽ mặc định là nếu không có giá trị nào tồn tại. Tôi cũng đã thêm các vị trí thích hợp cho các phần khác của khóa đa giá trị. Điều này chỉ được thử nghiệm trên 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

Tôi đã viết ra một cách nhanh chóng để làm điều đó. Không chắc điều này là hiệu quả nhất, nhưng hãy hoàn thành công việc. Lưu ý rằng nó không cho bạn biết khoảng trống, nhưng cho bạn biết id trước và sau khoảng trống (hãy nhớ rằng khoảng cách có thể là nhiều giá trị, vì vậy, ví dụ 1,2,4,7,11, v.v.)

Tôi đang sử dụng sqlite làm ví dụ

Nếu đây là cấu trúc bảng của bạn

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

và đây là những hàng của bạn

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

Truy vấn là

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

Đây là giải pháp SQL tiêu chuẩn chạy trên tất cả các máy chủ cơ sở dữ liệu mà không thay đổi:

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

Xem trong hành động cho;


0

Nó cũng hoạt động cho các bảng trống hoặc với các giá trị phủ định. Vừa được thử nghiệm trong 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

Nếu bạn sử dụng Firebird 3, điều này là thanh lịch và đơn giản nhất:

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

Tìm thấy hầu hết các cách tiếp cận chạy rất, rất chậm mysql. Đây là giải pháp của tôi cho mysql < 8.0. Đã thử nghiệm trên 1M bản ghi với khoảng cách gần cuối ~ 1 giây để kết thúc. Không chắc liệu nó có phù hợp với các phiên bản SQL khác hay không.

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
Tôi giả sử rằng trình tự bắt đầu từ `1`.

0

Nếu bộ đếm của bạn bắt đầu từ 1 và bạn muốn tạo số đầu tiên của chuỗi (1) khi trống, đây là đoạn mã đã sửa từ câu trả lời đầu tiên hợp lệ cho 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

Nếu các số trong cột là số nguyên dương (bắt đầu từ 1) thì đây là cách giải quyết dễ dàng. (giả sử ID là tên cột của bạn)

    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

nó sẽ tìm thấy những khoảng trống chỉ đến số hàng trong 'TABLE-NAME' như "SELECT ROW_NUMBER () OVER () AS NUM TỪ 'TABLE-NAME'" sẽ cung cấp cho id đến số hàng chỉ
vijay Shanker
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.