tags:

views:

67

answers:

5

I have various reasons for needing to implement, in addition to the identity column PK, a second, concurrency safe, auto-incrementing column in a SQL Server 2005 database. Being able to have more than one identity column would be ideal, but I'm looking at using a trigger to simulate this as close as possible to the metal.

I believe I have to use a serializable isolation level transaction in the trigger. Do I go about this like Ii would use such a transaction in a normal SQL query?

It is a non-negotiable requirement that the business meaning of the second incrementing column remain separated from the behind the scenes meaning of the first, PK, incrementing column.

To put things as simply as I can, if I create JobCards '0001', '0002', and '0003', then delete JobCards '0002' and '0003', the next Jobcard I create must have ID '0002', not '0004'.

+2  A: 

Just an idea, if you have 2 "identity" columns, then surely they would be 'in synch' - if not exactly the same value, then would differ by a constant value. If so, then why not add the "second identity" column as a computed column, which offsets the primary identity? Or is my logic flawed here?

Edit : As per Martin's comment, note that your calc might need to be N * id + C, where N is the Increment and C the offset / delta - excuse my rusty maths.

Edit : OP has restated the requirement (noting that identity columns in SQL do not reclaim used ID's once deleted) so this answer is no longer applicable.

nonnb
Damn - you type faster than I do. +1 for excellent suggestion.
ktharsis
Maybe they have different increments? I'm sure the OP must have his reasons!
Martin Smith
Yup, well spotted.
nonnb
@nonnb - But it would still be possible to calculate one from the other. I admit I'm curious about the reason as well.
Martin Smith
+1  A: 

A solution to this issue from "Inside Microsoft SQL Server 2008: T-SQL Querying" is to create another table with a single row that holds the current max value.

CREATE TABLE dbo.Sequence(
 val int 
 )

Then to allocate a range of sufficient size for your insert

CREATE PROC dbo.GetSequence
@val AS int OUTPUT,
@n as int =1
AS
UPDATE dbo.Sequence 
SET @val = val = val + @n;

SET @val = @val - @n + 1; 

This will block other concurrent attempts to increment the sequence until the first transaction commits.

For a non blocking solution that doesn't handle multi row inserts see my answer here.

Martin Smith
I've been told to not use another table.
ProfK
@ProfK This wouldn't work for your clarified requirement anyway.
Martin Smith
A: 

if you never delete from the table, you could create a view with a materialized column that uses ROW_NUMBER().

ALSO, a SQL Server identity can get out of sync with a user generated one, depending on the use of rollback.

KM
+1  A: 

I would disallow all the deletes from this table altogether. Instead of deleting, I would mark rows as available or inactive. Instead of inserting, I would first search if there are inactive rows, and reuse the one with the smallest ID if they exist. I would insert only if there are no available rows already in the table.

Of course, I would serialize all inserts and deletes with sp_getapplock.

You can use a trigger to disallow all deletes, it is simpler than filling gaps.

AlexKuznetsov
I am doing that, but my requirement is "if records are deleted", maybe not by the user, who can effectively only mark them Inactive, but maybe by a support person or dev.
ProfK
You can use a trigger to disallow all deletes, it is simpler than filling gaps.
AlexKuznetsov
A: 

Just use a regular identity and reseed on deletes.

create table reseedtest (
   a int identity(1,1) not null,
   name varchar(100)
)

insert reseedtest values('erik'),('john'),('selina')
select * from reseedtest

go
CREATE TRIGGER TR_reseedtest_D ON reseedtest FOR DELETE
AS
BEGIN TRAN
DECLARE @a int
SET @a = (SELECT TOP 1 a FROM reseedtest WITH (TABLOCKX, HOLDLOCK))
--anyone know another way to lock a table besides doing something to it?
DBCC CHECKIDENT(reseedtest, reseed, 0)
DBCC CHECKIDENT(reseedtest, reseed)
COMMIT TRAN
GO

delete reseedtest where a >= 2
insert reseedtest values('katarina'),('david')
select * from reseedtest

drop table reseedtest

This won't work if you are deleting from the "middle of the stack" as it were, but it works fine for deletes from the incrementing end.

Reseeding once to 0 then again is just a trick to avoid having to calculate the correct reseed value.

Emtucifor
+1 for the idea, but ouch, table lock!
nonnb
It cannot work. If you delete just one row in the middle, reseed your identity, and add two rows, you will get duplicates.
AlexKuznetsov
@AlexKuznetsov - that's what I said, please read carefully. This may not be the best answer possible, but you have to admit it is an interesting technique. It's minimalist and solves the problem. Also, the OP has not spoken whether he needs to handle gaps in the middle or not. His given scenario only shows deleting rows at the end.
Emtucifor
@nonnb Somewhere a lock has to occur in order for two sessions to not calculate the same value. A table lock is unfortunate, but what happens if someone inserts between the two DBCC statements? I honestly don't know how to get the current identity value into a variable and even if I did, does DBCC CHECKIDENT accept a variable as the third parameter?
Emtucifor
@AlexKuznetsov I just reread what you said, and realized it's incorrect. If you delete a row in the middle and reseed the identity to the end, you won't get duplicate rows, you'll just have a hole. Look carefully at my reseed code: it auto-adjusts to start after the last-used identity value.
Emtucifor
@Emtucifor: agreed, it will not work because there will be a hole, not because there will be a duplicate. Did you test how your approach works under high concurrency?
AlexKuznetsov
@AlexKuznetsov You're not making sense. The OP didn't talk about deleting from the middle but about deleting from the end. There will be no duplicates. I didn't test under high concurrency, but I can say with confidence that it will work fine, though be blocked while the reseeding is going on. Reseeding works fine even if there are missing IDs in the middle. Could you please be more specific about what the failure will be that you think will happen?
Emtucifor
@AlexKuznetsov - I think I just got an idea of what you're talking about. You mean deletes at the same time? That could indeed blow things up. I don't have time now, but I'll test it later. Perhaps it would have to be an instead of update trigger to lock the table before the delete. Not sure.
Emtucifor