如何获取序列中的下一个数字

时间:2016-02-08 01:53:34

标签: sql sql-server sql-server-2014

我有一张这样的表:

+----+-----------+------+-------+--+
| id | Part      | Seq  | Model |  |
+----+-----------+------+-------+--+
| 1  | Head      | 0    | 3     |  |
| 2  | Neck      | 1    | 3     |  |
| 3  | Shoulders | 2    | 29    |  |
| 4  | Shoulders | 2    | 3     |  |
| 5  | Stomach   | 5    | 3     |  |
+----+-----------+------+-------+--+

如何为模型3的Stomach之后的下一个seq插入另一条记录。所以这是新表假设的样子:

+----+-----------+------+-------+--+
| id | Part      | Seq  | Model |  |
+----+-----------+------+-------+--+
| 1  | Head      | 0    | 3     |  |
| 2  | Neck      | 1    | 3     |  |
| 3  | Shoulders | 2    | 29    |  |
| 4  | Shoulders | 2    | 3     |  |
| 5  | Stomach   | 5    | 3     |  |
| 6  | Groin     | 6    | 3     |  |
+----+-----------+------+-------+--+

有没有办法制作一个插入查询,它只会在模型3的最高seq之后给出下一个数字。此外,寻找并发安全的东西。

9 个答案:

答案 0 :(得分:12)

如果您不维护计数器表,则有两种选择。在事务中,首先使用以下表提示之一选择MAX(seq_id)

  1. WITH(TABLOCKX, HOLDLOCK)
  2. WITH(ROWLOCK, XLOCK, HOLDLOCK)
  3. TABLOCKX + HOLDLOCK有点矫枉过正。它会阻止常规的select语句,即使事务很小,也可以将其视为 heavy

    ROWLOCK, XLOCK, HOLDLOCK表提示可能是一个更好的主意(但是:进一步阅读带有计数器表的替代方案)。优点是它不会阻止常规选择语句,即当选择语句不出现在SERIALIZABLE事务中时,或者当select语句不提供相同的表提示时。使用ROWLOCK, XLOCK, HOLDLOCK仍会阻止插入语句。

    当然,您需要确保程序中没有其他部分在没有这些表提示的情况下(或在MAX(seq_id)事务之外)选择SERIALIZABLE,然后使用此值插入行。

    请注意,根据以这种方式锁定的行数,SQL Server可能会将锁升级为表锁。阅读有关锁定升级的更多信息here

    使用WITH(ROWLOCK, XLOCK, HOLDLOCK)的插入过程如下所示:

    DECLARE @target_model INT=3;
    DECLARE @part VARCHAR(128)='Spine';
    BEGIN TRY
        BEGIN TRANSACTION;
        DECLARE @max_seq INT=(SELECT MAX(seq) FROM dbo.table_seq WITH(ROWLOCK,XLOCK,HOLDLOCK) WHERE model=@target_model);
        IF @max_seq IS NULL SET @max_seq=0;
        INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@max_seq+1,@target_model);
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH
    

    另一种可能更好的想法是拥有一个计数器表,并在计数器表上提供这些表提示。该表如下所示:

    CREATE TABLE dbo.counter_seq(model INT PRIMARY KEY, seq_id INT);
    

    然后您将按如下方式更改插入过程:

    DECLARE @target_model INT=3;
    DECLARE @part VARCHAR(128)='Spine';
    BEGIN TRY
        BEGIN TRANSACTION;
        DECLARE @new_seq INT=(SELECT seq FROM dbo.counter_seq WITH(ROWLOCK,XLOCK,HOLDLOCK) WHERE model=@target_model);
        IF @new_seq IS NULL 
            BEGIN SET @new_seq=1; INSERT INTO dbo.counter_seq(model,seq)VALUES(@target_model,@new_seq); END
        ELSE
            BEGIN SET @new_seq+=1; UPDATE dbo.counter_seq SET seq=@new_seq WHERE model=@target_model; END
        INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@new_seq,@target_model);
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH
    

    优点是使用的行锁更少(即dbo.counter_seq中每个模型一个),锁升级无法锁定整个dbo.table_seq表,从而阻塞了select语句。

    您可以测试所有这些并自己查看效果,方法是从WAITFOR DELAY '00:01:00'中选择序列后放置counter_seq,然后在第二个SSMS选项卡中摆弄表格。

    PS1:使用ROW_NUMBER() OVER (PARTITION BY model ORDER BY ID)不是一个好方法。如果删除/添加行,或者ID已更改,则序列将更改(考虑应永远不会更改的发票ID)。此外,在检索单行时必须确定所有先前行的行号的性能方面也是一个坏主意。

    PS2:当SQL Server已经通过隔离级别或细粒度的表提示提供锁定时,我永远不会使用外部资源来提供锁定。

