【问题标题】:Create Sequence in MS SQL Server 2008在 MS SQL Server 2008 中创建序列
【发布时间】:2013-09-19 10:05:30
【问题描述】:

我编写了一个程序,我可以在其中请求身份证。

有不同类型的身份证(红、蓝、绿)

在请求时,程序应生成标识号。数字(数字范围)取决于请求的卡。

Red Card: 1 - 50000 
Blue Card: 50001 - 100000 
Green Card: 100001 - 150000

如果我向系统添加新的身份证,那么序列应该会自动为新添加的身份证创建一个新的数字范围。数字不应重复出现。一个号码只能使用一次。

我该怎么做?谁能帮我解决这个问题?

【问题讨论】:

  • 第 50001 次红牌请求会发生什么?
  • 解决方案是否需要并发,或者我们可以假设数据库请求是由应用层序列化的?也就是说,就数据库而言,我们可以假设应用层是单线程的吗?
  • 您是否假设任何给定类型的卡片永远不会超过 50k,并且您的卡片永远不会超过 (MAXIMUM_INT_VALUE)/50k?
  • 我们可以只创建一个额外的表格来容纳所有卡片颜色和范围吗?

标签: sql sql-server sql-server-2008


【解决方案1】:

您可以为此使用而不是插入触发器

create table Cards_Types (Color nvarchar(128) primary key, Start int);
create table Cards (ID int primary key, Color nvarchar(128));

insert into Cards_Types
select 'RED', 0 union all
select 'BLUE', 50000 union all
select 'GREEN', 100000;

create trigger utr_Cards_Insert on Cards
instead of insert as
begin
    insert into Cards (id, Color)
    select
        isnull(C.id, CT.Start) + row_number() over(partition by i.Color order by i.id),
        i.Color
    from inserted as i
        left outer join Cards_Types as CT on CT.Color = i.Color
        outer apply (
            select max(id) as id
            from Cards as C
            where C.Color = i.Color
        ) as C
end

sql fiddle demo

它允许您一次插入多行:

insert into Cards (Color)
select 'GREEN' union all
select 'GREEN' union all
select 'RED' union all
select 'BLUE'

请注意,您最好在卡片列Color, ID 上有索引。

另请注意,您只能为每种类型插入 50000 条记录。您可以使用不同的种子,例如 1 代表“红色”,2 代表“蓝色”等等,并为例如 100 个类型的卡片预留位置:

create table Cards_Types (Color nvarchar(128) primary key, Start int);
create table Cards (ID int primary key, Color nvarchar(128));

insert into Cards_Types
select 'RED', 1 union all
select 'BLUE', 2 union all
select 'GREEN', 3;

create trigger utr_Cards_Insert on Cards
instead of insert as
begin
    insert into Cards (id, Color)
    select
        isnull(C.id, CT.Start - 100) + row_number() over(partition by i.Color order by i.id) * 100,
        i.Color
    from inserted as i
        left outer join Cards_Types as CT on CT.Color = i.Color
        outer apply (
            select max(id) as id
            from Cards as C
            where C.Color = i.Color
        ) as C
end;

sql fiddle demo

这样,“RED”的 ID 将始终以 1 结束,“BLUE”的 ID 以 2 结束,依此类推。

