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 1
值Order
。
在注意到重复后,我们想出了解决方案 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 次后停止)。不确定这有多么必要。
我认为在插入值后尝试设置值的触发器是问题所在,如果您可以远离那里,您将获得更好的结果。