Reputation: 43
I have a primary key OrderHeaderID that auto increments, but i also need to auto increment the OrderNumber based what company the order is placed for.
Current Strategy: (On my insert statement to the table)
(SELECT MAX(OrderNumber) + 1 FROM OrderHeader WHERE CompanyID = @CompanyID)
Issue: once i started testing with some volume, i started getting duplicate key errors:
Table OrderHeader:
OrderHeaderID CompanyID OrderNumber
1 1 10000
2 1 10001
3 1 10002
4 2 10000
5 2 10001
6 2 10002
Upvotes: 4
Views: 3247
Reputation: 407
I would use a function to auto-increment the OrderNumber per a given ID.
Here is what I put together in 2 minutes. It does what you are asking.
create table dbo.OrderHeader (
OrderHeader int identity(1,1) primary key
,CompanyID int
,OrderNumber int
)
go
create function dbo.NextOrderNumber (
@CompanyID int
)
returns int
as
begin
declare @result int;
select @result = OrderNumber + 1
from OrderHeader
where CompanyID = @CompanyID
if @result is null
set @result = 10000
return @result;
end
go
insert into OrderHeader select 1, dbo.NextOrderNumber(1)
insert into OrderHeader select 2, dbo.NextOrderNumber(1)
insert into OrderHeader select 2, dbo.NextOrderNumber(1)
insert into OrderHeader select 1, dbo.NextOrderNumber(1)
select *
from OrderHeader
Upvotes: 0
Reputation: 74267
take a look at my answer here: sql server: generate primary key based on counter and another column value
For your purposes, you could define your company table thus:
create table dbo.company
(
id int not null primary key ,
name varchar(32) not null unique ,
order_counter not null default(0) ,
...
)
and your order table thus:
create table dbo.order
(
company_id int not null foreign key references dbo.company( id ) ,
id int not null ,
order_number as 100000*company_id + id ,
...
constraint order_AK01 unique nonclustere ( order_number ) ,
constraint order_PK01 primary key clustered ( company_id , id ) ,
)
And set up your "add order" query thus:
declare @new_order_number int
update dbo.company
set @new_order_number = dbo.company.order_counter + 1 ,
order_counter = dbo.company.order_counter + 1
where dbo.company.id = @some_company_id
insert dbo.order ( company_id , id ) value ( @some_company_id , @new_order_number )
You have no concurrency (race) conditions: the "interlocked update" takes care of that. Further, you haven't denormalized the database design (1st normal form requires that every row and column intersection be atomic/non-decomposable: that it contain exactly one value from the applicable domain and nothing else. Compound identifers like yours are verboten.)
Easy!
Upvotes: 1
Reputation: 37388
To solve your concurrency issue, you'll have to raise the isolation level of your SELECT
statement, and then subsequently perform the INSERT
inside of the same transaction.
The specific syntax of this will vary for each RBDMS, but here's an example using Sql Server:
BEGIN TRANSACTION
DECLARE @OrderNumber INT
SELECT @OrderNumber = MAX(OrderNumber) + 1
FROM OrderHeader WITH (XLOCK)
WHERE CompanyID = @CompanyID
INSERT... (@OrderNumber)
COMMIT
This places an exclusive lock on the rows read during the SELECT
statement, which will prevent another instance of this routine from running concurrently. Instead, the second instance will be blocked until the original process performs the INSERT
and COMMIT
, at which point the second instance will then proceed to read and lock the newly generated value.
Upvotes: 3