2009-03-26 14 views
34

¿Cómo se encuentra el número más pequeño no utilizado en una columna de SQL Server?Encuentra el número más pequeño no utilizado en SQL Server

Estoy a punto de importar una gran cantidad de registros grabados manualmente de Excel en una tabla de SQL Server. Todos ellos tienen una identificación numérica (llamada número de documento), pero no fueron asignados secuencialmente por razones que ya no se aplican, lo que significa que a partir de ahora cuando mi sitio web registre un nuevo registro, debe asignarle el número de documento más pequeño posible (mayor que cero) que aún no se ha tomado.

¿Hay alguna manera de hacerlo mediante SQL simple o es un problema para TSQL/código?

Gracias!

EDITAR

agradecimiento especial a WW para plantear la cuestión de la concurrencia. Dado que esta es una aplicación web, tiene múltiples subprocesos por definición y cualquiera que se enfrente a este mismo problema debería considerar bloquear un código o un nivel de DB para evitar un conflicto.

LINQ

FYI - esto se puede lograr a través de LINQ con el siguiente código:

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

Respuesta

48

encontrar la primera fila en la que no existe una fila con 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 

Editar:

para manejar el caso especial en el que el identificador más bajo existente no es 1, aquí es una fea solución:

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

Esto perderá cualquier bloque contiguo de identificadores que comience al principio del rango. Por ejemplo, si 'table' tiene identificadores (5,6,8,9,10), esto devolverá 7, no ninguno de 1-4. – joshperry

+0

@joshperry Tienes razón. Me perdí el comentario sobre querer completar todas las identificaciones mayores a cero. Añadí una solución fea. Quizás alguien sugiera una mejora. –

+0

+1 ¡Muy útil, gracias! La mayoría de las otras respuestas aquí han sido "no necesitas hacer eso, deja que el sistema incremente la clave", pero mi caso no es para la clave principal, sino otro campo numérico único donde las ranuras vacías son un problema pero pueden ocurrir, y no como resultado de una eliminación. –

2

¿Existe una razón por la que tiene que ser la más pequeña número posible? ¿Por qué necesitas llenar los agujeros?

Editar para responder a la pregunta, ya que es una regla de negocios.

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 

(que no tienen una base de datos a mano, por lo que este puede no ser 100% exacto, pero usted debería ser capaz de conseguir que a partir de ahí)

+0

es una regla de negocio. Estos números de documento se entregan a los usuarios y se usan realmente. Hice la misma pregunta, pero se mantienen firmes en esta. :) –

+0

Eso es desafortunado ... La única forma que conozco sería recorrerlos todos hasta encontrar una ID no utilizada. Lo siento por tu suerte. –

3

Si hay lagunas en la secuencia, puede encontrar la primera brecha con algo como esto:

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 otras palabras, encontrar el menor ID cuyo sucesor no existe, y volver que el sucesor. Si no hay lagunas, devuelve una ID mayor que la mayor existente. Se inserta una ID de marcador de posición de 0 para garantizar que se consideren los ID que comienzan con 1.

Tenga en cuenta que esto llevará al menos n log n time.

Microsoft SQL permite el uso de una cláusula from en una declaración insert, por lo que puede que no tenga que recurrir al código de procedimiento.

11

Si los ordena por ID numérico, el número que está buscando será el primero para el cual la función ROW_NUMBER() no es igual a la ID.

+0

+1 buen truco específico de SQL Server. ¿Podría hacerse esto en una subselección para elegir la primera no coincidencia, luego unificada con max (id) +1 para hacerlo de una vez? – 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; 

Esta es una alternativa a las respuestas usando subconsultas correlacionadas dada por @Jeffr ey Hantlin y @Darrel Miller.

Sin embargo, la política que está describiendo realmente no es una buena idea. Los valores de ID deben ser únicos, pero no se debe exigir que sean consecutivos.

¿Qué sucede si envía un correo electrónico a alguien con un enlace al documento n. ° 42 y luego lo elimina posteriormente? Más tarde, volverá a usar la ID # 42 para un nuevo documento. ¡Ahora el destinatario del correo electrónico seguirá el enlace al documento equivocado !

+0

Admito que esto no encuentra un valor que falta de 1. Sin embargo, es un problema falso, que es mi verdadero objetivo, ¡así que no estoy interesado en encontrar una solución! :-P –

+0

Los números de documento nunca se borran. Sin embargo, estaré de acuerdo contigo en que esta es una forma pobre de identificar documentos. Sin embargo, estoy eligiendo mis batallas y hay peces más grandes para freír. –

12

Ninguna mención de bloqueo o concurrencia en ninguna de las respuestas hasta el momento.

Considere estas dos usuarios añadiendo un documento casi al mismo tiempo: -

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

Tiene que poner: a) Handle que el error y dar la vuelta al bucle de nuevo en busca de la siguiente ID disponible, o b) Realice un bloqueo al inicio del proceso para que solo 1 usuario esté buscando identificadores en un momento determinado

1

Debería intentar convertir la columna en IDENTIDAD. BACKUP primero, luego use ROW_NUMBER para actualizar la identificación del documento para que empiece desde 1 y hasta el recuento de documentos. Debería hacerlo en WHILE uno a la vez porque si la columna de número se usa como referencia en otras tablas (claves externas) SQL Server intentará actualizar las claves externas y puede fallar debido a conflictos. Al final solo habilite las especificaciones de identidad para la columna.

:) Es más trabajo ahora, pero le ahorrará muchos problemas más adelante.

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 

qué 1 exploración de tabla en lugar de 2 scans un partido de hash y una unión como la respuesta superior

+1

mucho más rápido que la respuesta superior +1! –

2

Aquí es un enfoque simple. Puede que no sea rápido. No encontrará números faltantes al principio.

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

Sé que esta respuesta es tardía, pero se puede encontrar el número más pequeño no usado mediante el uso de una expresión de tabla recursiva:

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

Supongamos que su ID de siempre debe comenzar con 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 

Esto maneja todos los casos que se me ocurre, incluso sin registros existentes.

La única cosa que no me gusta de esta solución es que las condiciones adicionales tienen que ser incluidos en dos ocasiones, al igual que:

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 

Por favor notar también los comentarios sobre el bloqueo y concurrencia - el requisito para llenar los vacíos en la mayoría de los casos es un mal diseño y puede causar problemas. Sin embargo, I tenía una buena razón para hacerlo: los ID deben ser impresos y escritos por personas y no queremos tener ID con muchos dígitos después de un tiempo, mientras que todos los bajos son gratuitos ...

0

que se enfrentaron a un problema similar y se le ocurrió esto:

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 
Cuestiones relacionadas