Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Data Management

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

Authors

Search

XML Feeds

Google Ads

« SSRS 05 Passing Multi-Value Parameters Between Reports.Best Practice: Do not cluster on UniqueIdentifier when you use NewId »
comments
Rate Post:
submit to reddit Digg!FacebookDotnetkicks

Best Practice: coding SQL Server triggers for multi-row operations

There are many forum posts where people code triggers but these triggers are coded incorrectly because they don't account for multi-row operations. A trigger fires per batch not per row, if you are lucky you will get an error...if you are not lucky you will not get an error but it might take a while before you notice that you are missing a whole bunch of data
Let's take a look, first create these two tables

  1. CREATE TABLE Test(id INT IDENTITY not null PRIMARY KEY,
  2.             SomeDate DATETIME not null)
  3. GO
  4.  
  5. CREATE TABLE TestHistory(id INT  not null,
  6.             InsertedDate DATETIME not null)
  7. GO

Now create this trigger, this trigger is very simple, it basically inserts a row into the history table every time an insert happens in the test table

  1. CREATE  TRIGGER trTest
  2.     ON Test
  3.     FOR INSERT
  4.     AS
  5.      
  6.     IF @@ROWCOUNT =0
  7.     RETURN
  8.      
  9.     DECLARE @id INT
  10.     SET @id = (SELECT id
  11.     FROM inserted)
  12.    
  13.     INSERT TestHistory (id,InsertedDate)
  14.     SELECT @id, GETDATE()
  15.    
  16.     GO

Run this insert statement which only inserts one row

  1. INSERT Test(SomeDate) VALUES(GETDATE())

Now run this to see what is in the history table

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103

That all works fine, what happens when we try to insert 2 rows?

  1. INSERT Test(SomeDate)
  2. SELECT GETDATE()
  3. UNION all
  4. SELECT GETDATE() + 1

Here is the error.

Server: Msg 512, Level 16, State 1, Procedure trTest, Line 11
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
The statement has been terminated.

What would happen if you coded the trigger in this way

  1. ALTER TRIGGER trTest
  2.     ON Test
  3.     FOR INSERT
  4.     AS
  5.      
  6.     IF @@ROWCOUNT =0
  7.     RETURN
  8.      
  9.     DECLARE @id INT
  10.     SELECT @id = id
  11.     FROM inserted
  12.    
  13.     INSERT TestHistory (id,InsertedDate)
  14.     SELECT @id, GETDATE()
  15.    
  16.     GO

Now insert one row

  1. INSERT Test(SomeDate) VALUES(GETDATE())

We look again what is in the history table, as you can see we have id 1 and 4, this is because id 2 and 3 failed and were rolled back

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103
4	2009-11-12 14:52:08.370

Here is where it gets interesting, run this code

  1. INSERT Test(SomeDate)
  2. SELECT GETDATE()
  3. UNION all
  4. SELECT GETDATE() + 1

That runs fine but when we look now we are missing row 5 in the history table

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103
4	2009-11-12 14:52:08.370
6	2009-11-12 14:52:20.167

let's try that again

  1. INSERT Test(SomeDate)
  2. SELECT GETDATE()
  3. UNION all
  4. SELECT GETDATE() + 1

Now we are missing row 7 in the history table

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103
4	2009-11-12 14:52:08.370
6	2009-11-12 14:52:20.167
8	2009-11-12 14:52:38.917

The problem is with this line of code

  1. SELECT @id = id FROM inserted

@id will only hold the value for the row that was returned last in the result set

Here is how you would change the trigger to work correctly

  1. ALTER TRIGGER trTest
  2.     ON Test
  3.     FOR INSERT
  4.     AS
  5.      
  6.     IF @@ROWCOUNT =0
  7.     RETURN
  8.      
  9.        
  10.     INSERT TestHistory (id,InsertedDate)
  11.     SELECT id, GETDATE()
  12.     FROM inserted
  13.    
  14. GO

Now run this

  1. INSERT Test(SomeDate) VALUES(GETDATE())

We can now verify that it works correctly

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103
4	2009-11-12 14:52:08.370
6	2009-11-12 14:52:20.167
8	2009-11-12 14:52:38.917
9	2009-11-12 14:53:44.433

Now run this for 2 rows

  1. INSERT Test(SomeDate)
  2. SELECT GETDATE()
  3. UNION all
  4. SELECT GETDATE() + 1

And as you can see both rows were inserted into the history table

  1. SELECT * FROM TestHistory
1	2009-11-12 14:51:21.103
4	2009-11-12 14:52:08.370
6	2009-11-12 14:52:20.167
8	2009-11-12 14:52:38.917
9	2009-11-12 14:53:44.433
10	2009-11-12 14:53:51.870
11	2009-11-12 14:53:51.870

So what is worse in this case? The error message or the fact that the code didn't blow up but that the insert wasn't working correctly? I'll take an error message any time over the other problem.

I am putting together a SQL Server Best Programming Practices wiki page, this blog post is part of it as are other posts and articles either from this site as well as from other sites. I am still working on the SQL Server Best Programming Practices wiki page but I encourage you to bookmark it and come back every now and then because I will be adding more content




*** If you have a SQL related question try our Microsoft SQL Server Programming forum or our Microsoft SQL Server Admin forum

About the Author

User bio imageDenis has been working with SQL Server since version 6.5. Although he worked as an ASP/JSP/ColdFusion developer before the dot com bust, he has been working exclusively as a database developer/architect since 2002. In addition to English, Denis is also fluent in Croatian and Dutch, but he can curse in many other languages and dialects (just ask the SQL optimizer) He lives in Princeton, NJ with his wife and three kids.
Social SitingsTwitterFacebookLinkedInHomePageLTD RSS Feed
1205 views
submit to reddit Digg!FacebookDotnetkicks

Comments and Feedback

4 comments

Comment from: vanman [Member] Email
I also found that placing

INSERT TestHistory (id,InsertedDate)
SELECT @id, GETDATE()

inside a cursor also works.

any comments on this method?
11/17/09 @ 01:35
Comment from: SQLDenis [Member] Email
vanman code in a trigger should execute as fast as possible..in general a set based operation will always be faster than a cursor that loops over a bunch of rows
11/17/09 @ 10:19
Comment from: vanman [Member] Email
thx 4 feedback. yes i agree, although i have to do row by row validations before allowing transaction to commit.
11/23/09 @ 06:32
Comment from: Branko [Visitor] Email · http://www.beotel.rs
I have problem with connect code JAVA JDK 6 on SQL Server 2000
12/14/09 @ 10:07

Leave a comment


Your email address will not be revealed on this site.

Your URL will be displayed.
(Line breaks become <br />)
(Name, email & website)
(Allow users to contact you through a message form (your email will not be revealed.)