Je voudrais trouver le premier "écart" dans une colonne de compteur dans une table SQL. Par exemple, s’il existe des valeurs 1, 2, 4 et 5, je voudrais savoir 3.
Je peux bien sûr mettre les valeurs en ordre et les parcourir manuellement, mais j'aimerais savoir s'il serait possible de le faire en SQL.
De plus, il devrait s'agir de SQL assez standard, fonctionnant avec différents SGBD.
Dans MySQL
et 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être coulissante:
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
Vos réponses fonctionnent toutes correctement si vous avez une première valeur id = 1, sinon cet espace ne sera pas détecté. Par exemple, si les valeurs de votre identifiant 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)
La première chose qui me vint à l’esprit. Je ne sais pas si c'est une bonne idée de suivre cette voie, mais cela devrait fonctionner. Supposons que la table est t
et 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 une tique plus rapide (et plus courte!):
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
Cela fonctionne sous SQL Server - impossible de le tester sur d'autres systèmes, mais cela semble être la norme ...
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 alors que 2003 et 2004 n'existaient pas, il renverrait 2003.
Il n’existe pas vraiment de méthode SQL standard extrêmement pour le faire, mais vous pouvez le faire avec une forme quelconque
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)
Jointure interne à une vue ou à une séquence contenant toutes les valeurs possibles.
Pas de table? Faire 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) ;
Pour PostgreSQL
Un exemple qui utilise une requête récursive.
Cela peut être utile si vous voulez trouver un espace dans une plage spécifique .__
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
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;
Celui-ci représente tout ce qui a été mentionné jusqu'à présent. Il inclut 0 comme point de départ, auquel il sera appliqué 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
J'ai écrit un moyen rapide de le faire. Pas sûr que ce soit le plus efficace, mais fait le travail. Notez que cela ne vous dit pas l’écart, mais l’identifiant avant et après l’écart (gardez à l’esprit que l’écart peut être composé de plusieurs valeurs, par exemple 1,2,4,7,11, etc.)
J'utilise sqlite comme exemple
Si c'est la structure de votre table
create table sequential(id int not null, name varchar(10) null);
et ce sont vos lignes
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
Si votre compteur commence à 1 et que vous voulez 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
)
La solution suivante:
Numérote les lignes ordonnées de manière séquentielle dans la clause " avec " puis réutilise le résultat deux fois avec une jointure interne sur le numéro de la ligne, mais décalé de 1 afin de comparer la ligne précédente avec la ligne suivante, 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
Cela fonctionne pour les tables vides ou avec des valeurs négatives. Vient de tester 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
Nous avons trouvé que la plupart des approches fonctionnent très, très lentement dans mysql
. Voici ma solution pour mysql < 8.0
. Testé sur des enregistrements 1M avec un écart près de la fin ~ 1s pour finir. Vous ne savez pas si cela convient aux autres versions de 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
select min([ColumnName]) from [TableName]
where [ColumnName]-1 not in (select [ColumnName] from [TableName])
and [ColumnName] <> (select min([ColumnName]) from [TableName])
Si vous utilisez Firebird 3, ceci est très élégant et 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
-- 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
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;