2009-03-26 5 views
34

Comment trouvez-vous le plus petit numéro inutilisé dans une colonne SQL Server?Trouvez le plus petit numéro inutilisé dans SQL Server

Je suis sur le point d'importer un grand nombre d'enregistrements enregistrés manuellement à partir d'Excel dans une table SQL Server. Ils ont tous un numéro numérique (appelé numéro de document), mais ils n'ont pas été attribués séquentiellement pour des raisons qui ne s'appliquent plus, ce qui signifie que mon site web enregistre un nouvel enregistrement, il doit lui assigner le plus petit numéro de document possible (supérieur à zéro) qui n'a pas déjà été pris.

Existe-t-il un moyen de le faire par le langage SQL ou est-ce un problème pour TSQL/code?

Merci!

EDIT

grâce à WW pour soulever la question de la concurrence spéciales. Étant donné qu'il s'agit d'une application Web, elle est multi-threadée par définition et toute personne confrontée à ce même problème doit envisager un code ou un verrou de niveau DB pour éviter un conflit.

LINQ

Pour votre information - cela peut être accompli par LINQ avec le code suivant:

var nums = new [] { 1,2,3,4,6,7,9,10}; 

int nextNewNum = (
    from n in nums 
    where !nums.Select(nu => nu).Contains(n + 1) 
    orderby n 
    select n + 1 
).First(); 

nextNewNum == 5

Répondre

48

Trouver la première ligne où il n'existe pas de ligne Id + 1

SELECT TOP 1 t1.Id+1 
FROM table t1 
WHERE NOT EXISTS(SELECT * FROM table t2 WHERE t2.Id = t1.Id + 1) 
ORDER BY t1.Id 

Edit:

Pour gérer le cas particulier où l'est pas 1 id le plus bas existant, voici une vilaine solution:

SELECT TOP 1 * FROM (
    SELECT t1.Id+1 AS Id 
    FROM table t1 
    WHERE NOT EXISTS(SELECT * FROM table t2 WHERE t2.Id = t1.Id + 1) 
    UNION 
    SELECT 1 AS Id 
    WHERE NOT EXISTS (SELECT * FROM table t3 WHERE t3.Id = 1)) ot 
ORDER BY 1 
+0

Cela manquera tout bloc contigu d'identifiants commençant au début de la plage. Par exemple, si 'table' a des identifiants (5,6,8,9,10) cela retournera 7, pas n'importe lequel de 1-4. – joshperry

+0

@joshperry Vous avez raison. J'ai manqué le commentaire à propos de vouloir remplir tous les id plus grand que zéro. J'ai ajouté une solution laide. Peut-être que quelqu'un va suggérer une amélioration. –

+0

+1 Très utile, merci! La plupart des autres réponses ici ont été «vous n'avez pas besoin de faire cela, laissez le système incrémenter la clé», mais mon cas n'est pas pour la clé primaire mais plutôt un autre champ numérique unique où les emplacements vides sont un problème mais peuvent se produire, et non à la suite d'une suppression. –

2

Y at-il une raison pour laquelle il doit être le plus petit nombre possible? Pourquoi avez-vous besoin de remplir les trous?

Modifiez pour ajouter une réponse, car il s'agit d'une règle métier.

DECLARE @counter int 
DECLARE @max 
SET @counter = 0 
SET @max = SELECT MAX(Id) FROM YourTable 
WHILE @counter <= @max 
BEGIN 
    SET @counter = @counter + 1 
    IF NOT EXISTS (SELECT Id FROM YourTable WHERE Id = @counter) 
     BREAK 
    END 
END 

(je n'ai pas db à portée de main, donc ce ne peut être précis à 100%, mais vous devriez être en mesure d'obtenir à partir de là)

+0

Il est une règle d'entreprise. Ces numéros de documents sont ensuite distribués aux utilisateurs et effectivement utilisés. J'ai posé la même question, mais ils restent fermes sur ce point. :) –

+0

C'est dommage ... La seule façon que je connaisse serait de les parcourir tous jusqu'à ce que vous trouviez un ID inutilisé. Désolé pour ta chance. –

3

S'il y a des lacunes dans la séquence, vous peut trouver le premier écart avec quelque chose comme ceci:

select top 1 (found.id + 1) nextid from (select id from items union select 0) found 
    where not exists (select * from items blocking 
          where blocking.id = found.id + 1) 
    order by nextid asc 

En d'autres termes, trouver le moins ID dont le successeur n'existe pas, et retourner ce successeur. S'il n'y a pas d'espace, il renvoie un plus grand que le plus grand ID existant. Un identifiant d'espace réservé de 0 est inséré pour s'assurer que les ID commençant par 1 sont pris en compte.

Notez que cela prendra au moins n heure n log.

Microsoft SQL autorise l'utilisation d'une clause from dans une instruction insert, vous n'avez donc pas besoin de recourir au code de procédure.

11

Si vous les triez par ID numérique, le numéro que vous recherchez sera le premier pour lequel la fonction ROW_NUMBER() ne correspond pas à l'ID.

+0

+1 bonne astuce spécifique à SQL Server. Est-ce que cela pourrait être fait dans un sous-select pour sélectionner le premier non-correspondant, puis unionné avec max (id) +1 pour le faire en une fois? – bobince

9
SELECT TOP 1 t1.id+1 
FROM mytable t1 
LEFT OUTER JOIN mytable t2 ON (t1.id + 1 = t2.id) 
WHERE t2.id IS NULL 
ORDER BY t1.id; 

