Sql-server – Merge statement deadlocking itself

deadlockmergesql serversql-server-2008-r2

I have the following procedure (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey form the composite key for the target table. CompanyId is a foreign key to a parent table. Also, there is a non-clustered index on CompanyId asc, UserId asc.

It is called from many different threads, and I am consistently getting deadlocks between different processes calling this same statement. My understanding was that the "with (holdlock)" was necessary to prevent insert/update race condition errors.

I assume that two different threads are locking rows (or pages) in different orders when they are validating the constraints, and thus are deadlocking.

Is this a correct assumption?

What is the best way to resolve this situation (i.e. no deadlocks, minimum impact on multi-threaded performance)?

Query Plan Image
(If you view the image in a new tab, it is readable. Sorry for the small size.)

  • There are at most 28 rows are in the @datatable.
  • I have traced back through the code, and I cannot see anywhere that we start a transaction here.
  • The foreign key is set up to cascade only on delete, and there were no deletions from the parent table.

Best Answer

OK, after looking everything over a couple of times, I think that your basic assumption was correct. What's probably going on here is that:

  1. The MATCH part of the MERGE checks the index for matches, read-locking those rows/pages as it goes.

  2. When it has a row without a match, it will try to insert the new Index Row first so it will request a row/page write-lock ...

But if another user has also gotten to step 1 on the same row/page, then the first user will be blocked from the Update, and ...

If the second user also needs to insert on the same page, then they're in a deadlock.

AFAIK, there's only one (simple) way to be 100% sure that you cannot get a deadlock with this procedure and that would be to add a TABLOCKX hint to the MERGE, but that would probably have a really bad impact on performance.

It is possible that adding a TABLOCK hint instead would be enough to solve the problem without having to big an effect on your performance.

Finally, you could also try adding PAGLOCK, XLOCK or both PAGLOCK and XLOCK. Again that might work and performance might not be too awful. You'll have to try it to see.