...why with clustered index, the deadlock is still there (though hit rate seems to be dropped)
The question isn't precisely clear (e.g. how many updates and to which id
values are in each transaction) but one obvious deadlock scenario arises with multiple single-row updates within a single transaction, where there is an overlap of [id]
values, and the ids are updated in a different [id]
order:
[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;
Deadlock sequence: T1 (u2), T2 (u1), T1 (u1) wait, T2 (u2) wait.
This deadlock sequence might be avoided by updating strictly in id order within each transaction (acquiring locks in the same order on the same path).
When use the clustered index, there is an exclusive lock on the key as well as an exclusive lock on RID when do update, which is expected; while there are two exclusive lock on two different RID if non-clustered index is used, which confuses me.
With a unique clustered index on id
, an exclusive lock is taken on the clustering key to protect writes to the in-row data. A separate RID
exclusive lock is required to protect the write to the LOB text
column, which is stored on a separate data page by default.
When the table is a heap with only a nonclustered index on id
, two things happen. First, one RID
exclusive lock relates to the heap in-row data, and the other is the lock on the LOB data as before. The second effect is that a more complex execution plan is required.
With a clustered index and a simple single-value equality predicate update, the query processor can apply an optimization that performs the update (read and write) in a single operator, using a single path:
The row is located and updated in a single seek operation, requiring only exclusive locks (no update locks are needed). An example locking sequence using your sample table:
acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB
releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW
With only a nonclustered index, the same optimization cannot be applied because we need to read from one b-tree structure and write another. The multi-path plan has separate read and write phases:
This acquires update locks when reading, converting to exclusive locks if the row qualifies. Example lock sequence with the schema given:
acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB
releasing lock reference on PAGE: 6:1:59091
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105
Note the LOB data is read and written at the Table Update iterator. The more complex plan and multiple read and write paths increase the chances of a deadlock.
Finally, I can't help but notice the data types used in the table definition. You should not use the deprecated text
data type for new work; the alternative, if you really need the ability to store up to 2GB of data in this column, is varchar(max)
. One important difference between text
and varchar(max)
is that text
data is stored off-row by default, while varchar(max)
stores in-row by default.
Use Unicode types only if you need that flexibility (e.g. it is hard to see why an IP address would need Unicode). Also, choose appropriate length limits for your attributes - 255 everywhere seems unlikely to be correct.
Additional reading:
Deadlock and livelock common patterns
Bart Duncan's deadlock troubleshooting series
Tracing locks can be done in a variety of ways. SQL Server Express with Advanced Services (2014 & 2012 SP1 onward only) contains the Profiler tool, which is a supported way to view the details of lock acquisition and release.
Best Answer
You are approaching this from the wrong direction. Any query can deadlock anytime. So, you must be prepared to replay the entire transaction when a deadlock occurs.
Sure, it is a good idea to try to avoid deadlocks, there there are several things that can decrease their frequency. The main technique is to make queries faster.
In the case you have mentioned, the Optimizer probably decided that avoiding the 'sort' for the
ORDER BY
was better than using any other index, or even using no index. Let's seeSHOW CREATE TABLE
andEXPLAIN SELECT
to further dissect what happened.Another technique is to include the optimal index -- often a "composite" index over multiple columns. Sometimes it is even good to have a "covering" index -- one that includes all the columns mentioned in the query.
When using
IN
, another technique is to sort the values.