Wie finde ich eine "Lücke" beim Ausführen von Zählern mit SQL?


106

Ich möchte die erste "Lücke" in einer Zählerspalte in einer SQL-Tabelle finden. Wenn es zum Beispiel die Werte 1,2,4 und 5 gibt, würde ich gerne 3 herausfinden.

Ich kann die Werte natürlich in Ordnung bringen und manuell durchgehen, aber ich würde gerne wissen, ob es eine Möglichkeit gibt, dies in SQL zu tun.

Darüber hinaus sollte es sich um Standard-SQL handeln, das mit verschiedenen DBMS arbeitet.


In SQL Server 2008 und höher können Sie die LAG(id, 1, null)Funktion mit OVER (ORDER BY id)Klausel verwenden.
Ajeh

Antworten:


184

In MySQLund 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 (funktioniert überall, am wenigsten effizient):

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

Systeme, die Schiebefensterfunktionen unterstützen:

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: Bitte bitten Sie sie, die Einrückung beizubehalten. Bitte beachten Sie auch, dass Sie für die Creative-Commons-Lizenz meinen Nick und die Frage tätowieren müssen URL, obwohl sie meiner Meinung nach möglicherweise QR-codiert sind.
Quassnoi

4
Das ist großartig, aber wenn ich es hätte [1, 2, 11, 12], dann würde das nur finden 3. Was ich gerne finden würde, ist stattdessen 3-10 - im Grunde der Anfang und das Ende jeder Lücke. Ich verstehe, dass ich möglicherweise mein eigenes Python-Skript schreiben muss, das SQL nutzt (in meinem Fall MySql), aber es wäre schön, wenn SQL mich näher an das bringen könnte, was ich will (ich habe eine Tabelle mit 2 Millionen Zeilen, die Lücken aufweist). Also muss ich es in kleinere Teile schneiden und etwas SQL darauf ausführen. Ich nehme an, ich könnte eine Abfrage ausführen, um den Anfang einer Lücke zu finden, und eine andere, um das Ende einer Lücke zu finden, und sie könnten die beiden Sequenzen "zusammenführen, sortieren".
Hamish Grubijan

1
@ HamishGrubijan: Bitte posten Sie es als eine andere Frage
Quassnoi

2
@Malkocoglu: Sie erhalten NULLnicht 0, wenn die Tabelle leer ist. Dies gilt für alle Datenbanken.
Quassnoi

5
Dadurch werden die anfänglichen Lücken nicht richtig gefunden. wenn Sie 3,4,5,6,8 haben. Dieser Code meldet 7, da er NO 1 hat, mit dem er sich sogar befassen kann. Wenn Sie also Startnummern vermissen, müssen Sie dies überprüfen.
Tomsen

12

Ihre Antworten funktionieren alle einwandfrei, wenn Sie einen ersten Wert id = 1 haben. Andernfalls wird diese Lücke nicht erkannt. Wenn Ihre Tabellen-ID beispielsweise 3,4,5 beträgt, geben Ihre Abfragen 6 zurück.

Ich habe so etwas gemacht

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) 

Dies wird die erste Lücke finden. Wenn Sie die ID 0 haben, 2,3,4. Die Antwort ist 1. Ich habe nach einer Antwort gesucht, um die größte Lücke zu finden. Angenommen, die Sequenz ist 0,2,3,4, 100,101,102. Ich möchte 4-99 Lücke finden.
Kemin Zhou

8

Es gibt nicht wirklich eine extrem standardmäßige SQL-Methode, um dies zu tun, aber mit irgendeiner Form von Begrenzungsklausel können Sie dies tun

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)

oder

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)

oder

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

(Orakel)


Wenn es einen Lückenbereich gibt, wird nur die erste Zeile im Bereich für Ihre Postgres-Abfrage zurückgegeben.
John Haugeland

Dies ist für mich am sinnvollsten. Wenn Sie einen Join verwenden, können Sie auch Ihren TOP-Wert ändern, um mehr Lückenergebnisse anzuzeigen.
AJ_

1
Danke, das funktioniert sehr gut und wenn Sie alle Punkte sehen möchten, an denen es eine Lücke gibt, können Sie das Limit entfernen.
mekbib.awoke

8

Das erste, was mir in den Sinn kam. Ich bin mir nicht sicher, ob es eine gute Idee ist, diesen Weg überhaupt zu gehen, sollte aber funktionieren. Angenommen, die Tabelle ist tund die Spalte ist 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

Bearbeiten: Dies kann ein Tick schneller (und kürzer!) Sein:

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


