为给定的外键值生成唯一的增量 ID

sil*_*lvo 6 sql-server t-sql azure-sql-database

免责声明:这个问题最初是在 SO 上被问到的,但在那里并没有太大的吸引力,所以我在这里尝试,希望它对 DBA 更有趣......


我们正在开发一个电子商务系统,在该系统中我们汇总来自不同卖家的订单。正如您可以轻松想象的那样,我们有一个Order包含所有卖家订单数据的表格。每个卖家都有一个唯一的AccountID,它是Order表中的外键。

我们希望为进入系统的每个订单生成一个订单号,以便对于给定的卖家(和给定AccountID),这些订单号正在创建一个序列(第一个订单获得 1,然后是 2,然后是 3 等)。

我们已经尝试了几种解决方案,但它们有我们想要避免的缺点。所有这些都在触发器中:

ALTER TRIGGER [dbo].[Trigger_Order_UpdateAccountOrderNumber] 
   ON [dbo].[Order]
   AFTER INSERT
BEGIN
     ...
END
Run Code Online (Sandbox Code Playgroud)

我们的解决方案 1是:

UPDATE
    [Order]
SET
    AccountOrderNumber = o.AccountOrderNumber
FROM
(
    SELECT
        OrderID,
        AccountOrderNumber =
            ISNULL((SELECT TOP 1 AccountOrderNumber FROM [Order] WHERE AccountID = i.AccountID ORDER BY AccountOrderNumber DESC), 1) +
            (ROW_NUMBER() OVER (PARTITION BY i.AccountID ORDER BY i.OrderID))
    FROM
        inserted AS i
) AS o
WHERE [Order].OrderID = o.OrderID
Run Code Online (Sandbox Code Playgroud)

请注意,我们有我们有READ_COMMITTED_SNAPSHOT ON。它似乎运行良好,但最近我们注意到AccountOrderNumber列中有一些重复的值。分析代码后,由于操作不是原子操作,可能会出现重复项似乎是合乎逻辑的,因此如果在完全相同的时间添加 2 个订单,它们将从表中读取相同的TOP 1Order

在注意到重复后,我们想出了解决方案 2,其中我们有一个单独的表来跟踪AccountOrderNumber每个的下一个Account

DECLARE @NewOrderNumbers TABLE
    (
        AccountID int, 
        OrderID int,
        AccountOrderNumber int
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,触发器主体如下:

INSERT INTO @NewOrderNumbers (AccountID, OrderID, AccountOrderNumber)
    SELECT
        I.AccountID,
        I.OrderID,
        ASN.Number + (ROW_NUMBER() OVER (PARTITION BY I.AccountID ORDER BY I.OrderID))
    FROM
        inserted AS I
        INNER JOIN AccountSequenceNumber ASN WITH (UPDLOCK) ON I.AccountID = ASN.AccountID AND ASN.AccountSequenceNumberTypeID = @AccountOrderNumberTypeID


    UPDATE [Order] ...
Run Code Online (Sandbox Code Playgroud)

虽然此解决方案没有创建任何重复项,但@NewOrderNumbers由于WITH (UPDLOCK). 不幸的是,锁定是必要的,以避免重复。

我们最新的尝试(解决方案 3)是使用序列。为此,我们需要为Account系统中的每个创建一个序列,然后在插入新订单时使用它。下面是创建序列的代码AccountID = 1

CREATE SEQUENCE Seq_Order_AccountOrderNumber_1 AS INT START WITH 1 INCREMENT BY 1 CACHE 100
Run Code Online (Sandbox Code Playgroud)

和触发器主体AccountID = 1

    DECLARE @NumbersRangeToAllocate INT = (SELECT COUNT(1) FROM inserted);

    DECLARE @range_first_value_output SQL_VARIANT; 
    EXEC sp_sequence_get_range N'Seq_Order_AccountOrderNumber_1', @range_size = @NumbersRangeToAllocate, @range_first_value = @range_first_value_output OUTPUT; 

    DECLARE @Number INT = CAST(@range_first_value_output AS INT) - 1;
    UPDATE 
        o
    SET 
        @Number = o.AccountOrderNumber = @Number + 1
    FROM 
        dbo.[Order] AS b JOIN inserted AS i on o.OrderID = i.OrderID
Run Code Online (Sandbox Code Playgroud)

使用序列的方法让我们感到担忧,因为我们预计系统中很快就会有 10 万多个帐户,而对于这些帐户中的每一个,我们目前需要在 6 个不同的表中增加这种 ID。这意味着我们最终会得到数十万个序列,这可能会对整个数据库的性能产生负面影响。我们不知道它是否会产生任何影响,但几乎不可能在网络上找到任何在 SQL Server 中使用过这么多序列的人的证词。

最后,问题是:你能想到一个更好的解决问题的方法吗?看起来这应该是一个非常常见的用例,您需要一个 ID,该 ID 为外键的每个值单独增加。也许我们在这里遗漏了一些明显的东西?

我们也欢迎您对上述 3 种解决方案提出意见。也许其中一个接近可以接受,只需要一些小的调整?

小智 1

没有提到,但我会在 AccountID 和订单号上放置一个唯一的约束/索引,以帮助防止重复(如果重复项已经存在并且无法清理,则可能不可能)。

在我使用过的系统中,要么在 INSERT 操作期间通过获取当前最高值并加 1 来生成 ID,要么使用序列表(如您提到的)。我个人不喜欢顺序方法;这意味着依赖一个完全不相关的表来生成本质上只是递增的数字,并且还有其他方法可以做到这一点。

要在代码中设置订单号,您必须获取给定 AccountID 的订单号当前最高值,然后添加到其中。假设 AccountID 作为参数传入:

INSERT INTO dbo.Orders
(
      AccountID
    , AccountOrderNo
)
SELECT
    AccoutID = @AccountID
    , AccountOrderNo = chk + 1
FROM
    (
        SELECT
            currAccountOrderNo = MAX(AccountOrderNo)
        FROM
            dbo.Orders
        WHERE
            (AccountID = @AccountID)
    ) AS chk
;
Run Code Online (Sandbox Code Playgroud)

我认为如果你把它从触发器中取出来,你应该不会遇到重复的问题。如果仍然得到重复项,UNIQUE 约束将阻止这种情况。我可以看到使用 UNIQUE 违规错误来驱动代码自动重试的可能选项,并使用 WHILE 循环来检查状态值(良好或失败)和重试尝试(重试 5 次后停止)。不确定这有多么必要。

我认为在插入值后尝试设置值的触发器是问题所在,如果您可以远离那里,您将获得更好的结果。