Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 SQL Server 2000 Forums
 SQL Server Development (2000)
 3 line trigger causing deadlocks

Author  Topic 

jfmenard
Starting Member

18 Posts

Posted - 2005-07-08 : 10:15:14
I use an UpdateDT datetime field on all tables I want to track the last modification date. This field is updated by a trigger in my Sql 2000 database:

CREATE TRIGGER Trig_MyTable_Update ON dbo.MyTable FOR INSERT, UPDATE
AS
IF @@ROWCOUNT=0 RETURN
UPDATE U SET UpdateDT = GETDATE()
FROM MyTable U INNER JOIN INSERTED I ON U.Id = I.Id

This looked to me as a simple way of accomplishing what I needed. However this trigger is source of deadlocks. Yes, this is the only trigger on that table and it causes deadlocks!

Just to be sure, I made a simple .NET application that start 2 threads that each repetedly update the same row in MyTable using this statement:

UPDATE MyTable SET Field1 = 123 WHERE Id=1

It takes a few seconds and a deadlock appears.

My understanding is:
- Thread-1: Call Update on a row.
- Thread-2: Call Update on same row, but waits that Thread-1 finishes.
- Thread-1: Trigger gets called to update same row, waits that Thread-2 finishes.
- Sql Server detects deadlock and terminate one of the queries (let say Thread-2).
- Thread-2: Aborts and raise an error.
- Thread-1: Completes trigger Update statement.
- Thread-1: Commits main Update and terminates.

I successfully solved this issue by using a pair of INSTEAD OF triggers like this (only UPDATE shown below):

CREATE TRIGGER Trig_MyTable_Update ON dbo.MyTable INSTEAD OF UPDATE
AS
IF @@ROWCOUNT=0 RETURN
UPDATE U SET
U.Field1 = I.Field1,
U.Field2 = I.Field2,
U.Field3 = I.Field3,
U.Field4 = I.Field4,
U.Field5 = I.Field5,
U.Field6 = I.Field6,
U.Field7 = I.Field7,
U.Field8 = I.Field8,
U.Field9 = I.Field9,
U.UpdateDT = GETDATE()
FROM MyTable U INNER JOIN INSERTED I ON U.Id = I.Id

But this becomes just too complicated for a simple timestamping thing:
- Other (AFTER) triggers will think that all columns are updated when querying the UPDATED(field) function.
- If we change the table structure, we must not forget to update the INSTEAD OF trigger which complicates maintenance.

Is there something I missed?

Thanks,

J-F

Kristen
Test

22859 Posts

Posted - 2005-07-08 : 13:15:03
UPDATE U SET UpdateDT = GETDATE()
FROM MyTable U INNER JOIN INSERTED I ON U.Id = I.Id

We do this all the time and I've never had a deadlock with it - I doubt we've been lucky, but in our case simultaneous updating of the SAME record from 2 workstations is very unlikely ... except that we probably have some large UPDATE statements and thus the trigger will be processing lots of records, and then one or more of those is quite likely to be the subject of a simultaneous update / trigger.

I appreciate that's no help, but it might suggest that it is something special about your circumstances, rather than the code per-se.

Kristen
Go to Top of Page

KLang23
Posting Yak Master

115 Posts

Posted - 2005-07-08 : 14:18:07
Hi,

See if you can capure the deadlock with Profiler, and find which objects are involved (ie: index or data). You may be surprised.

Good luck with this.
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-08 : 16:00:22
quote:
Originally posted by Kristen

UPDATE U SET UpdateDT = GETDATE()
FROM MyTable U INNER JOIN INSERTED I ON U.Id = I.Id

We do this all the time and I've never had a deadlock with it - I doubt we've been lucky...


Hi Kristen, I followed KLang23's suggestion and it seems to confirm you where lucky. That's surprising me also because I was doing this all the time. But it's the first time I have a multi-threading system that sollicitate my database extensively.

I had a look at the profiler when reproducing the deadlock. Here is what I see:
- SPID=57: Reset connection.
- SPID=58: Reset connection.
- SPID=57: Starting UPDATE MyTable SET Field1 = 123 WHERE Id=1.
- SPID=58: Starting UPDATE MyTable SET Field1 = 123 WHERE Id=1.
- SPID=57: Exclusive Lock Acquired on MyTable (*A*).
- SPID=58: Exclusive Lock Acquire Timeout on MyTable (*B*).
- SPID=57: Starting UPDATE U SET UpdateDT = GETDATE() FROM MyTable U INNER JOIN INSERTED I ON U.Id = I.Id (trigger).
- SPID=57: Exclusive Lock Acquire Timeout (because of *B*).
- SPID=58: Exclusive Lock Acquire Timeout (because of *A*).
- Deadlock Chain between SPID=57 and SPID=58

Any comments anyone?
Go to Top of Page

elwoos
Master Smack Fu Yak Hacker

2052 Posts

