Skip to content
Advertisement

SQL deadlock on concurrent delete

I have an API endpoint to delete children of a parent, unless a child is specified. The SQL is:

DELETE FROM
   Child
WHERE
   ParentID = @ParentID AND
   ChildID NOT IN (@ChildIDToKeep);

(This query is sanitised, so ignore the fact that I’m not doing ChildID != @ChildIDToKeep)

The executed SQL is also wrapped in a transaction.

My web application attempts to send hundreds of requests to this endpoint concurrently, so this SQL transaction gets executed concurrently many times. For some reason this is causing a deadlock.

Why am I getting a deadlock, and what can I do to resolve it?

Here is the deadlock XML:

<deadlock>
 <victim-list>
  <victimProcess id="process25c3045b848" />
 </victim-list>
 <process-list>
  <process id="process25c3045b848" taskpriority="0" logused="6336" waitresource="OBJECT: 14:788354023:0 " waittime="3039" ownerId="39730548" transactionname="user_transaction" lasttranstarted="2021-08-13T13:28:05.690" XDES="0x25c36f9f900" lockMode="IX" schedulerid="1" kpid="6588" status="suspended" spid="70" sbid="4" ecid="0" priority="0" trancount="2" lastbatchstarted="2021-08-13T13:28:05.957" lastbatchcompleted="2021-08-13T13:28:05.957" lastattention="1900-01-01T00:00:00.957" clientapp="Core .Net SqlClient Data Provider" hostname="DER-UK-LAP-101" hostpid="14344" loginname="USERNAME" isolationlevel="serializable (4)" xactid="39730548" currentdb="14" currentdbname="Database" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
   <executionStack>
    <frame procname="adhoc" line="2" stmtstart="102" stmtend="500" sqlhandle="0x0200000053d2042c7d4e4199da6b8531c53adb3464bc8b610000000000000000000000000000000000000000">
unknown    </frame>
    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
(@ParentID int,@ChildIDToKeep int)
                    DELETE FROM
                        Child
                    WHERE
                        ParentID = @ParentID AND
                        ChildID NOT IN (@ChildIDToKeep);   </inputbuf>
  </process>
  <process id="process25c2a462ca8" taskpriority="0" logused="6348" waitresource="OBJECT: 14:788354023:0 " waittime="2901" ownerId="39730549" transactionname="user_transaction" lasttranstarted="2021-08-13T13:28:05.690" XDES="0x25c39560e80" lockMode="IX" schedulerid="2" kpid="2252" status="suspended" spid="63" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2021-08-13T13:28:06.097" lastbatchcompleted="2021-08-13T13:28:06.097" lastattention="1900-01-01T00:00:00.097" clientapp="Core .Net SqlClient Data Provider" hostname="DER-UK-LAP-101" hostpid="14344" loginname="USERNAME" isolationlevel="serializable (4)" xactid="39730549" currentdb="14" currentdbname="Database" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
   <executionStack>
    <frame procname="adhoc" line="2" stmtstart="102" stmtend="500" sqlhandle="0x0200000053d2042c7d4e4199da6b8531c53adb3464bc8b610000000000000000000000000000000000000000">
unknown    </frame>
    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
(@ParentID int,@ChildIDToKeep int)
                    DELETE FROM
                        Child
                    WHERE
                        ParentID = @ParentID AND
                        ChildID NOT IN (@ChildIDToKeep);   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <objectlock lockPartition="0" objid="788354023" subresource="FULL" dbid="14" objectname="Database.dbo.Child" id="lock255443f2580" mode="S" associatedObjectId="788354023">
   <owner-list>
    <owner id="process25c2a462ca8" mode="S" />
    <owner id="process25c2a462ca8" mode="IX" requestType="convert" />
   </owner-list>
   <waiter-list>
    <waiter id="process25c3045b848" mode="IX" requestType="convert" />
   </waiter-list>
  </objectlock>
  <objectlock lockPartition="0" objid="788354023" subresource="FULL" dbid="14" objectname="Database.dbo.Child" id="lock255443f2580" mode="S" associatedObjectId="788354023">
   <owner-list>
    <owner id="process25c3045b848" mode="S" />
    <owner id="process25c3045b848" mode="IX" requestType="convert" />
   </owner-list>
   <waiter-list>
    <waiter id="process25c2a462ca8" mode="IX" requestType="convert" />
   </waiter-list>
  </objectlock>
 </resource-list>
</deadlock>

Here is the deadlock graph:

Deadlock graph

Advertisement

Answer

You’ve asked for the deadlock by running the transaction in SERIALIZABLE isolation level:

isolationlevel="serializable (4)" 

SERIALIZABLE transactions hold shared (S) locks until the end of the transaction, so when you “Select Parent’s Children.” you acquire and hold a S lock. And it’s a table lock since you don’t have proper indexing on the table.

SERIALIZABLE enforces its guarantees with deadlocks, see rant here: using new TransactionScope() considered harmful.

So when two sessions take and hold an S lock on the table, neither can acquire the IX lock required to start the DELETE, nor would they be able to acquire the X lock required to complete the DELETE. SERIALIZABLE is just doing its job here, as each session has read data that the other is trying to update, so if allowed to complete, the state of the database would not be the same as if the transactions ran sequentially, which is the definition of SERIALIZABLE. So it’s expected one of the transactions must be terminated, and that you would catch and retry if you still needed that work done.

So I would drop to READ_COMMITTED, and put an index on ParentId. You might want to put an UPDLOCK hint on your query to “Select Parent” so two sessions can’t run against the same parent concurrently.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement