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

sil*_*lvo 7 sql t-sql sql-server

我们正在开发一个电子商务系统,我们汇总来自不同卖家的订单.正如您可以轻易想象的那样,我们有一个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)

使用序列的方法让我们担心,因为我们希望很快就会在系统中拥有100K +帐户,并且对于每个帐户,我们当前需要在6个不同的表中增加ID.这意味着我们最终会有数十万个序列,这可能会对整个数据库的性能产生负面影响.我们不知道它是否会产生任何影响,但几乎不可能在网络上找到任何在SQL Server中使用过那么多序列的人的证词.

最后,问题是:你能想出更好的解决方案吗?看起来这应该是一个非常常见的用例,你需要一个ID,它对于外键的每个值都是单独增加的.也许我们错过了一些明显的东西?

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

Ana*_*and 1

可能有更聪明的方法来实现这一目标,但这是我的快速建议。创建维护最高账户订单号的视图;使用模式绑定来提高速度并使用视图来插入记录。

查看定义:

create view dbo.vwMaxAccountOrderNumber
with schemabinding as
select o.AccountID,
    max(o.AccountOrderNumber) as Sequence
from Orders as o
group by o.AccountID
Run Code Online (Sandbox Code Playgroud)

然后将它用于插入语句,如果你insert...selecting像这样:

insert into Orders (
    OrderID,
    AccountID,
    AccountOrderNumber,
    ...
    )
select SomeAutoIncrementValue,
       AccountID,
       vmaon.Sequence + row_number() (partition by a.AccountID order by SomeOtherColumn)
from Accounts as a
inner join dbo.vwMaxAccountOrderNumber as vmaon
on vmaon.AccountID = a.AccountID
inner join ...
Run Code Online (Sandbox Code Playgroud)

或者像这样insert...values

insert into Orders (
    OrderID,
    AccountID,
    AccountOrderNumber,
    ...
    )
values (
    SomeAutoIncrementValue,
    12,
    (select Sequence + 1 from dbo.vwMaxAccountOrderNumber where AccountID = 12),
    ...
    )
Run Code Online (Sandbox Code Playgroud)