答案 1 :(得分:4)

处理此类插入的正确方法是使用identity列,或者,如果您愿意,还可以使用列的序列和默认值。

但是,NULL列的值为seq,这似乎不正确。

查询的问题,例如:

Insert into yourtable(id, Part, Seq, Model)
    Select 6, 'Groin', max(Seq) + 1, 3 
    From yourtable;

是同时运行的两个这样的查询可以产生相同的值。建议将seq声明为唯一的标识列,让数据库完成所有工作。

答案 2 :(得分:3)

让我们首先列出挑战:

  1. 我们不能使用普通约束,因为存在空值,我们还需要满足重复项和间隙 - 如果我们查看现有数据。 这很好,我们会弄明白的; - >在第3步
  2. 我们要求并发操作的安全性(因此某些形式或混合的事务,隔离级别以及可能的“有点SQL互斥体”。)Gut感觉这里存储过程有几个原因:

    2.1它更容易保护sql注入

    2.2我们可以更容易地控制隔离级别(表锁定)并从这种要求带来的一些问题中恢复

    2.3我们可以使用应用程序级数据库锁来控制并发性

  3. 我们必须在每个插页上存储或找到下一个值。并发这个词告诉我们已经存在争用并且可能存在高吞吐量(否则请坚持使用单线程)。因此,我们必须已经在考虑:不要在已经很复杂的世界中从同一个表中读取。
  4. 因此,使用这个简短的前传,让我们尝试一个解决方案:

    首先,我们创建原始表,然后还有一个表来保存序列(BodyPartsCounter),我们将其设置为最后使用的序列+ 1:

        CREATE TABLE BodyParts
            ([id] int identity, [Part] varchar(9), [Seq] varchar(4), [Model] int)
        ;
    
        INSERT INTO BodyParts
            ([Part], [Seq], [Model])
        VALUES
            ('Head', NULL, 3),
            ('Neck', '1', 3),
            ('Shoulders', '2', 29),
            ('Shoulders', '2', 3),
            ('Stomach', '5', 3)
        ;
    
        CREATE TABLE BodyPartsCounter
            ([id] int
            , [counter] int)
        ;
    
        INSERT INTO BodyPartsCounter
            ([id], [counter])
        SELECT 1, MAX(id) + 1 AS id FROM BodyParts
        ;
    

    然后我们需要创建能够实现魔力的存储过程。简而言之,它充当互斥体,基本上保证您的并发性(如果您不在其他地方插入或更新相同的表)。然后得到下一个seq,更新它并插入新行。在发生这一切之后,它将提交事务并释放存储的proc以用于下一个等待的调用线程。

    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    -- =============================================
    -- Author:      Charlla
    -- Create date: 2016-02-15
    -- Description: Inserts a new row in a concurrently safe way
    -- =============================================
    CREATE PROCEDURE InsertNewBodyPart 
    @bodypart varchar(50), 
    @Model int = 3
    AS
    BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    
        BEGIN TRANSACTION;
    
        -- Get an application lock in your threaded calls
        -- Note: this is blocking for the duration of the transaction
        DECLARE @lockResult int;
        EXEC @lockResult = sp_getapplock @Resource = 'BodyPartMutex', 
                       @LockMode = 'Exclusive';
        IF @lockResult = -3 --deadlock victim
        BEGIN
            ROLLBACK TRANSACTION;
        END
        ELSE
        BEGIN
            DECLARE @newId int;
            --Get the next sequence and update - part of the transaction, so if the insert fails this will roll back
            SELECT @newId = [counter] FROM BodyPartsCounter WHERE [id] = 1;
            UPDATE BodyPartsCounter SET [counter] = @newId + 1 WHERE id = 1;
    
            -- INSERT THE NEW ROW
            INSERT INTO dbo.BodyParts(
                Part
                , Seq
                , Model
                )
                VALUES(
                    @bodypart
                    , @newId
                    , @Model
                )
            -- END INSERT THE NEW ROW
            EXEC @lockResult = sp_releaseapplock @Resource = 'BodyPartMutex';
            COMMIT TRANSACTION;
        END;
    
    END
    GO
    

    现在运行测试:

    EXEC    @return_value = [dbo].[InsertNewBodyPart]
        @bodypart = N'Stomach',
        @Model = 4
    
    SELECT  'Return Value' = @return_value
    
    SELECT * FROM BodyParts;
    SELECT * FROM BodyPartsCounter
    

    这一切都有效 - 但要小心 - 任何类型的多线程应用都需要考虑很多。

    希望这有帮助!

