Perrier
Perrier

Reputation: 2827

SQL Server Deadlock by IX pagelock

I have a deadlock and I don't see how coult it be solved. I have already fixed some other queries coming from EF side that caused the same deadlock (at same row in the sp) but this one cannot be modified, it's a very basic query and I think there must be an easier way rewriting the SP instead or modifying indexes to avoid page locks.

Three tables:

Two processes:

The deadlock:

It is thrown at the very last step when the SP tries to remove empty Charges, where ChargeItems has no records. To this point it has already deleted all ChargeItems and only empty charges and workitems has to be deleted.

Deadlock graph

The query run by EF is searching for a Workitem by its DMC while the SP tries to remove Charges.

SELECT 
    [Limit1].[Id] AS [Id], 
    [Limit1].[DMC] AS [DMC], 
    [Limit1].[FirstSeen] AS [FirstSeen], 
    [Limit1].[DrawingNo] AS [DrawingNo], 
    [Limit1].[MachineId] AS [MachineId], 
    [Limit1].[WorkItemState_Id] AS [WorkItemState_Id], 
    [Limit1].[ItemType_Id] AS [ItemType_Id], 
    [Limit1].[Repaired] AS [Repaired], 
    [Limit1].[MachineCycle] AS [MachineCycle], 
    [Limit1].[FirstSeenCheck] AS [FirstSeenCheck], 
    [Limit1].[LastSeen] AS [LastSeen], 
    [Limit1].[Archive] AS [Archive], 
    [Limit1].[CastingDateString] AS [CastingDateString], 
    [Limit1].[Deleted] AS [Deleted], 
    [Limit1].[DMC2] AS [DMC2], 
    [Limit1].[Id1] AS [Id1], 
    [Limit1].[WorkPlace_Id] AS [WorkPlace_Id], 
    [Limit1].[CastingFormIdent_Id] AS [CastingFormIdent_Id], 
    [Limit1].[FormIdentItemType_Id] AS [FormIdentItemType_Id]
    FROM ( SELECT TOP (1) 
        [Extent1].[Id] AS [Id], 
        [Extent1].[DMC] AS [DMC], 
        [Extent1].[FirstSeen] AS [FirstSeen], 
        [Extent1].[DrawingNo] AS [DrawingNo], 
        [Extent1].[MachineId] AS [MachineId], 
        [Extent1].[WorkItemState_Id] AS [WorkItemState_Id], 
        [Extent1].[ItemType_Id] AS [ItemType_Id], 
        [Extent1].[Repaired] AS [Repaired], 
        [Extent1].[MachineCycle] AS [MachineCycle], 
        [Extent1].[FirstSeenCheck] AS [FirstSeenCheck], 
        [Extent1].[LastSeen] AS [LastSeen], 
        [Extent1].[Archive] AS [Archive], 
        [Extent1].[CastingDateString] AS [CastingDateString], 
        [Extent1].[Deleted] AS [Deleted], 
        [Extent1].[DMC2] AS [DMC2], 
        [Extent1].[WorkPlace_Id] AS [WorkPlace_Id], 
        [Extent1].[CastingFormIdent_Id] AS [CastingFormIdent_Id], 
        [Extent1].[FormIdentItemType_Id] AS [FormIdentItemType_Id], 
        [Extent2].[Id] AS [Id1]
        FROM WorkItems AS [Extent1]
        LEFT OUTER JOIN [dbo].[ChargeItems] AS [Extent2] ON [Extent1].[Id] = [Extent2].[WorkItem_Id]
        WHERE ([Extent1].[DMC] = '') OR (([Extent1].[DMC] IS NULL))
    )  AS [Limit1]

Execution plan of the select:

Execution plan of the select statement from a different run. (Not caused deadlock.)

Part of the SP:

;with chargesToDelete(id, ciid) as (
                    select c.id, ci.Id from @chargeids c
                    left join dbo.chargeitems ci on ci.Charge_Id = c.id
                    where ci.id is null
                )

                delete from dbo.charges
                    where Id in (select id from chargesToDelete)

