MS.COM Operations Tools Team WebLog

Hey - What does this button do?

What's Wrong With This Code (T-SQL Edition)

This code (actually, code very much like it) made it through development and test (in a different team; we never make mistakes,) and worked beautifully.  In production, however, it was an utter failure.  It isn't difficult to spot the problem; can you?

----------------------------------------------


INSERT INTO BigFatTable (Name, Description, Time)
VALUES (@Name, @Description, @Time)
SELECT @Output = SCOPE_IDENTITY()
FROM BigFatTable

----------------------------------------------


//
//   Llywarch Moel
//      (aka Number 5)
//
Published Tuesday, January 11, 2005 5:59 PM by mscomts

Comments

 

Wayne Bloss said:

I think that

SELECT @Output = SCOPE_IDENTITY()
FROM BigFatTable

should just be

SELECT @Output = SCOPE_IDENTITY()
January 11, 2005 6:20 PM
 

Jon Galloway said:

Hmmm... Well, I agree with Wayne that SCOPE_IDENTITY() isn't table specific. You'd use SELECT IDENT_CURRENT(BigFatTable) for that.

But I don't see why this worked in Dev and Test.
January 11, 2005 6:36 PM
 

Todd said:

SCOPE_IDENTITY() is scoped by the module (stored procedure, trigger, function, batch) it's in. If the goal is to get the last identity value inserted into BigFatTable in this module, I see no reason why this shouldn't work. If the goal is to get the latest identity value inserted into BigFatTable (this module or not), then IDENT_CURRENT() would be what you want.

Given the code:

--
INSERT INTO BigFatTable(Name, Description, Time)
VALUES (@Name, @Description, @Time)

INSERT INTO AnotherTable(This, That, TheOther)
VALUES (@This, That, TheOther)

SELECT @BigFatIdentity = SCOPE_IDENTITY()
--

You'll the identity inserted into AnotherTable in @BigFatIdentity, not the identity in BigFatTable.

Now, assume that the last line uses IDENT_CURRENT(BigFatTable), but the following code runs in a different module between the two inserts:

--
INSERT INTO BigFatTable(Name, Description, Time)
VALUES (@AnotherName, @AnotherDescription, @AnotherTime)
--

In this case, you do get an identity value from BigFatTable into @BigFatIdentity, but it's not the one you wanted because the value from IDENT_CURRENT was bumped by the second batch. Now change the first batch to:

--
INSERT INTO BigFatTable(Name, Description, Time)
VALUES (@Name, @Description, @Time)

SELECT @BigFatIdentity = SCOPE_IDENTITY()

INSERT INTO AnotherTable(This, That, TheOther)
VALUES (@This, @That, @TheOther)
--

Assume that the second batch above (the second insert into BigFatTable) runs after the first insert and before the SCOPE_IDENTITY() select. Now @BigFatIdentity will have the proper value for the identity field inserted into BigFatTable in this batch. The out-of-scope insert doesn't affect SCOPE_IDENTITY(), and because you're getting SCOPE_IDENTITY() before inserting into another table it's not going to get overwritten by the second insert.

Now, my guess at the problem here is that the trailing "FROM BigFatTable" is the culprit. If you look at the execution plan with and without that part, you'll see that having the FROM clause results in a table scan over the entire BigFatTable (which one can assume from the naming convention will be very big, and thus costly to scan), while removing the FROM clause ends up with a very trivial select. So, it fell down in production not because @output was being set wrong, but because it was choking on the huge and unnecessary table scan while trying to set @output.
January 11, 2005 7:14 PM
 

Number 5 said:

Excellent analysis, Todd; I could not have said it any better.

The addition of that FROM clause essentially caused @Output to be assigned with the last IDENTITY value from the current scope *iteratively for each row in the table*. It was correct, but comically redundant.

Although I did say that this code was tested, it should be pretty obvious that it was never performance tested under the expected scale. In test, BigFatTable obviously never got large enough during testing for that table scan to hurt them.

If anyone reading this far cannot yet see the problem, try running this in pubs:

-----------------------------------------
DECLARE @host varchar(50)
DECLARE @complete varchar(8000)

SELECT @host = HOST_NAME() FROM authors

SELECT @complete = ' '
SELECT @complete = @complete + ';' + HOST_NAME() FROM authors

SELECT @host, @complete
-----------------------------------------

Now, try to explain the output to yourself.
January 12, 2005 9:46 AM
 

Genting Lane said:

The utter error is your assumption of your team's perfectness.
January 13, 2005 4:37 AM
 

Number 5 said:

Sarchasm: (n) The gap between the author of sarcastic wit and the person who doesn't get it.
January 13, 2005 9:28 AM
 

Adam Machanic said:

Todd hit it perfectly.

Table scans on big fat tables are not fun in production environments!
January 17, 2005 10:11 AM
 

Gil Adi said:

Use This

IDENT_CURRENT('table_name')

Returns the last identity value generated for a specified table in any session and any scope.


SELECT @Output = IDENT_CURRENT('BigFatTable')



OR



SCOPE_IDENTITY()

Returns the last IDENTITY value inserted into an IDENTITY column in the same scope. A scope is a module -- a stored procedure, trigger, function, or batch. Thus, two statements are in the same scope if they are in the same stored procedure, function, or batch.

SELECT @Output = SCOPE_IDENTITY()


I wold Choose
the SELECT @Output = IDENT_CURRENT('BigFatTable')
since when using OLDB to call the server a siralizable isolation level is initiated on the stor proc hance no concurrency issues
January 17, 2005 10:29 PM
 

Microsoft » Blog Archives » Eric Bowen's .NET Technology Blog : SQL Server T-SQL code samples … said:

September 13, 2006 12:17 AM
 

Microsoft » Blog Archives » Hans VB's WebLog : SQL 2005: .NET vs. T-SQL, the meaningless … said:

September 23, 2006 11:40 PM
 

Microsoft » Blog Archives » Microsoft SQL Server Survival Guide said:

October 3, 2006 8:04 AM
New Comments to this post are disabled

This Blog

Syndication

Tags

No tags have been created or used yet.

News

All opinions posted here are those of the author(s) and are in no way intended to represent the opinions of our employer. This is provided "AS IS" with no warranties, and confers no rights. Use of included code samples are subject to the terms specified in the Terms of Use.

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker