Найдите наименьшее неиспользуемое число в SQL Server - PullRequest
40 голосов
/ 26 марта 2009

Как найти наименьшее неиспользуемое число в столбце SQL Server?

Я собираюсь импортировать большое количество записей, записанных вручную, из Excel в таблицу SQL Server. Все они имеют числовой идентификатор (называемый номером документа), но они не были назначены последовательно по причинам, которые больше не применяются, то есть с этого момента, когда мой веб-сайт записывает новую запись, ему необходимо присвоить ему наименьший возможный номер документа больше нуля), который еще не был взят.

Есть ли способ сделать это с помощью простого SQL или это проблема для TSQL / кода?

Спасибо!

EDIT

Особая благодарность WW за поднятие проблемы параллелизма. Учитывая, что это веб-приложение, оно по определению является многопоточным, и всем, кто сталкивается с этой же проблемой, следует рассмотреть возможность блокировки на уровне кода или БД для предотвращения конфликта.

* LINQ 1018 *

FYI - это может быть выполнено через LINQ со следующим кодом:

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

Ответы [ 14 ]

54 голосов
/ 26 марта 2009

Найти первую строку, где не существует строки с 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:

Для обработки особого случая, когда самый низкий существующий идентификатор не равен 1, вот уродливое решение:

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
12 голосов
/ 26 марта 2009

Никаких упоминаний о блокировке или параллелизме в ответах пока нет.

Рассмотрим, как эти два пользователя добавляют документ почти одновременно: -

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

Вам либо нужно: а) Обработайте эту ошибку и снова обойдите цикл, ища следующий доступный идентификатор, ИЛИ б) снять блокировку в начале процесса, чтобы только 1 пользователь искал идентификаторы в определенное время

12 голосов
/ 26 марта 2009

Если вы сортируете их по числовому идентификатору, искомый номер будет первым, для которого функция ROW_NUMBER () не равна идентификатору.

10 голосов
/ 26 марта 2009
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;

Это альтернатива ответам с использованием коррелированных подзапросов, заданных @Jeffrey Hantlin и @Darrel Miller.

Однако политика, которую вы описываете, на самом деле не очень хорошая идея. Значения идентификатора должны быть уникальными, но не должны быть последовательными.

Что произойдет, если вы отправите кому-нибудь по электронной почте ссылку на документ № 42, а затем впоследствии удалите документ? Позже вы повторно используете идентификатор № 42 для нового документа. Теперь получатель письма перейдет по ссылке на неправильный документ !

5 голосов
/ 27 августа 2009
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

Сканирует ли 1 таблица вместо 2 хэш-совпадений и объединений, как верхний ответ

3 голосов
/ 26 марта 2009

Если в последовательности есть пробелы, вы можете найти первый пробел примерно так:

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

Другими словами, найдите наименьший идентификатор, преемник которого не существует, и верните этого преемника. Если пробелов нет, возвращается один больше, чем самый большой существующий идентификатор. Идентификатор заполнителя 0 вставляется, чтобы гарантировать, что идентификаторы, начинающиеся с 1, будут считаться.

Обратите внимание, что это займет не менее n log n времени.

Microsoft SQL разрешает использование предложения from в выражении insert, поэтому вам может не потребоваться прибегать к процедурному коду.

2 голосов
/ 24 мая 2016

Давайте предположим, что ваши идентификаторы всегда должны начинаться с 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

Это обрабатывает все случаи, о которых я могу подумать, включая отсутствие существующих записей.

Единственное, что мне не нравится в этом решении, это то, что дополнительные условия должны быть включены дважды, например:

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

Также обратите внимание на комментарии о блокировке и параллелизме - требование заполнить пробелы в большинстве случаев является плохим дизайном и может вызвать проблемы. Однако у I была веская причина для этого: идентификаторы должны быть напечатаны и напечатаны людьми, и мы не хотим, чтобы через некоторое время идентификаторы имели много цифр, тогда как все младшие из них бесплатны ...

2 голосов
/ 09 октября 2014

Вот простой подход. Это может быть не быстро. Он не найдет пропущенные номера в начале.

SELECT MIN(MT1.MyInt+1)
FROM MyTable MT1
LEFT OUTER JOIN MyTable MT2 ON (MT1.MyInt+1)=MT2.MyInt
WHERE MT2.MyInt Is Null
2 голосов
/ 27 апреля 2009
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
2 голосов
/ 26 марта 2009

Есть ли причина, по которой это должно быть наименьшее возможное число? Зачем вам нужно заполнить отверстия?

Измените , чтобы объявить ответ, так как это бизнес-правило.

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

(у меня нет удобной базы данных, так что она может быть не точной на 100%, но вы сможете получить ее оттуда)

...