Posted - 2005-07-11 : 03:16:58
I have some very similar triggers that haven't been causing deadlocks, these are on fairly low usage tables though, essentially I would say the situation I have in this regard is very similar to that of Kristen.

steve

Alright Brain, you don't like me, and I don't like you. But lets just do this, and I can get back to killing you with beer.
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-07-11 : 11:12:27
I think something else must be going on here. I also set up a small .net test where 3 different threads updated the same record simultaneously 20 times each (60 total updates in less than 200 milliseconds). no deadlocking.

Do you create an explicit transaction from the application? Or perhaps your ado.net objects are colliding somehow? I don't know.

I think the easiest solution is to use SPs to perform your updates. You could let the SP code handle what your trigger is doing to avoid this problem.

Below is my sql code. I'll post the .net test code if you want:

drop table junk
go

create table junk (rowid int, threadID int, TotalCallCount int, updatedDate datetime)
go

insert junk values (1, 0, 0, getdate())
insert junk values (2, 0, 0, getdate())
go

create trigger trJunk on junk for update
as
begin
update a set
a.updatedDate = getdate()
,a.TotalCallCount = isNull(a.TotalCallCount,0) + 1
from junk a
join inserted b
on a.rowid = b.rowid
end
go

--after running the test:
select * from junk

rowid threadID TotalCallCount updatedDate
----------- ----------- -------------- ------------------------
1 0 0 2005-07-11 11:04:28.370
2 3 60 2005-07-11 11:04:35.480


Be One with the Optimizer
TG
Go to Top of Page

Kristen
Test

22859 Posts

Posted - 2005-07-11 : 12:48:58
Yup, I concur. I'm struggling to see how:

UPDATE U SET SomeColumn = 'foo' FROM MyTable U WHERE MyPK = 'BAR'

which in turn fires a trigger which does

UPDATE U SET MyUpdateDate = GetDate() FROM MyTable U JOIN inserted i ON U.MyPK = i.MyPK

can trigger a deadlock.

Kristen
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-11 : 16:16:15
Ok I got news for everyone.