Deadlock graph xml:

    <deadlock>
 <victim-list>
  <victimProcess id="process6472ad498" />
 </victim-list>
 <process-list>
  <process id="process6472ad498" taskpriority="5" logused="152924" waitresource="PAGE: 5:1:531207 " waittime="794" ownerId="10001638" transactionname="DELETE" lasttranstarted="2018-08-29T11:50:14.510" XDES="0x6ff07f078" lockMode="IX" schedulerid="7" kpid="7620" status="suspended" spid="89" sbid="0" ecid="0" priority="-5" trancount="2" lastbatchstarted="2018-08-29T11:22:53.457" lastbatchcompleted="2018-08-29T11:22:53.457" lastattention="1900-01-01T00:00:00.457" clientapp="Microsoft SQL Server Management Studio - Query" hostname="PCSERVER151" hostpid="6480" loginname="PRC\administrator" isolationlevel="read uncommitted (1)" xactid="10001638" currentdb="5" lockTimeout="4294967295" clientoption1="673187936" clientoption2="390200">
   <executionStack>
    <frame procname="LP_R.dbo.Archive_Finish" line="190" stmtstart="12876" stmtend="13044" sqlhandle="0x03000500063ecd76e962c80014a9000001000000000000000000000000000000000000000000000000000000">
delete from LP_R.dbo.workitems where id in (select id from @workitemIds);    </frame>
    <frame procname="LP_R.dbo.Archive" line="64" stmtstart="5142" stmtend="5244" sqlhandle="0x030005007886b57874f2b30014a9000001000000000000000000000000000000000000000000000000000000">
exec Archive_Finish @Day, @Dryrun, @MaxWorkitems;    </frame>
    <frame procname="adhoc" line="4" stmtstart="62" stmtend="200" sqlhandle="0x0100050010f3f82c309a63770600000000000000000000000000000000000000000000000000000000000000">
EXEC    @return_value = [dbo].[Archive]
        @Day = 450,
        @Dryrun = 0    </frame>
   </executionStack>
   <inputbuf>

DECLARE @return_value int

EXEC    @return_value = [dbo].[Archive]
        @Day = 450,
        @Dryrun = 0

SELECT  'Return Value' = @return_value

   </inputbuf>
  </process>
  <process id="process66f184558" taskpriority="0" logused="0" waitresource="PAGE: 5:1:114492 " waittime="913" ownerId="10002051" transactionname="SELECT" lasttranstarted="2018-08-29T11:50:15.210" XDES="0x6b379ad00" lockMode="S" schedulerid="5" kpid="3860" status="suspended" spid="67" sbid="2" ecid="0" priority="0" trancount="0" lastbatchstarted="2018-08-29T11:50:15.210" lastbatchcompleted="2018-08-29T11:50:15.210" lastattention="1900-01-01T00:00:00.210" clientapp="EntityFramework" hostname="PCSERVER151" hostpid="3520" loginname="sa" isolationlevel="read committed (2)" xactid="10002051" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
   <executionStack>
    <frame procname="adhoc" line="1" stmtstart="56" sqlhandle="0x02000000412fd7099fe0d3410b538a2193192ac8c5143cf20000000000000000000000000000000000000000">