Ceci est une alternative aux réponses à l'aide donnée par les sous-requêtes corrélées @Jeffr ey Hantlin et @Darrel Miller. Toutefois, la politique que vous décrivez n'est vraiment pas une bonne idée. Les valeurs d'ID doivent être uniques, mais ne doivent pas nécessairement être consécutives. Que se passe-t-il si vous envoyez un lien vers le document n ° 42 par e-mail à quelqu'un, puis supprimez le document? Plus tard, vous réutilisez l'id # 42 pour un nouveau document. Maintenant, le destinataire de l'email suivra le lien vers le mauvais document!

+0

J'avoue que cela ne trouve pas une valeur manquante de 1. Cependant, c'est un problème bidon, ce qui est mon vrai point, donc je ne suis pas intéressé à trouver une solution! :-P –

+0

Les numéros de document ne sont jamais supprimés. Je suis d'accord avec vous, cependant, que c'est une mauvaise façon d'identifier les documents. Je choisis mes batailles, et il y a de plus gros poissons à frire. –

12

Aucune mention de verrouillage ou de concurrence dans aucune des réponses jusqu'à présent.

Tenir compte de ces deux utilisateurs d'ajouter un document à peu près en même temps: -

User 1    User 2 
Find Id    
         Find Id 
Id = 42    
         Id = 42 
Insert (42..) 
         Insert (42..) 
         Error! 

Vous devez soit: a) gérer cette erreur et faire le tour de la boucle à la recherche de nouveau pour la prochaine disponible Id, OU b) Faites un verrouillage au début du processus pour qu'un seul utilisateur recherche des ID à un moment donné

1

Vous devriez vraiment essayer de convertir la colonne en IDENTITY. SAUVEGARDE d'abord, puis utilisez ROW_NUMBER pour mettre à jour l'ID de document afin qu'ils commencent à partir de 1 et jusqu'au nombre de documents. Vous devez le faire dans un moment à la fois parce que si la colonne de nombre est utilisée comme référence dans d'autres tables (clés étrangères) SQL Server va essayer de mettre à jour les clés étrangères et peut-être échouer en raison de conflits. En fin de compte, il suffit d'activer les spécifications d'identité pour la colonne.

:) C'est plus de travail maintenant mais cela vous évitera beaucoup de problèmes plus tard.

2
select 
    MIN(NextID) NextUsableID 
from (
    select (case when c1 = c2 then 0 
      else c1 end) NextID 
    from ( select ROW_NUMBER() over (order by record_id) c1, 
        record_id c2 
      from myTable) 
) 
where NextID > 0 
3
declare @value int 

select @value = case 
        when @value is null or @value + 1 = idcolumn 
        then idcolumn 
        else @value end 
    from table 
    order by idcolumn 

select @value + 1 

Est-ce que 1 scan de table plutôt que 2 scans d'une correspondance de hachage et une jointure comme le sommet réponse

+1

beaucoup plus rapide que le Top réponse +1! –

2

Voici une approche simple. Cela peut ne pas être rapide. Il ne trouvera pas de numéros manquants au début.

SELECT MIN(MT1.MyInt+1) 
FROM MyTable MT1 
LEFT OUTER JOIN MyTable MT2 ON (MT1.MyInt+1)=MT2.MyInt 
WHERE MT2.MyInt Is Null 
1

Je sais que cette réponse est en retard, mais vous pouvez trouver le plus petit nombre utilisé en utilisant une expression de table récursive:

CREATE TABLE Test 
(
    ID int NOT NULL 
) 

--Insert values here 

;WITH CTE AS 
(
    --This is called once to get the minimum and maximum values 
    SELECT nMin = 1, MAX(ID) + 1 as 'nMax' 
    FROM Test 
    UNION ALL 
    --This is called multiple times until the condition is met 
    SELECT nMin + 1, nMax 
    FROM CTE 
    WHERE nMin < nMax 
) 

--Retrieves all the missing values in the table. Removing TOP 1 will 
--list all the unused numbers up to Max + 1 
SELECT TOP 1 nMin 
FROM CTE 
WHERE NOT EXISTS 
(
    SELECT ID 
    FROM Test 
    WHERE nMin = ID 
) 
1

Supposons que votre ID devrait toujours commencer par 1:

SELECT MIN(a.id) + 1 AS firstfree 
FROM (SELECT id FROM table UNION SELECT 0) a 
LEFT JOIN table b ON b.id = a.id + 1 
WHERE b.id IS NULL 

Cela gère tous les cas auxquels je peux penser - y compris aucun enregistrement existant.

La seule chose que je n'aime pas cette solution est que les conditions supplémentaires doivent être inclus deux fois, comme ça:

SELECT MIN(a.id) + 1 AS firstfree 
FROM (SELECT id FROM table WHERE column = 4711 UNION SELECT 0) a 
LEFT JOIN table b ON b.column = 4711 AND b.id = a.id + 1 
WHERE b.id IS NULL 

S'il vous plaît noter également les commentaires sur le verrouillage et la concurrence - l'obligation de combler les lacunes est dans la plupart des cas une mauvaise conception et peut causer des problèmes. Cependant, I avait une bonne raison de le faire: les ID doivent être imprimés et dactylographiés par des humains et nous ne voulons pas avoir d'ID avec beaucoup de chiffres après un certain temps, alors que tous les plus bas sont libres ...

0

je faisais face à un problème similaire et est venu avec ceci:

Select Top 1 IdGapCheck 
From (Select Id, ROW_NUMBER() Over (Order By Id Asc) AS IdGapCheck 
    From dbo.table) F 
Where Id > IdGapCheck 
Order By Id Asc 
Questions connexes