Here is the test application I did to reproduce the deadlock constantly (C# .Net Win Form application with sources):
[url]http://www.streamload.com/wizik/temp/DbDeadlockTester.zip[/url]

Feel free to test this on your database to test your UPDATE triggers (you supply the command to execute and the connection string).

I discovered that the problem arise when the clustered index is not the same as the primary key. i.e. I have a large table with an ID (identity) primary key field (where clustered index has been unchecked) and I created a separate clustered index on a datetime field for faster queries. So when the initial UPDATE and the trigger UPDATE where clauses are both NOT on the clustered index, then the deadlock is almost immediate with DbDeadlockTester.

That is my conclusion for the moment. Any comments?

Thanks,

J-F
Go to Top of Page

Kristen
Test

22859 Posts

Posted - 2005-07-11 : 18:42:09
What service pack are you using on SQL 2000 - SP4?

Kristen
Go to Top of Page

eyechart
Master Smack Fu Yak Hacker

3575 Posts

Posted - 2005-07-11 : 18:55:10
two suggestions:

1. try using a rowlock hint
2. Assign the value of getdata() to a variable before your update. Make sure to include a SET NOCOUNT statement if you do this.

Are you running SQL2K SP4 by chance?



-ec
Go to Top of Page

Kristen
Test

22859 Posts

Posted - 2005-07-12 : 05:30:09
"Are you running SQL2K SP4 by chance"

hehehehe .... GreatMindItis

Kristen
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-12 : 09:27:16
No I am using SQL 2000 SP3.

Do I understand no one was able to reproduce my experiment?
Go to Top of Page

Kristen
Test

22859 Posts

Posted - 2005-07-12 : 09:51:49
Sorry, don't know anything about your "C# .Net Win Form" combination to be able to run the test

Kristen
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-12 : 10:14:49
quote:
Originally posted by eyechart

two suggestions:
1. try using a rowlock hint
2. Assign the value of getdata() to a variable before your update. Make sure to include a SET NOCOUNT statement if you do this.



I am astonished!

Using the ROWLOCK hint in the trigger did nothing.
Using a variable for the GETDATE() in the trigger did nothing.
BUT, using SET NOCOUNT ON in the trigger SOLVES THE PROBLEM!

Now my trigger does not cause deadlocks anymore:
CREATE  TRIGGER Trig_Dead_Update
ON dbo.DeadTable
FOR INSERT, UPDATE
AS
SET NOCOUNT ON -- Solves deadlock issues
IF @@ROWCOUNT=0 RETURN
UPDATE U SET
Updated = GETDATE()
FROM DeadTable U INNER JOIN INSERTED I ON U.AutoId = I.AutoId

Can someone explain me why?
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-12 : 10:19:43
quote:
Originally posted by Kristen

Sorry, don't know anything about your "C# .Net Win Form" combination to be able to run the test

Kristen



It is very easy. You don't need any programming knowledge. Just unzip the file and launch the \bin\Debug\DbDeadLock.exe. You'll be prompted with a connection string and a command to execute. Press start and it will start the number of threads specified with your command. I included the source just to let know there is nothing malicious in my code.

J-F
Go to Top of Page

Kristen
Test

22859 Posts

Posted - 2005-07-12 : 10:23:50
"Can someone explain me why?"

Without NOCOUNT SQL would be sending an additional recordset to your application. Possibly your application was not processing it (you would specifically have to iterate all resultsets in order to process it). So maybe that was creating a further timedelay, and that was causing the timeout.

Best to have the NOCOUNT stuff at the top of ALL your Sprocs and Triggers.

Kristen
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-12 : 10:43:54
quote:
Originally posted by Kristen
Without NOCOUNT SQL would be sending an additional recordset to your application. Possibly your application was not processing it (you would specifically have to iterate all resultsets in order to process it). So maybe that was creating a further timedelay, and that was causing the timeout.

Best to have the NOCOUNT stuff at the top of ALL your Sprocs and Triggers.



My understanding is NOCOUNT OFF does not produce an additional recordset. It only keep track of modified rows. The only way of returning a recordset to an application is thru a SELECT command. And it is certainly not a question of timing, because my sample application generates more than 1000 commands per second in 3 threads so when letting it run for a minute I assume all timing issues where covered.
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-07-12 : 11:15:34
hmmm, I got slightly different results. After running your test code as well as mine, I found that what seems to consistently be the difference is having a nonclustered primary key on the table. If the PK is clustered, no deadlocking. If no PK, no deadlocking. Set nocount made no difference for me. Having a clustered index on a date column didn't matter either for me. I can't explain it. I'll have to do a little more digging.

EDIT:
I still would suggest that you replace your in-line statements with insert/update SPs. Let the SPs handle the UpdatedDate column and get rid of the trigger.

Be One with the Optimizer
TG
Go to Top of Page

jfmenard
Starting Member

18 Posts

Posted - 2005-07-12 : 11:40:22
quote:
Originally posted by TG

hmmm, I got slightly different results. After running your test code as well as mine, I found that what seems to consistently be the difference is having a nonclustered primary key on the table. If the PK is clustered, no deadlocking. If no PK, no deadlocking. Set nocount made no difference for me. Having a clustered index on a date column didn't matter either for me. I can't explain it. I'll have to do a little more digging.



Maybe I forgot to re-quote all the steps. The deadlock condition only appear on a table where:
- the PK is non clustered
- a clustered index is on another field (not the PK, not the updated field)
- there is an after update trigger that update a field on the same table and the WHERE is on the PK field (non clustered)
- there is no SET NOCOUNT ON statement

THEN:
- If I change the PK to clustered => no deadlock
or
- If I use an INSTEAD OF trigger => no deadlock
or
- If I change the trigger WHERE to use the clustered index => no deadlock
or
- If I add SET NOCOUNT ON in the trigger => no deadlock

I think I understand the first 3 solutions, but I don't understand the last one. How come adding SET NOCOUNT ON prevents the deadlock? That's my preferred solution for now because it has the less impact on our database design but I want to make sure it solve deadlocks definitively.
Go to Top of Page

rockmoose
SQL Natt Alfen

3279 Posts

Posted - 2005-07-12 : 12:13:12
This is from BOL (SET NOCOUNT ON : Processing Results)
quote:
Each INSERT, UPDATE, and DELETE statement returns a result set containing only the number of rows affected by the modification. This count is made available when application calls SQLRowCount. ODBC 3.x applications must either call SQLRowCount to retrieve the result set or SQLMoreResults to cancel it. When an application executes a batch or stored procedure containing multiple INSERT, UPDATE, or DELETE statements, the result set from each modification statement must be processed using SQLRowCount or cancelled using SQLMoreResults. These counts can be cancelled by including a SET NOCOUNT ON statement in the batch or stored procedure


I am curious about this statement in Your trigger:
IF @@ROWCOUNT=0 RETURN
Couldn't that reset the @@ROWCOUNT for another thread in the trigger ?
quote:
This variable is set to 0 by any statement that does not return rows, such as an IF statement


Is this something that people usually do in a trigger ?
Because I never do it... !
So I was wondering, if it has any efect on things.

rockmoose
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-07-12 : 12:50:43
>>IF @@ROWCOUNT=0 RETURN
I don't do that in a trigger either. As long as its the first statement in the trigger, I don't think it would hurt anything. I just don't think its necessary.

Like I said earlier, the only conditions I needed to cause the dealock were:
1. nonclustered PK
2. (regular) trigger for UPDATE or INSERT,UPDATE with an update to the trigger's table.

if those conditions are met then the "set nocount on" does NOT solve the problem.


Be One with the Optimizer
TG
Go to Top of Page
    Next Page

- Advertisement -