SELECT 
    [Limit1].[Id] AS [Id], 
    [Limit1].[DMC] AS [DMC], 
    [Limit1].[FirstSeen] AS [FirstSeen], 
    [Limit1].[DrawingNo] AS [DrawingNo], 
    [Limit1].[MachineId] AS [MachineId], 
    [Limit1].[WorkItemState_Id] AS [WorkItemState_Id], 
    [Limit1].[ItemType_Id] AS [ItemType_Id], 
    [Limit1].[Repaired] AS [Repaired], 
    [Limit1].[MachineCycle] AS [MachineCycle], 
    [Limit1].[FirstSeenCheck] AS [FirstSeenCheck], 
    [Limit1].[LastSeen] AS [LastSeen], 
    [Limit1].[Archive] AS [Archive], 
    [Limit1].[CastingDateString] AS [CastingDateString], 
    [Limit1].[Deleted] AS [Deleted], 
    [Limit1].[DMC2] AS [DMC2], 
    [Limit1].[Id1] AS [Id1], 
    [Limit1].[WorkPlace_Id] AS [WorkPlace_Id], 
    [Limit1].[CastingFormIdent_Id] AS [CastingFormIdent_Id], 
    [Limit1].[FormIdentItemType_Id] AS [FormIdentItemType_Id]
    FROM ( SELECT TOP (1) 
        [Extent1].[Id] AS [Id], 
        [Extent1].[DMC] AS [DMC], 
        [Extent1].[FirstSeen] AS [FirstSeen], 
        [Extent1    </frame>
    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
(@p__linq__0 nvarchar(4000))SELECT 
    [Limit1].[Id] AS [Id], 
    [Limit1].[DMC] AS [DMC], 
    [Limit1].[FirstSeen] AS [FirstSeen], 
    [Limit1].[DrawingNo] AS [DrawingNo], 
    [Limit1].[MachineId] AS [MachineId], 
    [Limit1].[WorkItemState_Id] AS [WorkItemState_Id], 
    [Limit1].[ItemType_Id] AS [ItemType_Id], 
    [Limit1].[Repaired] AS [Repaired], 
    [Limit1].[MachineCycle] AS [MachineCycle], 
    [Limit1].[FirstSeenCheck] AS [FirstSeenCheck], 
    [Limit1].[LastSeen] AS [LastSeen], 
    [Limit1].[Archive] AS [Archive], 
    [Limit1].[CastingDateString] AS [CastingDateString], 
    [Limit1].[Deleted] AS [Deleted], 
    [Limit1].[DMC2] AS [DMC2], 
    [Limit1].[Id1] AS [Id1], 
    [Limit1].[WorkPlace_Id] AS [WorkPlace_Id], 
    [Limit1].[CastingFormIdent_Id] AS [CastingFormIdent_Id], 
    [Limit1].[FormIdentItemType_Id] AS [FormIdentItemType_Id]
    FROM ( SELECT TOP (1) 
        [Extent1].[Id] AS [Id], 
        [Extent1].[DMC] AS [DMC], 
        [Extent1].[FirstSeen] AS [F   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <pagelock fileid="1" pageid="531207" dbid="5" subresource="FULL" objectname="LP_R.dbo.WorkItems" id="lock6d7b2d800" mode="S" associatedObjectId="72057594043891712">
   <owner-list>
    <owner id="process66f184558" mode="S" />
   </owner-list>
   <waiter-list>
    <waiter id="process6472ad498" mode="IX" requestType="wait" />
   </waiter-list>
  </pagelock>
  <pagelock fileid="1" pageid="114492" dbid="5" subresource="FULL" objectname="LP_R.dbo.WorkItems" id="lock5cd1a2b00" mode="IX" associatedObjectId="72057594043891712">
   <owner-list>
    <owner id="process6472ad498" mode="IX" />
   </owner-list>
   <waiter-list>
    <waiter id="process66f184558" mode="S" requestType="wait" />
   </waiter-list>
  </pagelock>
 </resource-list>
</deadlock>

Upvotes: 1

Views: 1170

Answers (2)

David Browne - Microsoft
David Browne - Microsoft

Reputation: 88931

First, since there are S lock in this deadlock, consider switching the database to READ COMMITTED SNAPSHOT, so that your SELECT queries will use row versioning instead of S locks to read the database. This will solve all S/X deadlock and other blocking in one fell swoop, but you will need to test.

Second, to address this deadlock, use a transaction in your stored procedure and get a big lock early. For instance instead of an IX lock force it to acquire an exclusive table lock with the TABLOCKX hint. Deadlocks only happen when two sessions first get compatible locks, then later try to get incompatible locks. IX and S locks are the start of this deadlock, so you can head it off by making sure that the stored procedure doesn't acquire the puny IX, and wait until it can acquire a lock that will enable it to complete successfully.

Upvotes: 1

IVNSTN
IVNSTN

Reputation: 9299

(as a side-note)

delete c 
from dbo.charges c
inner join @chargeids t
  on t.id = c.id
where not exists(
  select 1 from dbo.chargeitems ci 
  where ci.Charge_id = c.id
)

I suppose there are no chargeitems already at this stage actually. And they were probably deleted just before this delete statement. So archiving process transaction could be much longer then it's shown.

So, your delete process is deleting specific rows and places an intent-X lock on pages, whilst reading process is scanning pages, probably, in different order. FKs could place some more locks if there are any.

TIPS

Have a look at select statement's actual execution plan. It is requesting TOP 1 with no order, uses far not precise where predicate and joins chargeitems on workitem_id (which perhaps has no index on it). Fixing it could help getting rid of scan (if any) while reading. May be you could try to select top1 workitem and only after that select top1 chargeitem for it.

You could try to apply READPAST hint on reading statement (will not be waiting for locked pages) or raise granularity of delete statements to PAGLOCK for example. Try TABLOCK for delete process if it's rarely executed, and if it's fine for this system.

UPD

Actually I missed the major point: you pointed at delete from charges whereas deadlock is on WorkItem (as deadlock graph is clearly showing). But this does not cancel the rest of my assumptions. As the execution plan shows WorkItem is truly scanned whilst this delete is performed on specific rows:

delete from LP_R.dbo.workitems where id in (select id from @workitemIds); 

You can apply tips from my post to select statement and/or delete statement (all of them within archiving proc).

Upvotes: 1

Related Questions