Comment trouver une «lacune» dans l'exécution du compteur avec SQL?


106

Je voudrais trouver le premier "écart" dans une colonne de compteur dans une table SQL. Par exemple, s'il y a des valeurs 1, 2, 4 et 5, j'aimerais en savoir 3.

Je peux bien sûr mettre les valeurs dans l'ordre et les parcourir manuellement, mais j'aimerais savoir s'il y aurait un moyen de le faire en SQL.

De plus, cela devrait être du SQL assez standard, fonctionnant avec différents SGBD.


Dans Sql Server 2008 et plus, vous pouvez utiliser la LAG(id, 1, null)fonction avec OVER (ORDER BY id)clause.
ajeh

Réponses:


185

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

Dans 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

Dans 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 (fonctionne partout, moins efficace):

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

Systèmes prenant en charge les fonctions de fenêtres coulissantes:

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

40
@vulkanino: veuillez leur demander de conserver l'indentation. Veuillez également noter que la licence Creative Commons vous oblige à tatouer mon pseudo et la question URLégalement, bien qu'elle puisse être codée QR, je pense.
Quassnoi

4
C'est génial, mais si je l'avais fait [1, 2, 11, 12], ce serait seulement trouver 3. Ce que j'aimerais qu'il trouve, c'est 3-10 à la place - essentiellement le début et la fin de chaque écart. Je comprends que je devrais peut-être écrire mon propre script python qui exploite SQL (dans mon cas MySql), mais ce serait bien si SQL pouvait me rapprocher de ce que je veux (j'ai une table avec 2 millions de lignes qui a des lacunes, je vais donc devoir le découper en petits morceaux et exécuter du SQL dessus). Je suppose que je pourrais exécuter une requête pour trouver le début d'un intervalle, puis une autre pour trouver la fin d'un intervalle, et les "fusionner le tri" des deux séquences.
Hamish Grubijan

1
@HamishGrubijan: postez-la comme une autre question
Quassnoi

2
@Malkocoglu: vous obtiendrez NULL, non 0, si la table est vide. Cela est vrai pour toutes les bases de données.
Quassnoi

5
cela ne trouvera pas correctement les lacunes initiales. si vous avez 3,4,5,6,8. ce code rapportera 7, car il n'a même pas 1 pour vérifier. Donc, s'il vous manque des numéros de départ, vous devrez vérifier cela.
ttomsen

12

Vos réponses fonctionnent toutes bien si vous avez une première valeur id = 1, sinon cet écart ne sera pas détecté. Par exemple, si vos valeurs d'ID de table sont 3,4,5, vos requêtes renverront 6.

J'ai fait quelque chose comme ça

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) 

Cela trouvera le premier écart. Si vous avez l'id 0, 2,3,4. La réponse est 1. J'étais à la recherche d'une réponse pour trouver le plus grand écart. Supposons que la séquence soit 0,2,3,4, 100,101,102. Je veux trouver un écart de 4 à 99.
Kemin Zhou

8

Il n'y a pas vraiment de méthode SQL extrêmement standard pour ce faire, mais avec une forme de clause limitative, vous pouvez le faire

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)

ou

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

(Serveur SQL)

ou

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)


s'il y a une plage d'espacement, seule la première ligne de la plage sera renvoyée pour votre requête postgres.
John Haugeland

Cela me semble le plus logique, l'utilisation d'une jointure vous permettra également de modifier votre valeur TOP, pour afficher plus de résultats d'écart.
AJ_

1
Merci, cela fonctionne très bien et si vous souhaitez voir tous les points où il y a un écart, vous pouvez supprimer la limite.
mekbib.awoke

8

La première chose qui m'est venue à l'esprit. Je ne sais pas si c'est une bonne idée d'aller dans cette direction, mais cela devrait fonctionner. Supposons que la table est tet que la colonne est 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: Celui-ci peut être un tick plus rapide (et plus court!):

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


GAUCHE OUTER JOIN t ==> GAUCHE OUTER JOIN t2
Eamon Nerbonne

1
Non-non, Eamon, LEFT OUTER JOING t2vous obligerait à avoir t2table, qui est juste un alias.
Michael Krelin - hacker

6

Cela fonctionne dans SQL Server - impossible de le tester sur d'autres systèmes mais cela semble standard ...

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

Vous pouvez également ajouter un point de départ à la 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

Donc, si vous aviez 2000, 2001, 2002 et 2005 où 2003 et 2004 n'existaient pas, cela renverrait 2003.


3

La solution suivante:

  • fournit des données de test;
  • une requête interne qui produit d'autres lacunes; et
  • cela fonctionne dans SQL Server 2012.

Numérote les lignes ordonnées de manière séquentielle dans la clause " with ", puis réutilise le résultat deux fois avec une jointure interne sur le numéro de ligne, mais décalée de 1 afin de comparer la ligne avant avec la ligne après, en recherchant les ID avec un écart supérieur à 1. Plus que demandé mais plus largement 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

La requête interne produit:

gap_start   gap_end

3           3

9           11

La requête externe produit:

gap_start

3

2

Jointure interne à une vue ou une séquence qui a toutes les valeurs possibles.

Pas de table? Faites une table. Je garde toujours une table factice juste pour ça.

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

Ensuite,

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

2

Pour PostgreSQL

Un exemple qui utilise une requête récursive.

Cela peut être utile si vous souhaitez trouver un espace dans une plage spécifique (cela fonctionnera même si la table est vide, contrairement aux autres exemples)

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

Ma conjecture:

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

Celui-ci explique tout ce qui a été mentionné jusqu'à présent. Il inclut 0 comme point de départ, auquel il sera par défaut si aucune valeur n'existe également. J'ai également ajouté les emplacements appropriés pour les autres parties d'une clé à valeurs multiples. Cela n'a été testé que sur 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

J'ai écrit un moyen rapide de le faire. Pas sûr que ce soit le plus efficace, mais fait le travail. Notez qu'il ne vous indique pas l'écart, mais vous indique l'id avant et après l'écart (gardez à l'esprit que l'écart peut être plusieurs valeurs, par exemple 1,2,4,7,11, etc.)

J'utilise sqlite comme exemple

S'il s'agit de votre structure de table

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

et ce sont tes rangs

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

La requête est

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

Voici une solution SQL standard qui s'exécute sur tous les serveurs de base de données sans changement:

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

Voir en action pour;


0

Cela fonctionne également pour les tables vides ou avec des valeurs négatives. Juste testé dans 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

Si vous utilisez Firebird 3, c'est le plus élégant et le plus 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

0

J'ai trouvé que la plupart des approches fonctionnent très, très lentement mysql. Voici ma solution pour mysql < 8.0. Testé sur 1M d'enregistrements avec un écart près de la fin ~ 1sec pour terminer. Je ne sais pas si cela correspond à d'autres saveurs 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
Je suppose que la séquence commence à partir de «1».

0

Si votre compteur est à partir de 1 et que vous souhaitez générer le premier numéro de séquence (1) lorsqu'il est vide, voici le morceau de code corrigé de la première réponse valide pour 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

Si les nombres de la colonne sont des entiers positifs (à partir de 1), voici comment le résoudre facilement. (en supposant que l'ID est le nom de votre colonne)

    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

il trouvera des espaces seulement jusqu'au nombre de lignes dans 'TABLE-NAME' comme "SELECT ROW_NUMBER () OVER () AS NUM FROM 'TABLE-NAME'" donnera les identifiants jusqu'au nombre de lignes seulement
vijay shanker
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.