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

Posted

技术标签:

【中文标题】如何获取序列中的下一个数字【英文标题】:How to get the next number in a sequence 【发布时间】:2016-05-17 15:06:44 【问题描述】:

我有一张这样的桌子:

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

对于 Model 3,我如何在 Stomach 之后插入另一条记录,下面是新表的样子:

+----+-----------+------+-------+--+
| 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     |  |
+----+-----------+------+-------+--+

有没有一种方法可以制作一个插入查询,它只会在 Model 3 的最高序列之后给出下一个数字。另外,寻找并发安全的东西。

【问题讨论】:

是否应该将 null Seq 视为 0?还是负 1 ? 我将架构更改为不允许空值。因此,空值更改为 0 【参考方案1】:

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

    WITH(TABLOCKX, HOLDLOCK) WITH(ROWLOCK, XLOCK, HOLDLOCK)

TABLOCKX + HOLDLOCK 有点矫枉过正。它会阻塞常规的 select 语句,即使事务很小,也可以认为是 heavy

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

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

请注意,根据以这种方式锁定的行数,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

另一种可能更好的主意是有一个 counter 表,并在 counter 表上提供这些表提示。该表如下所示:

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 语句。

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


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

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

【讨论】:

您认为这是一笔沉重的交易吗? @Luke101 已修改答案以考虑交易的重要性。 你好,我要实现这个解决方案,看看它是如何工作的。我会告诉你的。 我喜欢(并且我使用)计数器表方法,但是您的实现有一点缺陷,当两个并发会话尝试获取不存在的下一个 seq 时,可能会发生主键冲突错误模型。可能失败的语句是INSERT INTO dbo.counter_seq(model,seq)VALUES(@target_model,@new_seq) @JesúsLópez 这根本不是真的。您可以从一个空的计数器表开始轻松地验证这一点(我刚刚做了),在检索计数器后使用此脚本两次,其中一个具有WAITFOR DELAY。启动一个等待,然后启动另一个。您将看到第二个将等待第一个完成,特别是在检索计数器时。两者都将成功完成,没有任何重复。【参考方案2】:

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

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

查询的问题,例如:

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

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

【讨论】:

是的,一些序列号可以为空。此外,它们也都可以为空。有办法解决吗? 如果这些值旨在枚举给定模型的记录,为什么要允许 NULL 值? 我已将表更改为没有 NULL 值。现在我面临并发问题。这个解决方案并发安全吗?此外,增量必须仅适用于 Model 3。 @Luke101 。 . .当前的解决方案是使用identity 列或序列。 是的,我在 id 字段上使用了一个身份。我必须只为Model: 3 而不是整个表增加Seq 字段。您的解决方案将为整个表找到最大值。另外,我在某处读到 max 对于并发不安全。这是真的吗?【参考方案3】:

让我们首先列出挑战:

    我们不能使用正常的约束,因为存在现有的空值,如果我们查看现有数据,我们还需要处理重复和间隙。 这很好,我们会在第 3 步中解决的;->

    我们需要并发操作的安全性(因此某种形式或混合的事务、隔离级别以及可能的“有点 SQL 互斥体”。)出于以下几个原因,直觉认为这里是一个存储过程:

    2.1 更容易防止sql注入

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

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

    我们必须在每次插入时存储或查找下一个值。并发这个词已经告诉我们会有争用并且可能会有高吞吐量(否则请坚持使用单线程)。所以我们一定已经在思考:在一个已经很复杂的世界里,不要从你想写入的同一张表中读取数据。

所以,有了这个简短的前传,让我们尝试一个解决方案:

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

    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
    ;

然后我们需要创建存储过程来发挥作用。简而言之,它充当互斥锁,基本上保证您的并发性(如果您不对其他地方的相同表进行插入或更新)。然后它获取下一个序列,更新它并插入新行。在这一切发生后,它将提交事务并释放存储的过程以供下一个等待调用线程。

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

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

希望这会有所帮助!

【讨论】:

通过对 sp 稍作更改,您可以根据模型存储下一个序列,其中 id 字段可以映射到您的模型并存储该模型的下一个序列。只需在 select 和 update 语句中将 'where id = 1' 更改为 'where id = @model' 即可。 此方法是否获取表锁?如果是这样,这可能有点慢。我一次在网站上有大约 100 个并发用户。我会说大约 2-3 个用户可能会尝试一次更新表格。 @Luke101 不会获得表锁。但是,它将在存储的过程(互斥锁)上获得“锁定”。但没有它,你几乎肯定会遇到死锁。完整的调用在 3 毫秒内执行,并且不会随着表的增长而增长,因为我们正在为序列使用单行表。如果您使用 MAX(Seq) 选项,随着表的增长,随着行数的增加,找到最大值需要更长的时间,您还将拥有必须在 seq 字段上维护的索引。没必要。我建议你计算一下你的交易量,看看这是否合适。 这很棒。我已经实现了这个并且工作正常。我认为这是迄今为止最好的解决方案。我想再等几天再颁奖。谢谢 @Luke101 这里的缺点是有一个锁来获取所有模型的新序列ID。如果您有三个进程想要分别插入不同的模型,则它们都必须相互等待。这类似于排他表锁(在本例中位于计数器表上)。要修改这种工作方式以获得行级锁定,需要将模型 ID 嵌入到 @Resource 参数中。但是,如果您可以使用表提示在 SQL 语言中请求这种锁定,为什么还要为此烦恼呢?我严重怀疑海报是否在生产中使用它。【参考方案4】:

我相信处理这种序列生成场景的最佳选择是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

【讨论】:

【参考方案5】:

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

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

【讨论】:

【参考方案6】:

我首先不会尝试将Seq 值存储在表中。

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

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

SELECT
    ID
    ,Part
    ,Model
    ,ROW_NUMBER() OVER(PARTITION BY Model ORDER BY ID) AS Seq
FROM YourTable

【讨论】:

在某些情况下,用户可以更改记录的顺序。 好的。有效点。在这种情况下,您必须存储Seq。但是,您真的需要为每个Model 设置单独的序列吗?我会将Seq 默认设置为ID(插入行时),然后允许用户交换两个Seq 值以调整两行的顺序。【参考方案7】:
insert into tableA (id,part,seq,model)
values
(6,'Groin',(select MAX(seq)+1 from tableA where model=3),3)

【讨论】:

【参考方案8】:
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。

【讨论】:

【参考方案9】:

假设你有下表:

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);

下面的查询将允许您导入多条记录,而不会破坏模型序列

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)

【讨论】:

以上是关于如何获取序列中的下一个数字的主要内容,如果未能解决你的问题,请参考以下文章

如果上一个值为空,如何获取下一个值的下一个

如何根据一行中的数字序列获取mysql-table列的最小值

如何找到现有数组的下一个数字索引?

如何找到现有数组的下一个数字索引?

如何获取 Laravel 集合中的下一个项目?

我如何确定下一个记录号 (PK) 是啥?