【问题标题】:Find the smallest unused number in SQL Server在 SQL Server 中查找最小的未使用数
【发布时间】:2010-10-15 14:35:48
【问题描述】:

如何在 SQL Server 列中找到最小的未使用数字?

我即将将大量手动记录的记录从 Excel 导入到 SQL Server 表中。它们都有一个数字 ID(称为文档编号),但由于不再适用的原因,它们没有按顺序分配,这意味着从现在开始,当我的网站记录新记录时,它需要为其分配尽可能小的文档编号(大于零)尚未被占用。

有没有办法通过普通的 SQL 来做到这一点,或者这是 TSQL/代码的问题?

谢谢!

编辑

特别感谢WW 提出并发问题。鉴于这是一个 Web 应用程序,它根据定义是多线程的,任何遇到同样问题的人都应该考虑使用代码或数据库级别的锁来防止冲突。

LINQ

仅供参考 - 这可以通过 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

【问题讨论】:

标签: sql sql-server gaps-and-islands


【解决方案1】:

ROW_NUMBER() 函数示例:

IF NOT EXISTS (SELECT TOP 1 row_num FROM (SELECT ROW_NUMBER() OVER (ORDER BY Id) row_num, Id FROM table) t WHERE t.Id > t.row_num) SELECT MAX (Id)+1 FROM table ELSE SELECT TOP 1 row_num FROM (SELECT ROW_NUMBER() OVER (ORDER BY Id) row_num, Id FROM table) t WHERE t.Id > t.row_num;

【讨论】:

    【解决方案2】:

    对于 Oracle DB,这应该可以完成工作:

    SELECT MIN(NI) FROM
            (SELECT ROWNUM AS NI,YOUR_ID
             FROM (SELECT YOUR_ID
                   FROM YOUR_TABLE 
                   ORDER BY YOUR_ID ASC))
    WHERE NI<>YOUR_ID
    

    【讨论】:

      【解决方案3】:

      我遇到了类似的问题并想出了这个:

      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
      

      【讨论】:

        【解决方案4】:

        假设您的 ID 应始终以 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
        

        还请注意有关锁定和并发性的 cmets - 填补空白的要求在大多数情况下是糟糕的设计,可能会导致问题。但是,有一个很好的理由这样做:ID 是由人类打印和键入的,我们不希望一段时间后有很多数字的 ID,而所有低的 ID 都是免费...

        【讨论】:

          【解决方案5】:

          我知道这个答案很晚,但您可以使用递归表表达式找到最小的未使用数字:

          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
          )
          

          【讨论】:

            【解决方案6】:

            这是一个简单的方法。它可能不会很快。它不会在开头找到丢失的数字。

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

            【讨论】:

              【解决方案7】:
              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
              

              【讨论】:

                【解决方案8】:

                查找不存在 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
                

                编辑:

                要处理现有的最低 id 不是 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
                

                【讨论】:

                • 这将错过从范围开头开始的任何连续的 id 块。例如,如果table 有 ids (5,6,8,9,10),这将返回 7,而不是 1-4 中的任何一个。
                • @joshperry 你是对的。我错过了关于想要填充所有大于零的 id 的评论。我添加了一个丑陋的修复。也许有人会提出改进建议。
                • +1 非常有帮助,谢谢!这里的大多数其他答案都是“您不需要这样做,让系统增加键”,但我的情况不是针对主键,而是针对另一个唯一的数字字段,其中空槽是一个问题,但可以发生,而不是作为删除的结果。
                • 第一部分有更简单的版本。 SELECT MAX(t1.Id + 1) FROM t1 它为您提供新的最高 ID。如果在 3 之前,它将返回 4。
                • @DarrelMiller,您要求改进 - 请参阅我的回答 :)
                【解决方案9】:
                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 次扫描哈希匹配和连接,如最佳答案

                【讨论】:

                • 比最佳答案 +1 快得多!
                【解决方案10】:

                您确实应该尝试将该列转换为 IDENTITY。 首先备份,然后使用 ROW_NUMBER 更新文档 ID,以便它们从 1 开始直到文档计数。 您当时应该在 WHILE 中执行此操作,因为如果将数字列用作其他表(外键)中的引用,SQL Server 将尝试更新外键并且可能由于冲突而失败。 最后只为列启用标识规范。

                :) 现在工作量更大,但以后会省去很多麻烦。

                【讨论】:

                  【解决方案11】:

                  如果序列中存在间隙,您可以通过以下方式找到第一个间隙:

                  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
                  

                  也就是说,找到后继不存在的最小ID,并返回那个后继。如果没有间隙,则返回比现存最大 ID 大 1 的值。插入占位符 ID 0 以确保考虑以 1 开头的 ID。

                  请注意,这至少需要 n log n 时间。

                  Microsoft SQL 允许在insert 语句中使用from 子句,因此您可能不需要求助于过程代码。

                  【讨论】:

                    【解决方案12】:

                    到目前为止,在任何答案中都没有提到锁定或并发。

                    考虑这两个用户几乎同时添加一个文档:-

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

                    您需要: a) 处理该错误并再次绕过循环寻找下一个可用的 Id,或者 b)在流程开始时锁定,因此只有 1 个用户在特定时间寻找 Id

                    【讨论】:

                      【解决方案13】:

                      它必须是最小的数字有什么原因吗?为什么要填坑?

                      编辑以宣传答案,因为这是商业规则。

                      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% 准确,但你应该可以从那里得到它)

                      【讨论】:

                      • 这是一个商业规则。然后将这些文档编号分发给用户并实际使用。我问了同样的问题,但他们在这个问题上立场坚定。 :)
                      • 那很不幸...我知道的唯一方法是遍历它们,直到找到一个未使用的 ID。对不起,你的运气。
                      【解决方案14】:
                      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 给出的相关子查询的答案的替代方案。

                      但是,您描述的政策确实不是一个好主意。 ID 值应该是唯一的,但不应要求是连续的。

                      如果您通过电子邮件向某人发送文档 #42 的链接,然后又删除了该文档,会发生什么情况?稍后,您将 id #42 重新用于新文档。现在,电子邮件的收件人将点击指向错误文档的链接!

                      【讨论】:

                      • 我承认这并没有找到缺失值 1。但是,这是一个虚假的问题,这是我的真正观点,所以我对提出解决方案不感兴趣! :-P
                      • 文件编号永远不会被删除。不过,我同意你的看法,这是一种识别文档的糟糕方法。不过,我正在挑选我的战斗,还有更大的鱼要炸。
                      【解决方案15】:

                      如果您按数字 ID 对它们进行排序,您要查找的数字将是第一个 ROW_NUMBER() 函数不等于 ID 的数字。

                      【讨论】:

                      • +1 很好的 SQL Server 特定技巧。这可以在子选择中完成以挑选第一个不匹配的,然后与 max(id)+1 联合以一次性完成吗?
                      • 有 SQL 示例吗?
                      • @niico: SELECT TOP 1 q.r FROM ( SELECT id, ROW_NUMBER() OVER (ORDER BY id ASC) AS r FROM &lt;table&gt; ORDER BY id OFFSET 0 ROWS ) q WHERE q.id &lt;&gt; q.r
                      猜你喜欢
                      • 2017-02-10
                      • 1970-01-01
                      • 2014-07-04
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多