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

    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

    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 SitingsTwitterFacebookLinkedInHomePageFlickrLTD RSS Feed
    11461 views
    InstapaperVote on HN

    9 comments

    Comment from: vanman [Member] Email
    vanman 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
    SQLDenis 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
    vanman 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: Pete [Visitor]
    Pete can you example can call a store procedure to do the insert? if so can you show how?


    thanks
    04/28/10 @ 22:22
    Comment from: Alex Feng [Visitor] Email
    Alex Feng Hi,

    I found something very strange - the sequence of the records in the TestHistory is DESC, and in your test it seems to be ASC. I am using SQL Server 2008 RTM Developer.

    This is my test below:

    -- Alter the trigger as this
    alter trigger trTest
    on Test
    for insert
    as

    if @@ROWCOUNT = 0
    return

    insert TestHistory (id, InsertedDate)
    select id, GETDATE()
    from inserted

    select * from inserted -- this is the statement i added
    go

    insert Test (SomeDate)
    select GETDATE()
    union all
    select GETDATE() + 1
    go

    run the above 2 statements and i get:
    14 2010-06-08 23:08:28.347
    13 2010-06-07 23:08:28.347

    which i think should be in your scenario:
    13 2010-06-07 23:08:28.347
    14 2010-06-08 23:08:28.347

    Does this behavior correct? or something changed in the SQL Server 2008? I will test this on other version of SQL Server (2005/2008 R2) to see if it is different.

    Regards,
    Alex Feng
    06/07/10 @ 18:29
    Comment from: SQLDenis [Member] Email
    SQLDenis Alex,

    I get on SQL Server 2008 sp1

    2 2010-06-08 21:04:57.480
    1 2010-06-07 21:04:57.480

    on SQL Server 2008 R2 I get

    2 2010-06-08 21:07:27.180
    1 2010-06-07 21:07:27.180

    So in both case the last row is returned first, I would not worry about this, if you want a specific order then just do an ORDER BY
    06/07/10 @ 19:03
    Comment from: Alex Feng [Visitor]
    Alex Feng Thanks for your quick response.

    Now, i know that it is a normal behaivor in SQL Server 2008.

    Thanks again!
    06/08/10 @ 03:30
    Comment from: Suhas [Member] Email
    Suhas Sending an email from within a trigger is recomended ? or is it the best way to invoke a job from trigger to send a mail ?
    12/27/10 @ 21:35
    Comment from: Naomi Nosonovsky [Member]
    Naomi Nosonovsky No, sending an e-mail from the trigger is not recommended. The recommended practice is to update a separate notifications table and let a SQL Job fire with needed frequency.
    03/22/11 @ 17:55

    Leave a comment


    Your email address will not be revealed on this site.

    To mislead the spambots.

    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.)