Get next primary key without identity

  • I have a table to store int primary keys. Where the value nextID is the value to use.

    I need to get the value and update it (nextID + 1) before the next person gets it.

    I thought using tran would work but it doesn't stop a select.

    How do I get a value (lock the table for the update), update the table (preventing any selects until done) and release the table.

    BEGIN TRAN

    UPDATE TableIds

    SET NextId = NextId + 1

    WHERE TableName = 'Users'

    WAITFOR DELAY '00:00:20'

    SELECT NextId

    FROM TableIds

    WHERE TableName = 'Users'

    COMMIT TRAN

    I really want to do this in the reverse order but it doesn't work.

    But if I do this and have another query to just do a select:

    SELECT NextId

    FROM TableIds

    WHERE TableName = 'Users'

    This query never returns. Doesn't the COMMIT TRAN release the lock on the table?

    Thanks,

    Tom

  • This should do what you need.

    Why do you have WAITFOR DELAY '00:00:20' in your sample code?

    declare @NextId_output table ( NextId int not null )

    UPDATE TableIds

    SET

    NextId = NextId + 1

    output

    inserted.NextId

    into

    @NextId_output

    WHERE

    TableName = 'Users'

    select NextId from @NextId_output

  • I was putting the waitfor in the query to test the locking and to see when it releases.

    The only problem is that the 2nd query (select) never comes back.

    If I run:

    select cmd,* from sys.sysprocesses

    where blocked > 0

    It shows the 2nd query as being blocked and will stay blocked until I kill the blocking query (the first one). Why is it blocking the 2nd query after it finishes?

    Thanks,

    Tom

  • Presumably because you still have an open transaction. Run SELECT @@trancount in the first window. Presumably you already had an open transaction when you issued the BEGIN TRANSACTION in your script. In this case BEGIN and COMMIT transactions only increase trancount; nothing is actually committed until trancount reaches zero.

    [font="Times New Roman"]Erland Sommarskog, SQL Server MVP, www.sommarskog.se[/font]

  • A couple of questions.

    You're in SQL 2014, and want to select an INT. Have you looked at SEQUENCE? It does exactly that.

    If you want to lock rows for the duration of a transaction from being read, or better keep anyone else from getting into the table at all until you're done, SELECT * FROM tbl (TABLOCKX) WHERE NextID = 'Yes'. That'll open an exclusive table lock on the table for the transaction. Noone else is getting in there at that point until you commit or rollback.

    If this is just a giant IDENTITY() table stuffed off to the side, why don't you simply put IDENTITY() on the tables that need it? What purpose is this table of IDs serving?


    - Craig Farrell

    Never stop learning, even if it hurts. Ego bruises are practically mandatory as you learn unless you've never risked enough to make a mistake.

    For better assistance in answering your questions[/url] | Forum Netiquette
    For index/tuning help, follow these directions.[/url] |Tally Tables[/url]

    Twitter: @AnyWayDBA

  • Evil Kraig F (8/1/2014)


    You're in SQL 2014, and want to select an INT. Have you looked at SEQUENCE? It does exactly that.

    Beware that sequence can give you gaps. Sometimes there are business requirements that calls for an contiguous series of numbers. In this case, you must roll your own.

    [font="Times New Roman"]Erland Sommarskog, SQL Server MVP, www.sommarskog.se[/font]

  • tshad (7/31/2014)


    I have a table to store int primary keys. Where the value nextID is the value to use.

    I need to get the value and update it (nextID + 1) before the next person gets it.

    I thought using tran would work but it doesn't stop a select.

    How do I get a value (lock the table for the update), update the table (preventing any selects until done) and release the table.

    BEGIN TRAN

    UPDATE TableIds

    SET NextId = NextId + 1

    WHERE TableName = 'Users'

    WAITFOR DELAY '00:00:20'

    SELECT NextId

    FROM TableIds

    WHERE TableName = 'Users'

    COMMIT TRAN

    I really want to do this in the reverse order but it doesn't work.

    But if I do this and have another query to just do a select:

    SELECT NextId

    FROM TableIds

    WHERE TableName = 'Users'

    This query never returns. Doesn't the COMMIT TRAN release the lock on the table?

    Thanks,

    Tom

    If you insist on using a home-grown sequence table, the following will keep others from "getting in" at the same time and will also help you avoid a total world of hurt when it come to deadlocks.

    DECLARE @NextId INT

    UPDATE TableIds

    SET @NextId = NextId = NextId + 1

    WHERE TableName = 'Users'

    ;

    SELECT @NextID

    ;

    Unfortunately, that also relegates you to a single row at a time just as your original code did, Will you ever be doing inserts on, say, the "Users" table in multi-row batches?

    Be aware that if the code that uses this has an explicit transaction around it, fails, and rolls back, you could also experience gaps.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Just a quick thought on the problem, why not use the ROWLOCK hint?

    To try it out, run the create script first and then the Update and the Select code simultaneously.

    😎

    Create table and insert sample data

    USE tempdb;

    GO

    CREATE TABLE dbo.TableIds

    (

    TableIds_ID INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_DBO_TABLEIDS_TABLEIDS_ID PRIMARY KEY CLUSTERED

    ,NextId INT NOT NULL

    ,TableName NVARCHAR(128) NOT NULL

    );

    INSERT INTO dbo.TableIds (NextId,TableName)

    VALUES

    (1000,'Users')

    ,(1000,'Contacts')

    ,(1000,'Employees')

    ,(1000,'Addresses');

    Update code

    USE tempdb;

    GO

    SET NOCOUNT ON;

    DECLARE @ITER INT = 0 ;

    DECLARE @COUNT INT = 100000;

    DECLARE @NextId INT = 0 ;

    WHILE @ITER < @COUNT

    BEGIN

    UPDATE T WITH (ROWLOCK)

    SET T.NextId = T.NextId + 1

    FROM dbo.TableIds T

    WHERE TableName = 'Users';

    SELECT @NextId = NextId FROM dbo.TableIds;

    SET @ITER += 1;

    END

    Select code

    USE tempdb;

    GO

    SET NOCOUNT ON;

    DECLARE @ITER INT = 0 ;

    DECLARE @COUNT INT = 10000000;

    DECLARE @BUCKET INT = 0 ;

    WHILE @ITER < @COUNT

    BEGIN

    SELECT @BUCKET = NextId FROM dbo.TableIds;

    SET @ITER += 1;

    END

  • Evil Kraig F (8/1/2014)


    If you want to lock rows for the duration of a transaction from being read, or better keep anyone else from getting into the table at all until you're done, SELECT * FROM tbl (TABLOCKX) WHERE NextID = 'Yes'. That'll open an exclusive table lock on the table for the transaction. Noone else is getting in there at that point until you commit or rollback.

    Using tablockx in something like this can sometimes be a pain - it blocks all access to the table, including reads. Using a table hint to put the statement (not the transaction) into REPEATABLEREAD (or even SERIALIZABLE) mode as far as the locks taken on rows or pages in this table are concerned might be better, because it will only block access using locks that the engine decides of its own accord to take.

    Tom

  • TomThomson (8/2/2014)


    Evil Kraig F (8/1/2014)


    If you want to lock rows for the duration of a transaction from being read, or better keep anyone else from getting into the table at all until you're done, SELECT * FROM tbl (TABLOCKX) WHERE NextID = 'Yes'. That'll open an exclusive table lock on the table for the transaction. Noone else is getting in there at that point until you commit or rollback.

    Using tablockx in something like this can sometimes be a pain - it blocks all access to the table, including reads. Using a table hint to put the statement (not the transaction) into REPEATABLEREAD (or even SERIALIZABLE) mode as far as the locks taken on rows or pages in this table are concerned might be better, because it will only block access using locks that the engine decides of its own accord to take.

    Quick question, how about adding a covering index?

    😎

  • Eirikur Eiriksson (8/2/2014)


    Update code

    USE tempdb;

    GO

    SET NOCOUNT ON;

    DECLARE @ITER INT = 0 ;

    DECLARE @COUNT INT = 100000;

    DECLARE @NextId INT = 0 ;

    WHILE @ITER < @COUNT

    BEGIN

    UPDATE T WITH (ROWLOCK)

    SET T.NextId = T.NextId + 1

    FROM dbo.TableIds T

    WHERE TableName = 'Users';

    SELECT @NextId = NextId FROM dbo.TableIds;

    SET @ITER += 1;

    END

    Gosh... be careful. Without a transaction around the UPDATE and the SELECT, there is the possibility of someone getting in between which would cause the wrong ID to be returned. With such a transaction, you're asking for and average of hundreds of deadlocks per day with spikes in the thousands per day. I know this because the code above is almost exactly what a company that I previously worked for had. There were and average of 460 deadlocks per day with spikes to 4000. We fixed it by using the 3 part update like the one I posted.

    Of course, we also included an "increment" to reserve large numbers of IDs to keep batch runs from being RBAR in nature but that also requires some tricks in the external code using Temp Tables to be both successful and fast.

    The best thing to do is to NOT try to write your own NextID sequencer. Use IDENTITY or SEQUENCE They won't let you down except for gaps after a reboot or a rollback and you shouldn't expect otherwise there.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff Moden (8/2/2014)


    Eirikur Eiriksson (8/2/2014)


    Update code

    USE tempdb;

    GO

    SET NOCOUNT ON;

    DECLARE @ITER INT = 0 ;

    DECLARE @COUNT INT = 100000;

    DECLARE @NextId INT = 0 ;

    WHILE @ITER < @COUNT

    BEGIN

    UPDATE T WITH (ROWLOCK)

    SET T.NextId = T.NextId + 1

    FROM dbo.TableIds T

    WHERE TableName = 'Users';

    SELECT @NextId = NextId FROM dbo.TableIds;

    SET @ITER += 1;

    END

    Gosh... be careful. Without a transaction around the UPDATE and the SELECT, there is the possibility of someone getting in between which would cause the wrong ID to be returned. With such a transaction, you're asking for and average of hundreds of deadlocks per day with spikes in the thousands per day. I know this because the code above is almost exactly what a company that I previously worked for had. There were and average of 460 deadlocks per day with spikes to 4000. We fixed it by using the 3 part update like the one I posted.

    Of course, we also included an "increment" to reserve large numbers of IDs to keep batch runs from being RBAR in nature but that also requires some tricks in the external code using Temp Tables to be both successful and fast.

    The best thing to do is to NOT try to write your own NextID sequencer. Use IDENTITY or SEQUENCE They won't let you down except for gaps after a reboot or a rollback and you shouldn't expect otherwise there.

    I get your point Jeff, in fact this was me on the first espresso:-) in the morning. Not suggesting that this is in any way, shape or form a good solution, rather an effort in gathering feedback like yours regarding using ROWLOCK. I tried this out using the script I posted and only got minor blocking, nothing serious, but then again the testing method is seriously flawed as it doesn't test for identical simultaneous operations. I also noticed that the update locks held where all escalated to a PAGELOCK.

    In my opinion, which I should have stated earlier, I agree with NOT rolling your own sequencer, leave that to the SQL Server. Itzik Ben-Gan did a two part article on the subject, "Sequences, Part 1" and "Sequences, Part 2", well worth reading.

    😎

  • Jeff Moden (8/2/2014)


    The best thing to do is to NOT try to write your own NextID sequencer. Use IDENTITY or SEQUENCE They won't let you down except for gaps after a reboot or a rollback and you shouldn't expect otherwise there.

    If you have a business requirements that there must not be gaps, they are out of the question.

    The chief reason to use IDENTITY/SEQUENCE is that you need to support high concurrency. If that is on your concern, I see little point in using IDENTITY which seems to cause people more trouble than benefit. Sequence are less problematic.

    If you need a range of values, there is of course little reason to iterate, but you can have code like:

    BEGIN TRANSACTION

    UPDATE idbtl

    SET @firstid = nextid,

    nextid = nextid + @no_of_ids_needed

    -- Use @firstid to @firstid + @no_of_ids_needed - 1 here

    COMMIT TRANSACTION

    This is only in the case you have a separate id table. But you can also use the table itself:

    BEGIN TRANSACTION

    SELECT @nextid = coalesce(MAX(id), 0)

    FROM tbl WITH (UPDLOCK)

    INSERT tbl(id, ....)

    SELECT @nextid + row_number() OVER(...), ....

    FROM

    COMMIT TRANSACTION

    Both these approaces avoids gaps, but cause serilaisation and are bad for concurrency. If you have a requirement to support high concurrency and have contiguous numbers, you have a challenge!

    [font="Times New Roman"]Erland Sommarskog, SQL Server MVP, www.sommarskog.se[/font]

  • I agree that SEQUENCE has the advantage of being able to cover multiple tables and that IDENTITY requires SET IDENTITY INSERT if you want to update the identity column but, other than that, how is SEQUENCE any less problematic than IDENTITY?

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff Moden (8/2/2014)


    I agree that SEQUENCE has the advantage of being able to cover multiple tables and that IDENTITY requires SET IDENTITY INSERT if you want to update the identity column but, other than that, how is SEQUENCE any less problematic than IDENTITY?

    You cannot update an IDENTITY column at all. You can set explicit values on insert, but if you for some reason want to update the values, you have a headache.

    Also, if you find out later that you want gaps, and decide to roll your own, you can just drop the default that says NEXT VALUE FOR. Whereas with IDENTITY, you need to do the create-new-table/copy-over/drop-old/rename dance.

    With sequences, you don't run the risk to have this pain.

    [font="Times New Roman"]Erland Sommarskog, SQL Server MVP, www.sommarskog.se[/font]

Viewing 15 posts - 1 through 15 (of 19 total)

You must be logged in to reply to this topic. Login to reply