Reduce locking and lock escalation
) locks on the clustered index pages containing those rows.
Intent exclusive (
) locks on the clustered index pages containing those rows.
An
lock on the clustered index and another on the table.
The
statement acquires these locks:
Shared (
) locks on all data rows it reads, unless the row is already protected by an
lock
from the
statement.
Intent Shared (
) locks on all clustered index pages containing those rows, unless the page
is already protected by an
lock.
No lock on the clustered index or table because they’re already protected by
locks.
If the
statement acquires enough locks to trigger lock escalation and the escalation
succeeds, the
lock on the table is converted to an
lock, and all the row, page, and index
locks are released. Both the updates and reads are protected by the
lock on the table.
In most cases, the Database Engine delivers the best performance when operating with its default
settings for locking and lock escalation.
Take advantage of
optimized locking.
Optimized locking
offers an improved transaction locking mechanism that reduces lock
memory consumption and blocking for concurrent transactions. Lock escalation is far less
likely to ever occur when optimized locking is enabled.
Avoid using
table hints with optimized locking. Table hints might reduce the effectiveness
of optimized locking.
Enable the
READ_COMMITTED_SNAPSHOT
option for the database for the most benefit
from optimized locking. This is the default in Azure SQL Database.
Optimized locking requires
accelerated database recovery (ADR)
to be enabled on the
database.
If an instance of the Database Engine generates a lot of locks and is seeing frequent lock
escalations, consider reducing the amount of locking with the following strategies:
Use an isolation level that doesn’t generate shared locks for read operations:
isolation level when the
database option is.
isolation level.
isolation level. This can only be used for systems that can operate with
dirty reads.
Use the
or
table hints to have the Database Engine use page, heap, or
index locks instead of low-level locks. Using this option, however, increases the problems of
users blocking other users attempting to access the same data and shouldn’t be used in
systems with more than a few concurrent users.
If optimized locking isn’t available, for partitioned tables, use the
option of
ALTER TABLE
to escalate locks to the partition instead of the table, or to disable lock
escalation for a table.
Break up large batch operations into several smaller operations. For example, suppose you
ran the following query to remove several hundred thousand old rows from an audit table,
and then you found that it caused a lock escalation that blocked other users:
By removing these rows a few hundred at a time, you can dramatically reduce the number
of locks that accumulate per transaction and prevent lock escalation. For example:
Reduce a query lock footprint by making the query as efficient as possible. Large scans or
large numbers of key lookups might increase the chance of lock escalation; additionally,
that increases the chance of deadlocks, and generally adversely affects concurrency and
performance. After you find the query that causes lock escalation, look for opportunities to
create new indexes or to add columns to an existing index to remove full index or table
scans and to maximize the efficiency of index seeks. Consider using the
Database Engine
Tuning Advisor
to perform an automatic index analysis on the query. For more information,
see
Tutorial: Database Engine Tuning Advisor. One goal of this optimization is to make
index seeks return as few rows as possible to minimize the cost of key lookups (maximize
the selectivity of the index for the particular query). If the Database Engine estimates that a
key lookup logical operator might return many rows, it might use a prefetch optimization to
perform the lookup. If the Database Engine does use prefetch for a lookup, it must increase
the transaction isolation level of a portion of the query to. This means that
what might look similar to a
statement at a
isolation level might
acquire many thousands of key locks (on both the clustered index and one nonclustered
index), which can cause such a query to exceed the lock escalation thresholds. This is
especially important if you find that the escalated lock is a shared table lock, which,
however, isn’t commonly seen at the default
isolation level.
If a key lookup with the prefetch optimization is causing lock escalation, consider adding
additional columns to the nonclustered index that appears in the Index Seek or the Index
Scan logical operator below the key lookup logical operator in the query plan. It might be
possible to create a covering index (an index that includes all columns in a table that were
used in the query), or at least an index that covers the columns that were used for join
criteria or in the
clause if including everything in the
column list is impractical.
A Nested Loop join might also use the prefetch optimization, and this causes the same
locking behavior.
Lock escalation can’t occur if a different SPID is currently holding an incompatible table lock.
Lock escalation always escalates to a table lock, and never to page locks. Additionally, if a
lock escalation attempt fails because another SPID holds an incompatible table lock, the
query that attempted escalation doesn’t block while waiting for a table lock. Instead, it
continues to acquire locks at its original, more granular level (row, key, or page), periodically
making additional escalation attempts. Therefore, one method to prevent lock escalation on
a particular table is to acquire and hold a lock on a different connection that isn’t
compatible with the escalated lock type. An intent exclusive (
) lock at the table level
doesn’t lock any rows or pages, but it’s still not compatible with an escalated shared (
) or
exclusive (
) table lock. For example, assume that you must run a batch job that modifies a
large number of rows in the
table and that has caused blocking that occurs
because of lock escalation. If this job always completes in less than an hour, you might
create a Transact-SQL job that contains the following code, and schedule the new job to
start several minutes before the batch job’s start time:
MSSQLSERVER_1204
IX
IX
SELECT
S
X
UPDATE
IS
IX
IX
SELECT
IX
X
X
READ COMMITTED
READ_COMMITTED_SNAPSHOT
ON
SNAPSHOT
READ UNCOMMITTED
PAGLOCK
TABLOCK
LOCK_ESCALATION
DELETE
FROM
LogMessages
WHERE
LogDate <
'2024-09-26'
DECLARE
@DeletedRows int
;
WHILE @DeletedRows IS NULL OR @DeletedRows > 0
BEGIN
DELETE
TOP (500)
FROM
LogMessages
WHERE
LogDate <
'2024-09-26'
SELECT
@DeletedRows = @@ROWCOUNT;
END
;
REPEATABLE READ
SELECT
READ COMMITTED
READ COMMITTED
WHERE
SELECT
IX
S
X
mytable
BEGIN
TRAN;
SELECT
*
FROM mytable
WITH (UPDLOCK, HOLDLOCK)