答案 3 :(得分:2)

我认为处理这种序列生成场景的最佳选择是建议的TT计数器表。我只想在这里向您展示TT实施的略微简化版本。

表:

CREATE TABLE dbo.counter_seq(model INT PRIMARY KEY, seq INT);
CREATE TABLE dbo.table_seq(part varchar(128), seq int, model int);

更简单的版本(无SELECT语句来检索当前seq):

DECLARE @target_model INT=3;
DECLARE @part VARCHAR(128)='Otra MAS';

BEGIN TRY
    BEGIN TRANSACTION;
    DECLARE @seq int = 1
    UPDATE dbo.counter_seq WITH(ROWLOCK,HOLDLOCK) SET @seq = seq = seq + 1 WHERE model=@target_model;
    IF @@ROWCOUNT = 0 INSERT INTO dbo.counter_seq VALUES (@target_model, 1);
    INSERT INTO dbo.table_seq(part,seq,model)VALUES(@part,@seq,@target_model);
    COMMIT
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH

答案 4 :(得分:0)

由于您希望序列基于特定模型,因此在执行select时只需将其添加到where子句中。这将确保Max(SEQ)仅适用于该模型系列。此外,因为SEQ可以将其包装在ISNULL中,所以如果它为null则为0,因此0 + 1将下一个设置为1。 这样做的基本方法是:

Observable.Never<Unit>()
    .TakeUntil(Observable.Empty<Unit>())
    .Wait();

答案 5 :(得分:0)

我不会尝试将"100"值存储在表格中。

正如您在评论中所说,您的SeqID,它会以非常有效且并发安全的方式自动增加服务器。用它来确定插入行的顺序以及应生成IDENTITY值的顺序。

然后使用ROW_NUMBER根据查询中的需要生成Seq Seq分区的值(序列从Model的每个值重新开始)。

Model

答案 6 :(得分:0)

insert into tableA (id,part,seq,model)
values
(6,'Groin',(select MAX(seq)+1 from tableA where model=3),3)

答案 7 :(得分:0)

create function dbo.fncalnxt(@model int)
returns int 
begin
declare @seq int
select @seq= case when @model=3 then max(id) --else
end from tblBodyParts
return @seq+1
end
--query idea To insert values, ideal if using SP to insert
insert into tblBodyParts values('groin',dbo.fncalnxt(@model),@model)

我猜你可以尝试一下。 一个新手,如果我错了,请纠正我。我建议使用函数来获取基于模型的seq列中的值; 你必须检查else的情况虽然返回你想要的另一个值,当model!= 3时,它现在将返回null。

答案 8 :(得分:0)

假设您有以下表格:

CREATE TABLE tab (
    id int IDENTITY(1,1) PRIMARY KEY,
    Part VARCHAR(32) not null,
    Seq int not null,
    Model int not null
);

INSERT INTO
    tab(Part,Seq,Model)
VALUES
    ('Head', 0, 3),
    ('Neck', 1, 3),
    ('Shoulders', 2, 29),
    ('Shoulders', 2, 3),
    ('Stomach', 5, 3);

下面的查询将允许您导入多个记录,而不会破坏model_seq

INSERT INTO
    tab (model, part, model_seq)
SELECT
    n.model,
    n.part,
    -- ensure new records will get receive the proper model_seq
    IFNULL(max_seq + model_seq, model_seq) AS model_seq
FROM
    (
        SELECT
            -- row number for each model new record
            ROW_NUMBER() OVER(PARTITION BY model ORDER BY part) AS model_seq,
            n.model,
            n.part,
            MAX(t.seq) AS max_seq
        FROM
            -- Table-values constructor allows you to prepare the
            -- temporary data (with multi rows),
            -- where you could join the existing one
            -- to retrieve the max(model_seq) if any
            (VALUES
                ('Stomach',3),
                ('Legs',3),
                ('Legs',29),
                ('Arms',1)
            ) AS n(part, model)
        LEFT JOIN
            tab
        ON
            tab.model = n.model
        GROUP BY
            n.model n.part
    ) AS t

我们需要row_number()来确保如果我们导入多个值,那么将保留订单。有关ROW_NUMBER() OVER() (Transact-SQL)

的更多信息

表值构造函数用于创建具有新值的表,并将MAX model_seq连接到模型。 您可以在此处找到有关表值构造函数的更多信息:Table Value Constructor (Transact-SQL)

相关问题