Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate an increment ID that is unique for a given value of a foreign key

We are working on an ecommerce system where we aggregate orders coming in from different sellers. As you can easily imagine we have an Order table that holds the data for orders of all sellers. Each seller has a unique AccountID which is a foreign key in the Order table.

We want to generate an order number for each order that is coming into the system so that for a given seller (and given AccountID) those order numbers are creating a sequence (first order gets 1, then 2, then 3 etc).

We have tried a couple of solutions to this but they have drawbacks we would like to avoid. All of them are inside a trigger:

ALTER TRIGGER [dbo].[Trigger_Order_UpdateAccountOrderNumber] 
   ON [dbo].[Order]
   AFTER INSERT
BEGIN
     ...
END

Our Solution 1 was:

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

Note that we have we have READ_COMMITTED_SNAPSHOT ON. It seemed to work well for a while but recently we noticed some duplicate values in the AccountOrderNumber column. After analysing the code it seems logical that duplicates might appear as the operation is not atomic, so if 2 orders are added at the exact same time they will read the same TOP 1 value from the Order table.

After noticing the duplicates we came up with Solution 2 where we have a separate table to track the next AccountOrderNumber for each Account:

DECLARE @NewOrderNumbers TABLE
    (
        AccountID int, 
        OrderID int,
        AccountOrderNumber int
    }

The trigger body is a as follows in that case:

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] ...

While this solution did not create any duplicates it did cause deadlocks on the newly created @NewOrderNumbers table due to WITH (UPDLOCK). Unfortunately, the locking was necessary to avoid duplicates.

Our latest attempt (Solution 3) is to use sequences. For this we need to create a sequence for each Account in our system and then use it when a new orders are inserted. Here is the code that creates a sequence for AccountID = 1:

CREATE SEQUENCE Seq_Order_AccountOrderNumber_1 AS INT START WITH 1 INCREMENT BY 1 CACHE 100

And the trigger body for 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

The approach with sequences worries us because we expect to have 100K+ accounts in the system pretty soon and for each of those accounts we currently need that sort of incremented ID in 6 different tables. This means we will end up with hundreds of thousands of sequences, which might have a negative impact on performance of the whole DB. We don't know if it will have any impact, but it is close to impossible to find any testimony on the web from people who have used that many sequences in SQL Server.

Finally, the question is: Can you think of a better solution to the problem? It seems like this should be a pretty common use case where you need an ID that is incremented separately for every value of a foreign key. Maybe we are missing something obvious here?

We will also welcome your comments on the 3 solutions detailed above. Maybe one of them is close to being acceptable and just needs some minor tweaking?

like image 777
silvo Avatar asked May 18 '17 14:05

silvo


People also ask

Can a foreign key Autoincrement?

A foreign key is a link to a specific record in another table (or another record in the same table). Creating a foreign key field that is auto-incrementing would create a link to an arbitrary (and possibly non-existent) record, which would defeat the whole purpose of having a foreign key in the first place.

How can create auto-increment serial number in SQL Server?

The MS SQL Server uses the IDENTITY keyword to perform an auto-increment feature. In the example above, the starting value for IDENTITY is 1, and it will increment by 1 for each new record. Tip: To specify that the "Personid" column should start at value 10 and increment by 5, change it to IDENTITY(10,5) .

What is auto-increment in SQL?

Auto-increment allows a unique number to be generated automatically when a new record is inserted into a table. Often this is the primary key field that we would like to be created automatically every time a new record is inserted.


1 Answers

There are probably cleverer ways to achieve this but here is my quick proposal. Create a view that maintains the highest account order number; use schemabinding for speed and use the view to insert records.

View definition:

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

Then use it for insert statements if you are insert...selecting like so:

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

Or like so for insert...values:

insert into Orders (
    OrderID,
    AccountID,
    AccountOrderNumber,
    ...
    )
values (
    SomeAutoIncrementValue,
    12,
    (select Sequence + 1 from dbo.vwMaxAccountOrderNumber where AccountID = 12),
    ...
    )
like image 169
Anand Avatar answered Oct 12 '22 10:10

Anand