I have a transaction that inserts a row into 2 tables that is getting hit from multiple threads concurrently and causing occasional deadlocks. We have reproduced in both SQL Server 2014 and 2016.
TableA has an IDENTITY (auto-incrementing int) column as its PK.
TableB has a composite PK made up of TableA.ID and another table's ID. (Effectively it models a many-to-many relationship.)
The transaction (implemented in C#/ADO.NET) looks approximately like this:
BEGIN TRAN
INSERT INTO TableA (...) OUTPUT INSERTED.ID VALUES (...)
INSERT INTO TableB (TableA_ID, ...) VALUES (...)
COMMIT TRAN
Here is a sample XML deadlock report we captured:
<deadlock>
<victim-list>
<victimProcess id="process22e2908c8" />
</victim-list>
<process-list>
<process id="process22e2908c8" taskpriority="0" logused="1448" waitresource="KEY: 5:72057594781630464 (274b30e6b09d)" waittime="14" ownerId="31777821" transactionname="user_transaction" lasttranstarted="2016-09-29T11:55:16.830" XDES="0x238c71270" lockMode="S" schedulerid="6" kpid="6156" status="suspended" spid="58" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2016-09-29T11:55:16.833" lastbatchcompleted="2016-09-29T11:55:16.830" lastattention="1900-01-01T00:00:00.830" clientapp=".Net SqlClient Data Provider" hostname="..." hostpid="2376" loginname="..." isolationlevel="read committed (2)" xactid="31777821" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="148" stmtend="408" sqlhandle="0x0200000076f9e4365c825254c9131048395bcaa4079e0fac0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
INSERT INTO TableB (...) VALUES (...)
</inputbuf>
</process>
<process id="process2232c2ca8" taskpriority="0" logused="1448" waitresource="KEY: 5:72057594781630464 (afb9ab6e6f5f)" waittime="15" ownerId="31777823" transactionname="user_transaction" lasttranstarted="2016-09-29T11:55:16.830" XDES="0x238c70890" lockMode="S" schedulerid="6" kpid="11480" status="suspended" spid="57" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2016-09-29T11:55:16.833" lastbatchcompleted="2016-09-29T11:55:16.830" lastattention="1900-01-01T00:00:00.830" clientapp=".Net SqlClient Data Provider" hostname="..." hostpid="2376" loginname="..." isolationlevel="read committed (2)" xactid="31777823" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="148" stmtend="408" sqlhandle="0x0200000076f9e4365c825254c9131048395bcaa4079e0fac0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
INSERT INTO TableB (...) VALUES (...)
</inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594781630464" dbid="5" objectname="MyDatabase.dbo.TableA" indexname="IndexOnTableA" id="lock2267be800" mode="X" associatedObjectId="72057594781630464">
<owner-list>
<owner id="process2232c2ca8" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process22e2908c8" mode="S" requestType="wait" />
</waiter-list>
</keylock>
<keylock hobtid="72057594781630464" dbid="5" objectname="MyDatabase.dbo.TableA" indexname="IndexOnTableA" id="lock2267f7080" mode="X" associatedObjectId="72057594781630464">
<owner-list>
<owner id="process22e2908c8" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process2232c2ca8" mode="S" requestType="wait" />
</waiter-list>
</keylock>
</resource-list>
</deadlock>
Things tried:
- READ COMMITTED and READ UNCOMMITTED isolation levels
- WITH (ROWLOCK) hint on the INSERT
- Snapshot Isolation and Read Committed Snapshot enabled
- Lots of index tweaks (open to suggestions though - is it likely we have too many or too few?)
I don't want to give up the transactional integrity of the 2 inserts, but it must be able to be hit from multiple threads without deadlocking. Any help is greatly appreciated.
UPDATE: Found the culprit, see my answer below. Thanks all for the help.