views:

87

answers:

3

This question certainly applies to a much broader scope, but here it is.

I have a basic ecommerce app, where users can, naturally enough, place orders. Said orders need to have a unique number, which I'm trying to generate right now.

Each order is Vendor-specific. Basically, I have an OrderNumberInfo (VendorID, OrderNumber) table. Now whenever a customer places an order I need to increment OrderNumber for a particuar Vendor and return that value. Naturally, I don't want other processes to interfere with me, so I need to exclusively lock this row somehow:

begin tranaction

    declare @n int
    select @n = OrderNumber 
      from OrderNumberInfo 
      where VendorID = @vendorID

    update OrderNumberInfo 
      set OrderNumber = @n + 1 
      where OrderNumber = @n and VendorID = @vendorID

commit transaction

Now, I've read about select ... with (updlock rowlock), pessimistic locking, etc., but just cannot fit all this in a coherent picture:

  • How do these hints play with SQL Server 2008s' snapshot isolation?
  • Do they perform row-level, page-level or even table-level locks?
  • How does this tolerate multiple users trying to generate numbers for a single Vendor?
  • What isolation levels are appropriate here?
  • And generally - what is the way to do such things?

EDIT

Just to make few things clearer:

  • Performance in this particular corner of the app is absolutely not an issue: orders will be placed relatively infrequently and will involve an expensive call to vendors' web service, so 1-second delay is pretty tolerable
  • We really need to have each vendors' order numbers to be independent and sequential
+3  A: 

You could use an OUTPUT clause. This should do it all atomically without requiring a transaction.

-- either return the order number directly as a single column resultset
UPDATE OrderNumberInfo 
SET OrderNumber = OrderNumber + 1
    OUTPUT DELETED.OrderNumber
WHERE VendorID = @vendorID


-- or use an intermediate table variable to get the order number into @n
DECLARE @n INT
DECLARE @temp TABLE ( OrderNumber INT )

UPDATE OrderNumberInfo 
SET OrderNumber = OrderNumber + 1
    OUTPUT DELETED.OrderNumber
    INTO @temp ( OrderNumber )
WHERE VendorID = @vendorID

SET @n = (SELECT TOP 1 OrderNumber FROM @temp)

The examples above assume that the VendorID column has a unique constraint, or at the very least that there'll only be one row per vendor ID. If that's not the case then you'll potentially be updating and/or returning multiple rows, which doesn't seem like a good idea!

LukeH
That's interesting! Thanks!
Anton Gogolev
@LukeH - If the OrderNumber column represents the last used OrderNumber, then wouldn't you want Inserted.OrderNumber?
Thomas
@Thomas: I'm just mimicking the behaviour of the code given in the question, which gets the order number first and then increments the column. But if you wanted to increment and then get the incremented order number then `INSERTED.OrderNumber` would be the way to do it.
LukeH
+4  A: 

Your solution will create a potential performance bottleneck on OrderNumberInfo table.

Is there any specific reason why the orders can't simply be an identity column, possibly prefixed with a vendor ID on application side (e.g. MSFT-232323)?

The only drawback of this approach is that per-vendor orders will not be an "Add-1-to-get-next-order-#" pattern, but I'm not aware of any technical or business consideration of why that would present a problem, though it might make in-sequence order processing slightly more complicated.

They'd still be incremented and unique per-vendor which is the only real requirement for an order ID.

It will, of course have the added side benefit of very easy vendor-independent logic assuming you ever have any) - such as application-wide QC/reporting.

DVK
Unfortunatelly, this _is_ the requirement: each Vendor needs to have an independent sequence.
Anton Gogolev
@Anton - ok... sorry for being dense, but what is the source/business reason for this requirement?
DVK
Agree with DVK, you are doing something totally unneeded and risky. IF I had this as a requirement, I would push back and tell them the risk outweighs any perceived benefits. As long as each vendor has unique orderids, then all is fine, it's not like they are going to see each other's IDs and there is literally no valid business reason why orders have to be in sequential order with no skipped values. The added cost of doing this and the added risk of data integrity problems if you don't get it right the first time are much higher than using an identity field, for literally no benefit. None.
HLGEM
@Anton - and, if the percieved requirement is "Vendor should not guess relative volume of other vendors based on IDs", simply change the fill rate of the identity column (which will assuage auditors), and explicitly state in your documentation that "order IDs are incremented by arbitrary increments, so don't base any logic you have on order # pattern aside from them being in increasing sequence"
DVK
@DVK, @HLGEM Well, it's Russia, my friends :) Other than that, there's an explicit requirement which states that order numbers should reflect actual number of orders made for each particular vendor.
Anton Gogolev
1) I just had this flashback of a programmer yelling at his project manager: THIS ... IS... RUSSIA!!! (while dressed in Spartan clothing ala "300") :)
DVK
2) Being it's Russia, you have a wide and exquisite supply of vocabulary to tell whoever came up with that requirement what to do with it... I can supply samples but that'd have to be a different StackOverflow Q :)
DVK
@Anton - one solution (depending on when they want to see the data) is to offer them an additional field (# of orders) in their order feed (I assume they get a feed of data instead of direct access to your DB), at which point you calculate the order sequence # in bulk, either in SQL (you can do that in Oracle) and populate it in the table, or on-the-fly in the ap code which produces the feed.
DVK
+1  A: 

I normally use something like this:

update OrderNumberInfo with (rowlock)
set @OrderNumber = OrderNumber, OrderNumber = OrderNumber + 1
where VendorID = @VendorID

It does not need to be wrapped in a transaction. In fact if you do wrap it in a transaction, then SQL Server will start holding locks on the table and slow everything down. When I need to do things like this in a web service, I always execute it on a separate database connection outside any transaction that might be open at the time, just to make sure.

I believe (but have not proved) that SQL Server uses a latch rather than a transaction to make it atomic, which should be more efficient.

If your table design is such that the vendor row needs to be created on demand if it doesn't exist, then use this logic instead:

declare @error int, @rowcount int

-- Attempt to read and update the number.
update OrderNumberInfo with (rowlock)
set @OrderNumber = OrderNumber, OrderNumber = OrderNumber + 1
where VendorID = @VendorID

select @error = @@error, @rowcount = @@rowcount
if @error <> 0 begin
    return @error
end

-- If the update succeeded then exit now.
if @rowcount > 0 begin
    return 0
end

-- Insert the row if it doesn't exist yet.
insert into OrderNumberInfo (VendorID, OrderNumber)
select VendorID, 1
where not exists (select null from OrderNumberInfo where VendorID = @VendorID)

select @error = @@error
if @error <> 0 begin
    return @error
end

-- Attempt to read and update the number.
update OrderNumberInfo with (rowlock)
set @OrderNumber = OrderNumber, OrderNumber = OrderNumber + 1
where VendorID = @VendorID

select @error = @@error
if @error <> 0 begin
    return @error
end

This code still doesn't require a transaction, because each atomic statement will succeed regardless of how many other connections are executing the code simultaneously.

Disclaimer: I have used this without problems on SQL Server 7-2005. I cannot yet comment on its behaviour in 2008.

Christian Hayter