【讨论】:

    【解决方案2】:

    从设计的角度来看,我强烈反对将额外的逻辑编码到标识符中,即将卡片颜色分配给特定范围。我宁愿使用能很好地处理唯一性和并发性的 IDENTITY 列,使 ID 完全替代并将给定 ID 的卡片颜色信息存储在另一个属性中。可能在该附加属性上创建索引以检索给定颜色的记录。

    还想一想,如果红卡的所有者要求将其更改为蓝卡,需要什么?对于范围,要保留颜色分配,您需要创建一个新的 id,并可能在其他地方存储有关从旧到新的 id 序列的信息。如果有人多次更改它怎么办?使用代理 ID,您可以一直拥有一个 ID,以便能够在整个历史记录中跟踪同一个人,也许只需将日期信息添加到您的表格中以按顺序订购更改。这只是一个简单场景的示例。

    【讨论】:

      【解决方案3】:

      您可以为此利用 SQL Server 的 IDENTITY 机制,因为它易于使用并且可以很好地处理并发。

      更具体地说,您可以使用以下脚本创建三个仅包含标识(自动递增)Id 列的表:

      create table RedCardIds(Id int identity(1, 1) primary key)
      create table BlueCardIds(Id int identity(50001, 1) primary key)
      create table GreenCardIds(Id int identity(100001, 1) primary key)
      GO
      

      这三个表的标识值设置为与您的区间下限匹配。

      然后,对于每个请求,您都将插入到相应的表中并使用the OUTPUT clause 来获取新生成的标识值。

      例如,如果请求是红牌,你可以写:

      insert RedCardIds 
      output inserted.Id
      default values
      

      会输出:

      Id
      -----------
      1
      
      (1 row(s) affected)
      

      在下一次运行时,它将返回 2,依此类推。

      同样,蓝卡的第一个请求会触发语句:

      insert BlueCardIds 
      output inserted.Id
      default values
      

      结果:

      Id
      -----------
      500001
      
      (1 row(s) affected)
      

      【讨论】:

      • 我认为创建表格对我来说不是一个好的解决方案。卡片可以动态创建。所以不仅有三张牌。在我的系统中,我可以创建新卡,所以我需要一个程序/序列,它也会为新插入的卡自动创建一个新范围
      • @Paks 表也可以在添加/删除卡片时动态创建/删除。
      • 大声笑,他们可以,但绝对不应该:D
      • 我喜欢。可能工作的最简单的事情。一目了然。
      【解决方案4】:

      编辑 #1:我更新了触发器 (IF UPDATE)、存储过程和最后两个示例。

      CREATE TABLE dbo.CustomSequence
      (
          CustomSequenceID INT IDENTITY(1,1) PRIMARY KEY,
          SequenceName NVARCHAR(128) NOT NULL, -- or SYSNAME
              UNIQUE(SequenceName),
          RangeStart INT NOT NULL,
          RangeEnd INT NOT NULL,
              CHECK(RangeStart < RangeEnd),
          CurrentValue INT NULL,
              CHECK(RangeStart <= CurrentValue AND CurrentValue <= RangeEnd)
      );
      GO
      CREATE TRIGGER trgIU_CustomSequence_VerifyRange
      ON dbo.CustomSequence
      AFTER INSERT, UPDATE
      AS
      BEGIN
           IF (UPDATE(RangeStart) OR UPDATE(RangeEnd)) AND EXISTS
          (
              SELECT  *
              FROM    inserted i 
              WHERE   EXISTS
              (
                  SELECT  * FROM dbo.CustomSequence cs 
                  WHERE   cs.CustomSequenceID <> i.CustomSequenceID
                  AND     i.RangeStart <= cs.RangeEnd
                  AND     i.RangeEnd >= cs.RangeStart
              )
          )
          BEGIN
              ROLLBACK TRANSACTION;
              RAISERROR(N'Range overlapping error', 16, 1);
          END
      END;
      GO
      --TRUNCATE TABLE dbo.CustomSequence
      INSERT  dbo.CustomSequence (SequenceName, RangeStart, RangeEnd)
      SELECT  N'Red Card',        1,  50000 UNION ALL
      SELECT  N'Blue Card',   50001, 100000 UNION ALL
      SELECT  N'Green Card', 100001, 150000;
      GO
      -- Test for overlapping range
      INSERT  dbo.CustomSequence (SequenceName, RangeStart, RangeEnd)
      VALUES  (N'Yellow Card', -100, +100);
      GO
      /*
      Msg 50000, Level 16, State 1, Procedure trgIU_CustomSequence_VerifyRange, Line 20
      Range overlapping error
      Msg 3609, Level 16, State 1, Line 1
      The transaction ended in the trigger. The batch has been aborted.
      */
      GO
      
      -- This procedure tries to reserve 
      CREATE PROCEDURE dbo.SequenceReservation
      (
          @CustomSequenceID INT, -- You could use also @SequenceName 
          @IDsCount INT, -- How many IDs do we/you need ? (Needs to be greather than 0)
          @LastID INT OUTPUT
      )   
      AS
      BEGIN
          DECLARE @StartTranCount INT, @SavePoint VARCHAR(32);
          SET @StartTranCount = @@TRANCOUNT;
          IF @StartTranCount = 0 -- There is an active transaction ?
          BEGIN
              BEGIN TRANSACTION -- If not then it starts a "new" transaction
          END
          ELSE -- If yes then "save" a save point -- see http://technet.microsoft.com/en-us/library/ms188378.aspx
          BEGIN
              DECLARE @ProcID INT, @NestLevel INT;
              SET @ProcID = @@PROCID;
              SET @NestLevel = @@NESTLEVEL;
              SET @SavePoint = CONVERT(VARCHAR(11), @ProcID) + ',' + CONVERT(VARCHAR(11), @NestLevel);
              SAVE TRANSACTION @SavePoint;
          END
      
          BEGIN TRY
              UPDATE  dbo.CustomSequence
              SET     @LastID = CurrentValue = ISNULL(CurrentValue, 0) + @IDsCount
              WHERE   CustomSequenceID = @CustomSequenceID;
      
              IF @@ROWCOUNT = 0
                  RAISERROR(N'Invalid sequence', 16, 1);
      
              COMMIT TRANSACTION;
          END TRY
          BEGIN CATCH
              IF @StartTranCount = 0
              BEGIN
                  ROLLBACK TRANSACTION;
              END
              ELSE -- @StartTranCount > 0
              BEGIN
                  ROLLBACK TRANSACTION @SavePoint
              END
      
              DECLARE @ErrorMessage NVARCHAR(2048), @ErrorSeverity INT, @ErrorState INT;
              SELECT @ErrorMessage = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE();
              RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState); 
          END CATCH;
      END;
      GO
      
      SELECT * FROM dbo.CustomSequence;
      GO
      
      -- Example usage #1
      DECLARE @LastID INT;
      EXEC dbo.SequenceReservation  
              @CustomSequenceID = 1, -- Red Card
              @IDsCount = 2, -- How many IDs ?
              @LastID = @LastID OUTPUT;
      SELECT @LastID - 2 + 1 AS [FirstID], @LastID AS [LastID];
      GO
      
      -- Example usage #2
      DECLARE @LastID INT;
      EXEC dbo.SequenceReservation  
              @CustomSequenceID = 1, -- Red Card
              @IDsCount = 7, -- How many IDs ?
              @LastID = @LastID OUTPUT;
      SELECT @LastID - 7 + 1 AS [FirstID], @LastID AS [LastID];
      
      SELECT * FROM dbo.CustomSequence;
      GO
      

      结果:

      CustomSequenceID SequenceName RangeStart  RangeEnd    CurrentValue
      ---------------- ------------ ----------- ----------- ------------
      1                Red Card     1           50000       9
      2                Blue Card    50001       100000      NULL
      3                Green Card   100001      150000      NULL
      

      【讨论】:

      • 为什么输出参数叫@FromID?它返回序列的当前值 - sqlfiddle.com/#!3/56f4d/2
      • 为什么你需要所有这些事务,你有一个语句无论如何都会在事务中(如果implicit_tranactions 开启)。关于调用过程后OP如何获取实际ID的一些解释也很好。目前代码对我来说看起来确实过于复杂,并且它没有显示将卡实际插入 Cards 表
      【解决方案5】:

      理想情况下,您必须维护一个表来存储此信息。

      CardCategry MinNumber MaxNumber RunningNumber
      

      然后你可以编写一个SP来获取下一个数字并将卡片类别作为参数传递。示例查询如下。

      SELECT @count=count(RunningNumber)
      FROM IdentificationTable
      WHERE CardCategry=@param
      
      IF (@count=1)
          SELECT @RunningNumber=RunningNumber
          FROM IdentificationTable
          WHERE CardCategry=@param
      ELSE
          SELECT TOP 1 @min=MinNumber,@max=MaxNumber
          FROM IdentificationTable
          ORDER BY MinNumber DESC
      
          INSERT INTO IdentificationTable VALUES (@param,@max+1,@max+(@max-@min),1)
          SET @RunningNumber=1
      
      RETURN @RunningNumber
      

      这不是一个完整的工作。显然您必须对检查边界限制等进行一些错误处理。

      【讨论】:

        【解决方案6】:

        我会尝试这样的事情:

        declare @cat2start int = 50000
        declare @cat3start int = 100000
        
        declare @catName varchar(10) = 'Red'
        
        if @catName = 'Green'
            begin
            select (max(cardnumber) + 1) as [This is the next number]
            from yourTable 
            where 
            cardnumber < @cat2start
        end
        if @catName = 'Blue'
            begin
            select (max(cardnumber) + 1) as [This is the next number]
            from yourTable 
            where 
            cardumber >= @cat2start and cardnumber < @cat3start
        end
        if @catName = 'Red'
            begin
            select (max(cardnumber) + 1) as [This is the next number]
            from yourTable 
        end
        

        【讨论】:

          【解决方案7】:

          有很多答案,但我会加上我的 2 美分。请注意,我假设我在对您原始帖子的评论中所写的内容:

          create table cardTypes(cardTypeName varchar(100) primary key, [50kSlot] int unique)
          
          create table cards (identificationNumber bigint primary key);
          
          --add slot if needed
          declare @cardToBeAdded varchar(100) = 'green'
          declare @tbl50kSlot table (i int)
          merge into cardTypes as t
          using (select @cardToBeAdded as newCard) as s
          on t.[cardTypeName] = s.newCard
          when not matched by target then
          insert (cardTypeName, [50kSlot]) values (s.newCard, isnull((select max([50kSlot]) + 1 from cardTypes),1))
          when matched then
          update set [50kSlot] = [50kSlot]
          output inserted.[50kSlot] into @tbl50kSlot;
          
          declare @50kSlot int = (Select i from @tbl50kSlot)
          
          insert into cards (identificationNumber) values (isnull(
              (select max(identificationNumber)+1 from cards where identificationNumber between ((@50kSlot-1)*50000+1) and @50kSlot*50000),
              (@50kSlot-1)*50000+1)
          )
          

          当然,您需要在卡片表中添加一些实际数据。请注意,如果存在足够有效的索引,则可以相对快速地执行最后一个查询。如果存在性能问题,可能值得解决对标识号进行索引的工作。例如,如果您有很多行,请考虑在此列上创建过滤索引。

          或者,您可以将 maxInt 保留在 cardTypes 表中,并使合并表稍微复杂一些。不利的一面是,如果查询之间出现某种错误,则永远不会使用该数字,因此我的解决方案会保持顺序紧凑。

          【讨论】:

            【解决方案8】:

            SQL Fiddle

            MS SQL Server 2008 架构设置

            CREATE TABLE Table1
                ([color] varchar(10), [id] int)
            ;
            
            INSERT INTO Table1
                ([color], [id])
            VALUES
                ('Red',(select isnull(case when (max(id)/50000)%3 = 1 and 
                                            max(id)%50000 = 0 then max(id)+100000 else
                                            max(id) end,0)+1 
                          from Table1 where color = 'Red'));
            
            INSERT INTO Table1  ([color], [id]) VALUES  ('Red',50000);
            
            INSERT INTO Table1
                ([color], [id])
            VALUES
                ('Red',(select isnull(case when (max(id)/50000)%3 = 1 and 
                                            max(id)%50000 = 0 then max(id)+100000 else
                                            max(id) end,0)+1 
                          from Table1 where color = 'Red'));
            
            INSERT INTO Table1
                ([color], [id])
            VALUES
                ('Blue',(select isnull(case when (max(id)/50000)%3 = 2 and 
                                            max(id)%50000 = 0 then max(id)+100000 else
                                            max(id) end,50000)+1 
                          from Table1 where color = 'Blue'));
            
            INSERT INTO Table1
                ([color], [id])
            VALUES
                ('Green',(select isnull(case when (max(id)/50000)%3 = 0 and 
                                            max(id)%50000 = 0 then max(id)+100000 else
                                            max(id) end,100000)+1 
                            from Table1 where color = 'Green'));
            

            查询 1

            SELECT *
            FROM Table1
            

            Results

            | COLOR |     ID |
            |-------|--------|
            |   Red |      1 |
            |   Red |  50000 |
            |   Red | 150001 |
            |  Blue |  50001 |
            | Green | 100001 |
            

            【讨论】:

              【解决方案9】:

              这是我对挑战的贡献。不需要额外的表,应该是并发安全的并且可以处理批量更新。可能不是最快的,但它有效。它基本上将要插入的行复制到单独的表中,为每种颜色创建 ID,最后将所有内容移动到目标表中。

              Create Trigger Trg_CreateID ON  dbo.Cards instead of insert
              as
              begin
                set nocount on
                -- declare a working table holding intermediate results
                declare @Tmp Table (cardID int, cardColor char(1), cardNumber char(20))
              
                -- copy the data to be inserted in our working table
                insert into @Tmp select * from inserted
              
                declare @Id int
                -- fill in the Id's once per color    
                select @Id=coalesce (max(cardID),0) from dbo.Cards where cardColor='Red'
                update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Red'
              
                select @Id=coalesce(max(cardID),50000) from dbo.Cards where cardColor='Blue'
                update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Blue'
              
                select @Id=coalesce(max(cardID),100000) from dbo.Cards where cardColor='Gree'
                update @Tmp set cardID = @Id, @Id=@id+1 where cardColor='Green'
              
                -- do the actual insert here
                insert into dbo.Cards select * from @tmp
              end
              

              它假设一个像这样的表Cards

              CREATE TABLE [dbo].[Cards]
              (
                  [cardID] [int] NOT NULL,
                  [cardColor] [char](1) NOT NULL,
                  [cardNumber] [char](20) NOT NULL
              ) ON [PRIMARY]
              

              我在cardID 列中添加了一个约束,以允许在插入语句中省略它

              ALTER TABLE [dbo].[Cards] 
                ADD CONSTRAINT [DF_Cards_cardID] DEFAULT ((0)) FOR [cardID]
              

              【讨论】:

                【解决方案10】:

                *此解决方案适用于单行插入,多个插入的并发需要不同的方法。更多详情请在 cmets 中讨论 *


                如果没有用于创建表的选项,那么您可以使用触发器来代替(像 oracle 中的触发器之前一样进行调整)。

                使用触发器内的特定条件来设置Identity 列的范围。以下是如何实施解决方案的示例。

                表格

                CREATE TABLE REQUEST_TABLE(
                      REQ_ID numeric(8, 0) NOT NULL,
                      REQ_COLOR VARCHAR(30) NOT NULL
                 ); -- I have used this sample table
                

                代替触发器

                CREATE TRIGGER tg_req_seq ON REQUEST_TABLE
                INSTEAD OF INSERT AS
                DECLARE @REQ_ID INT
                DECLARE @REQ_COLOR VARCHAR(30)
                DECLARE @REQ_START INT 
                BEGIN  
                  SELECT @REQ_COLOR= (SELECT ISNULL(REQ_COLOR,'NA') FROM INSERTED)
                
                  SELECT @REQ_START = (SELECT CASE WHEN @REQ_COLOR = 'Red' THEN 0
                                    WHEN @REQ_COLOR = 'Blue' THEN 50000 
                                    ELSE 100000 END)
                
                  SELECT @REQ_ID = ISNULL(MAX(REQ_ID),@REQ_START)+1 FROM REQUEST_TABLE   
                    WHERE REQ_COLOR = @REQ_COLOR  
                
                  INSERT INTO REQUEST_TABLE (REQ_ID,REQ_COLOR)
                   VALUES (@REQ_ID,@REQ_COLOR)
                END;
                

                现在在一些插入语句之后

                INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Red');
                
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Blue');
                
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');
                INSERT INTO REQUEST_TABLE VALUES(NULL,'Yellow');
                

                我在SqlFiddle 中添加了相同的结果。让我知道我是否遗漏了一些内容。

                编辑

                更新了Fiddle 以满足灵活的需求。

                【讨论】:

                • 这看起来是个不错的解决方案。我如何更改 SELECT REQ_START = (SELECT CASE WHEN REQ_COLOR = 'Red' THEN 0 WHEN REQ_COLOR = 'Blue' THEN 50000 ELSE 100000 END),这样如果我插入新的身份证,就会创建一个新的范围。每次我插入一张新的身份证时,都应该为该卡创建 50000 的范围。例如:新卡(绿色)范围 = 1000001 - 150000 以此类推
                • @Paks 如果卡片范围是已知的,那么您只需要修改 case 语句并添加您自己的范围。如果卡未配置,则 else 条件将处理该问题。
                • @Paks 请注意,此解决方案仅适用于单行插入,这不是并发的 - 您可以在 select @REQ_ID ...insert into REQUEST_TABLE... 之间添加 watifor '00:00:05',然后运行两种并发插入一种类型卡
                • @Steve,不仅如此,它真的很容易破解,如果select @REQ_IDinsert 之间有一些延迟,那么可以插入重复的@REQ_ID。我不知道为什么 OP 选择了这个答案。
                • @Pratik 所以基本上如果我用“而不是触发器”来写答案,它应该被视为答案吗?有很多人知道 SO 上的触发器,但我确信他们没有发布这样的解决方案,因为它完全没用。我什至不确定我的解决方案是否是并发稳定的,但我至少已经对其进行了测试,如果有人能创造出更好的解决方案,我会很高兴。但是您的解决方案显然很容易破解并且仅适用于单行插入。我不会反对它,但 OP 接受了这个作为答案,我当然不希望其他人使用它。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2013-07-22
                • 2017-08-08
                • 2013-10-31
                • 2011-10-03
                • 1970-01-01
                • 2010-12-08
                • 1970-01-01
                相关资源
                最近更新 更多