views:

1195

answers:

4

I have to insert some records in a table in a legacy database and, since it's used by other ancient systems, changing the table is not a solution.

The problem is that the target table has a int primary key but no identity specification. So I have to find the next available ID and use that:

select @id=ISNULL(max(recid)+1,1) from subscriber

However, I want to prevent other applications from inserting into the table when I'm doing this so that we don't have any problems. I tried this:

begin transaction
    declare @id as int
    select @id=ISNULL(max(recid)+1,1) from subscriber WITH (HOLDLOCK, TABLOCK)
    select @id
    WAITFOR DELAY '00:00:01'
    insert into subscriber (recid) values (@id)
commit transaction
select * from subscriber

in two different windows in SQL Management Studio and the one transaction is always killed as a deadlock victim.

I also tried SET TRANSACTION ISOLATION LEVEL SERIALIZABLE first with the same result...

Any good suggestions to how I can ensure that I get the next id and use that without risking that someone else (or me!) is getting hosed?

Sorry for not mentioning this earlier, but this is a SQL 2000 server so I can't use things like FOR UPDATE and OUTPUT

UPDATE: This is the solution that worked for me:

BEGIN TRANSACTION
    DECLARE @id int

    SELECT  @id=recid
    FROM    identities WITH (UPDLOCK, ROWLOCK)
    WHERE table_name = 'subscriber'

    waitfor delay '00:00:06'

    INSERT INTO subscriber (recid) values (@id)

    UPDATE identities SET recid=recid+1 
    WHERE table_name = 'subscriber'

COMMIT transaction

select * from subscriber

The WAITFOR is so that I can have multiple connections and start the query several times to provoke concurrency.

Thanks to Quassnoi for the answer and to all you other guys that contributed! Awesome!

+4  A: 

Add another table with an identity column and use this new table and column to select/generate your identity values for the old table.

Update: Depending on the frequency of INSERTS (and the number of existing rows e) you could seed your new IDENTITY values at e+x where x is sufficiently large. Thhis would avoid conflict with the legacy inserts. A sad solution, an imperfect one for sure, but something to think about?

Ed Guiness
+1 I used to do something similar where I create a table with just one column and one row and lock that table instead of using identity value. Locking a table with just one row and a column didn't seem to affect other transactions.
Sung Meister
This won't work since there is a legacy program which I have no control over that interacts with this database as well. So I can use the extra table but the other program will not.
henriksen
Updated with additional suggestion to avoid conflict with legacy inserts.
Ed Guiness
@edg - I have also used the e+x "hack" to get around the same issue. As you say, it's imperfect, but can fix a problem when nothing else is possible. Just have to be sure that x is sufficiently large to remove possibility of overlap between the "lower" id values and the "upper" id values.
CraigTP
The e+x might work, but it feels *really* hacky :)
henriksen
Yes it is a hack, but your options sound limited...or are they?
Ed Guiness
+4  A: 

Create another table:

t_identity (id INT NOT NULL PRIMARY KEY CHECK (id = 1), value INT NOT NULL)

with a single row, lock this row, and increment value by one each time you need an IDENTITY.

To lock, increment, and return the new value in a single statement, use:

UPDATE  t_identity
SET     value = value + 1
OUTPUT  INSERTED.value

If you don't want to update, just lock, then issue:

SELECT  value
FROM    t_identity WITH (UPDLOCK, ROWLOCK)

This will lock the table until the end of the transaction.

If you always first lock t_identity before messing with ancient_table, you will never get a deadlock.

Quassnoi
How would you lock and increment it? Similar to the way listed in the question?
KM
what about just using a single insert like my answer?
KM
INSERT will work too, but it will clog the table. Why keeping old identites?
Quassnoi
in my answer, I never create an extra table, I just insert into the actual table using a select max() with an output of the new id
KM
Yes, in case you have row-level locking on the table, this will work too. However, I'd still prefer another table so you can be more free in setting the original table parameters. +1 for your solution, nevertheless.
Quassnoi
This won't work since there is a legacy program which I have no control over that interacts with this database as well. So I can use the extra table but the other program will not.
henriksen
Also, the "FOR OUTPUT" won't work on a SQL 2000 box either
henriksen
From your post I can conclude that a stored procedure is used for inserts. If it's the case, then you can hide all details (including the extra table) into the procedure. FOR OUTPUT is not mandatory, you can use UPDATE first, then SELECT. UPDATE will lock the table as well.
Quassnoi
on my SQL Server 2005:SELECT valueFROM t_identityFOR UPDATEresults in:Msg 1003, Level 15, State 2, Line 3Line 3: FOR UPDATE clause allowed only for DECLARE CURSOR.
KM
Um... yes. See post update
Quassnoi
UPDATE is not available in SQL 2000, but the other way worked fine. Thank you!
henriksen
+2  A: 

EDIT this is basically the method purposed by @Quassnoi, I just implement it in a loop so you can run it against multiple windows at the same time to see it works great.

set up:

create user's existing table:

create table Subscriber
(
recid  int not null primary key
)

create new table to keep track of the missing identity, you could add an extra column to keep track of a table if this is needed for multiple tables, but I hve not done that in this example:

CREATE TABLE SubscriberIDs
(
SubscriberID int
)
insert into SubscriberIDs values (0) --row must exist first

create test script, put this into multiple windows and run them at the same time:

declare @idtable table --will hold next ID to use
(
id int
)
declare @x  int
declare @y  int
set @x=0
while @x<5000 --set up loop
begin
    set @x=@x+1
    begin transaction
    --get the next ID to use, lock out other users
    UPDATE SubscriberIDs
        SET SubscriberID= SubscriberID+ 1
        OUTPUT  INSERTED.SubscriberID
        INTO @idtable
    --capture the next id from temp table variable
    select @y=id from @idtable
    --print @y
    --use the next id in the actual table
    insert into subscriber values (@y)

    commit
    --print @x
    waitfor delay '00:00:00.005'
end --while

---------------------------------------------------------------
EDIT here is my original attempt, which will eventually get some deadlocks when run in a loop and in multiple windows at the same time. The above method always works. I tried all combinations of transactions, with(holdlock), and set transaction isolation level serializable, etc. but could not get it to run as well as the above method.

set up:

create table subscriber
(
recid  int not null primary key
)

used to capture the id:

declare @idtable table
(
id int
)

the insert:

insert into subscriber
    OUTPUT INSERTED.recid
        recid
    INTO @idtable
    SELECT ISNULL(MAX(recid),0)+1 FROM subscriber

list the new id:

select * from @idtable

list all the ids:

select * from subscriber
KM
Will this prevent another program from selecting the max recid in the subscriber table and insert a row with the same ID I'm going to use? I have no control over the other legacy program.
henriksen
A: 

You shouldn't get a deadlock here as the second should just wait for the first to complete. Your issue is that you are creating a transaction, then adding another lock while in that transaction.

Also, you are getting the ID then using it in two separate statements, whereas you could do it all in one solution:

set transaction isolation level serializable
begin transaction
    insert into subscriber (recid) 
       SELECT (select ISNULL(max(recid)+1,1) from subscriber)
commit transaction
select * from subscriber

This should ensure that you have only consistency on your inserts. However, as you specify that legacy applications are also using this table, can you be certain that when they insert new records it won't conflict with this?

ck
This is basically the same as my first attempt. I tried this in a loop and in multiple windows at the same time, and it always resulted in deadlocks.
KM
The legacy app is the reason I have this problem. I want to lock the table but not make the other app fail with a deadlock...
henriksen