LEFT OUTER JOIN t ==> LINKS OUTER JOIN t2
Eamon Nerbonne

1
Nein, nein, Eamon, Sie LEFT OUTER JOING t2müssten einen t2Tisch haben, der nur ein Alias ​​ist.
Michael Krelin - Hacker

6

Dies funktioniert in SQL Server - kann nicht in anderen Systemen getestet werden, scheint aber Standard zu sein ...

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

Sie können der where-Klausel auch einen Ausgangspunkt hinzufügen ...

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

Wenn Sie also 2000, 2001, 2002 und 2005 hätten, wo 2003 und 2004 nicht existierten, würde es 2003 zurückgeben.


3

Die folgende Lösung:

  • liefert Testdaten;
  • eine innere Abfrage, die andere Lücken erzeugt; und
  • Es funktioniert in SQL Server 2012.

Nummeriert die geordneten Zeilen nacheinander in der " with " -Klausel und verwendet das Ergebnis dann zweimal mit einem inneren Join für die Zeilennummer, jedoch versetzt um 1, um die vorherige Zeile mit der nachfolgenden Zeile zu vergleichen und nach IDs mit einer Lücke größer als zu suchen 1. Mehr als gewünscht, aber allgemeiner anwendbar.

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

Die innere Abfrage erzeugt:

gap_start   gap_end

3           3

9           11

Die äußere Abfrage erzeugt:

gap_start

3

2

Innerer Join zu einer Ansicht oder Sequenz, die alle möglichen Werte enthält.

Kein Tisch? Mach einen Tisch. Ich habe immer einen Dummy-Tisch dafür.

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

Dann,

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

2

Zum PostgreSQL

Ein Beispiel, das rekursive Abfragen verwendet.

Dies kann nützlich sein, wenn Sie eine Lücke in einem bestimmten Bereich suchen möchten (dies funktioniert auch, wenn die Tabelle leer ist, während die anderen Beispiele dies nicht tun).

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

Meine Vermutung:

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

Dieser erklärt alles, was bisher erwähnt wurde. Es enthält 0 als Ausgangspunkt, auf den standardmäßig gesetzt wird, wenn ebenfalls keine Werte vorhanden sind. Ich habe auch die entsprechenden Positionen für die anderen Teile eines mehrwertigen Schlüssels hinzugefügt. Dies wurde nur auf SQL Server getestet.

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

Ich habe einen schnellen Weg dazu geschrieben. Ich bin mir nicht sicher, ob dies am effizientesten ist, erledigt aber die Arbeit. Beachten Sie, dass es Ihnen nicht die Lücke, sondern die ID vor und nach der Lücke angibt (denken Sie daran, dass die Lücke mehrere Werte haben kann, z. B. 1,2,4,7,11 usw.).

Ich verwende SQLite als Beispiel

Wenn dies Ihre Tabellenstruktur ist

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

und das sind deine Zeilen

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

Die Abfrage ist

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

Hier ist standardmäßig eine SQL-Lösung, die ohne Änderung auf allen Datenbankservern ausgeführt wird:

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

Siehe in Aktion für;


0

Es funktioniert auch für leere Tabellen oder mit negativen Werten. Gerade in SQL Server 2012 getestet

 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

Wenn Sie Firebird 3 verwenden, ist dies am elegantesten und einfachsten:

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

Die meisten Ansätze laufen sehr, sehr langsam ab mysql. Hier ist meine Lösung für mysql < 8.0. Getestet an 1M-Datensätzen mit einer Lücke gegen Ende ~ 1 Sekunde bis zum Ende. Ich bin mir nicht sicher, ob es zu anderen SQL-Varianten passt.

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
Ich gehe davon aus, dass die Sequenz bei "1" beginnt.

0

Wenn Ihr Zähler bei 1 beginnt und Sie im leeren Zustand die erste Sequenznummer (1) generieren möchten, finden Sie hier den korrigierten Code aus der ersten Antwort, der für Oracle gültig ist:

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

Wenn die Zahlen in der Spalte positive ganze Zahlen sind (beginnend mit 1), können Sie dies auf einfache Weise lösen. (vorausgesetzt, ID ist Ihr Spaltenname)

    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

Es werden nur Lücken gefunden, bis die Anzahl der Zeilen in 'TABLE-NAME' als "SELECT ROW_NUMBER () OVER () AS NUM FROM 'TABLE-NAME'" IDs bis zur Anzahl der Zeilen nur gibt
vijay shanker
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.