How to Manage Locking in SQL Server for Optimal Performance

Aleh Belausau
Towards Data Engineering
13 min readMay 12, 2024
Photo by Parsoa Khorsand on Unsplash

To get the most out of your database, you need to strike a careful balance between keeping your data safe and making sure everything runs smoothly and SQL Server’s locking features can help you with that. Locking makes sure that different people or processes can use and change data at the same time without messing things up. But dealing with locking in SQL Server can be tricky because it’s hard to manage when lots of people are trying to access the database at once.

SQL Server employs multigranular locking to minimize locking costs by allowing transactions to lock different types of resources, including row-level, page-level, and table-level locks, but despite its benefits, locking can complicate SQL queries for several reasons:

  • Performance Impact: Locks can significantly affect performance. If a transaction holds a lock on a row or table for an extended period, it can cause delays for other transactions needing access to the same resource, resulting in performance issues.
  • Deadlocks: In some scenarios, multiple transactions may each hold a lock on a resource required by another transaction, leading to a deadlock situation.
  • Lock Escalation: SQL Server may automatically escalate a large number of row or page locks to a table lock for efficiency, potentially blocking other users.

SQL Server offers locking hints to override the default behavior of the query optimizer. However, using these hints requires caution as they can introduce data inconsistency. It’s essential to understand each hint thoroughly and consider examples before implementing them in your queries. Let’s explore each locking hint in detail and examine practical examples to grasp their usage effectively.

NOLOCK

This hint allows SQL Server to read data from tables by ignoring any locks and therefore not being blocked by other processes. This can improve query performance but also means that the data returned might not be up-to-date or correct.

Imagine you have a background process that regularly updates a summary table with the total sales for each product category. A reporting tool queries this summary table to generate reports for management.

To ensure the reporting tool’s performance is optimized and not delayed by ongoing updates to the summary table, you might use the NOLOCK hint in the query:

SELECT Category, TotalSales 
FROM SummarySalesTable WITH (NOLOCK);

By adding the NOLOCK hint, the query can retrieve data from the summary table without waiting for locks, potentially improving the reporting tool’s responsiveness. However, be cautious as using NOLOCK can lead to reading uncommitted or inconsistent data.

READUNCOMMITTED

This hint is similar to NOLOCK. It specifies that the Database Engine not issue shared locks and not honor exclusive locks. When this option is set, it is possible to read uncommitted or dirty data; values in the data can be changed and rows can appear or disappear in the data set before the end of the transaction.

Consider a scenario where a web application needs to quickly fetch user comments for display on a webpage. These comments are stored in a database table named UserComments. To ensure fast retrieval and not be delayed by ongoing updates to the comments table, you might use the READUNCOMMITTED hint in the query:

SELECT CommentText, Author, Date 
FROM UserComments WITH (READUNCOMMITTED)
WHERE PostID = @PostID;

By applying the READUNCOMMITTED hint, the query can read data from the UserComments table without waiting for locks to be released, potentially enhancing the web application's performance. However, it's important to note that READUNCOMMITTED can lead to reading uncommitted or inconsistent data, so it should be used cautiously, especially if data accuracy is critical.

UPDLOCK

This hint forces all readers of the data to take update locks instead of shared locks. This means that when a transaction reads the data, it also takes a lock that indicates an intention to update it later. By doing so, UPDLOCK helps to prevent deadlocks by ensuring that other transactions cannot concurrently update the same data that is being read.

Consider a scenario in a banking system where customers can transfer funds between their accounts. When a transfer is initiated, the system needs to first check the balance of the sender’s account and then update both the sender’s and receiver’s account balances.

To ensure data consistency and avoid potential deadlocks, you might use the UPDLOCK hint in the transaction that reads the sender’s account balance before updating it:

BEGIN TRANSACTION;
DECLARE @SenderBalance DECIMAL(10,2);
-- Read sender's account balance with an exclusive update lock
SELECT @SenderBalance = Balance
FROM Accounts WITH (UPDLOCK)
WHERE AccountNumber = @SenderAccountNumber;
-- Update sender's account balance
UPDATE Accounts
SET Balance = Balance - @TransferAmount
WHERE AccountNumber = @SenderAccountNumber;
-- Update receiver's account balance
UPDATE Accounts
SET Balance = Balance + @TransferAmount
WHERE AccountNumber = @ReceiverAccountNumber;
COMMIT TRANSACTION;

By applying the UPDLOCK hint in the query that reads the sender’s account balance, the system ensures exclusive update locks are taken on the relevant rows. This prevents other transactions from modifying the sender’s account balance concurrently, reducing the risk of deadlocks when multiple transfers occur simultaneously. However, it’s crucial to use UPDLOCK appropriately and consider the potential impact on concurrency and performance.

REPEATABLEREAD

This hint specifies that locks are to be taken and held until the transaction completes. This prevents others from updating or inserting rows into the data set until the transaction is complete.

Let’s consider a scenario in an online reservation system where customers can book tickets for events. When a customer selects seats for an event, the system needs to first check the availability of the selected seats and then reserve them if they are available.

To ensure data consistency and avoid potential conflicts, you might use the REPEATABLEREAD hint in the transaction that checks the availability of seats before reserving them:

BEGIN TRANSACTION;
DECLARE @SeatsAvailable INT;
-- Check the availability of selected seats with repeatable read
SELECT @SeatsAvailable = SeatsAvailable
FROM EventSeats WITH (REPEATABLEREAD)
WHERE EventID = @EventID AND SeatNumber IN (@SelectedSeats);
-- Check if the selected seats are available
IF @SeatsAvailable >= @NumSeatsToBook
BEGIN
-- Reserve the selected seats
UPDATE EventSeats
SET SeatsAvailable = SeatsAvailable - @NumSeatsToBook
WHERE EventID = @EventID AND SeatNumber IN (@SelectedSeats);
COMMIT TRANSACTION;
PRINT 'Seats reserved successfully.';
END
ELSE
BEGIN
ROLLBACK TRANSACTION;
PRINT 'Seats not available.';
END

By applying the REPEATABLEREAD hint in the query that checks the availability of seats, the system ensures a repeatable read isolation level, where the same data is read consistently throughout the transaction. This prevents other transactions from modifying the availability of the selected seats concurrently, reducing the risk of conflicts when multiple customers attempt to book seats simultaneously. However, it’s important to use REPEATABLEREAD appropriately and consider the trade-offs in terms of concurrency and data consistency.

SERIALIZABLE

This hint specifies that the Database Engine locks the entire range of key values that qualify for the statements of the transaction. This prevents other transactions from updating or inserting rows that would qualify for any of the statements in the transaction. In simpler terms, it ensures that no other transaction can change the data that the current transaction is working with until it’s finished, ensuring a consistent view of the data.

Let’s envision a scenario in an online auction platform where users can bid on items. When a user places a bid on an item, the system needs to ensure that no other bids are accepted for the same item until the current bid transaction completes.

To maintain data integrity and prevent concurrent bids on the same item, you might use the SERIALIZABLE hint in the transaction that retrieves the current highest bid before accepting a new bid:

BEGIN TRANSACTION;
DECLARE @CurrentHighestBid DECIMAL(10,2);
-- Retrieve the current highest bid with serializable isolation level
SELECT @CurrentHighestBid = MAX(BidAmount)
FROM Bids WITH (SERIALIZABLE)
WHERE ItemID = @ItemID;
-- Check if the new bid is higher than the current highest bid
IF @NewBidAmount > @CurrentHighestBid
BEGIN
-- Accept the new bid
INSERT INTO Bids (ItemID, UserID, BidAmount)
VALUES (@ItemID, @UserID, @NewBidAmount);
COMMIT TRANSACTION;
PRINT 'Bid accepted successfully.';
END
ELSE
BEGIN
ROLLBACK TRANSACTION;
PRINT 'Your bid is not higher than the current highest bid.';
END

By applying the SERIALIZABLE hint in the query that retrieves the current highest bid, the system ensures a serializable isolation level, where concurrent transactions cannot modify the data being read until the transaction completes. This prevents other bids from being accepted for the same item simultaneously, maintaining the integrity of the bidding process. However, it’s important to use SERIALIZABLE appropriately and consider the potential impact on concurrency and performance.

READCOMMITTED

This hint specifies that shared locks are to be held while the data is being read to avoid dirty reads, but the data can be changed before the end of the transaction, this can lead to issues like non-repeatable reads, where the same query may return different results during the same transaction, or phantom data, where new rows might appear or disappear while reading.

Let’s consider a scenario in a retail application where customers can place orders for products. When a customer places an order, the system needs to check the current stock level of the ordered products before confirming the order.

To ensure that the stock level information is up-to-date while still allowing other transactions to modify it, you might use the READCOMMITTED hint in the transaction that retrieves the current stock level:

BEGIN TRANSACTION;
DECLARE @CurrentStockLevel INT;
-- Retrieve the current stock level with read committed isolation level
SELECT @CurrentStockLevel = StockLevel
FROM Products WITH (READCOMMITTED)
WHERE ProductID = @ProductID;
-- Check if there is enough stock to fulfill the order
IF @CurrentStockLevel >= @OrderQuantity
BEGIN
-- Confirm the order and update the stock level
UPDATE Products
SET StockLevel = StockLevel - @OrderQuantity
WHERE ProductID = @ProductID;
COMMIT TRANSACTION;
PRINT 'Order confirmed successfully.';
END
ELSE
BEGIN
ROLLBACK TRANSACTION;
PRINT 'Insufficient stock to fulfill the order.';
END

By applying the READCOMMITTED hint in the query that retrieves the current stock level, the system ensures a read committed isolation level, where only committed data is read, thus providing more accurate stock information. However, it allows other transactions to modify the data concurrently, enabling better concurrency while still maintaining data consistency. Nonetheless, it’s crucial to use READCOMMITTED judiciously and consider potential issues related to data inconsistency in dynamic environments.

TABLOCK

This hint specifies that a lock is taken on the table and held until the end of the statement or transaction.

Let’s envision a scenario in a batch processing system where multiple users can submit data for processing concurrently. When a user submits a large dataset for processing, the system needs to ensure exclusive access to the processing table until the data processing completes to prevent interference from other concurrent operations.

To ensure exclusive access to the processing table and prevent interference during data processing, you might use the TABLOCK hint in the transaction that handles the data processing:

BEGIN TRANSACTION;
-- Insert data into the processing table with table lock
INSERT INTO ProcessingTable WITH (TABLOCK)
SELECT *
FROM SubmittedData;
-- Perform complex data processing operations
-- (e.g., data validation, transformation, aggregation)
COMMIT TRANSACTION;
PRINT 'Data processing completed successfully.';

By applying the TABLOCK hint in the query that inserts data into the processing table, the system ensures that an exclusive table lock is acquired, preventing other transactions from accessing or modifying the processing table until the data processing completes. This ensures data integrity and prevents interference from concurrent operations during critical data processing tasks. However, it’s essential to use TABLOCK judiciously, considering potential performance implications and ensuring that the duration of the table lock is minimized to avoid blocking other transactions unnecessarily.

TABLOCKX

This hint specifies that an exclusive lock is taken on the table until the end of the statement or transaction.

When a lock is taken on a table, it means that the Database Engine is controlling access to that entire table to ensure data integrity and consistency during transactions. This prevents other transactions from modifying the table while the lock is held.

An exclusive lock on a table goes a step further. It not only prevents other transactions from modifying the table’s data but also prevents any other transactions from even reading the data from the table. Essentially, an exclusive lock gives the transaction exclusive control over the entire table, ensuring that no other transaction can interfere with it in any way.

Consider a situation where a company is reorganizing its employee database, merging duplicate entries, and updating employee records. To guarantee exclusive access to the employee table during this process, the TABLOCKX hint is utilized:

BEGIN TRANSACTION;
-- Start the transaction and acquire an exclusive table lock
-- to prevent other transactions from accessing the employee table
SELECT *
FROM Employee
WITH (TABLOCKX)
WHERE Status = 'Duplicate';
-- Perform the necessary data updates, merges, or deletions
COMMIT TRANSACTION;
PRINT 'Employee database reorganization completed successfully.';

By incorporating the TABLOCKX hint, an exclusive table lock is acquired, ensuring that no other transactions can read or modify the employee table during the reorganization process. This safeguards data integrity and consistency during critical database operations. Nonetheless, it’s imperative to exercise caution when using TABLOCKX to prevent blocking other transactions unnecessarily and to minimize the duration of the exclusive lock.

PAGLOCK

This hint specifies that a lock is taken on the page and held until the end of the statement or transaction.

A “page” in the context of databases refers to a contiguous block of data storage within a database file. It’s a fundamental unit of data storage and retrieval, typically consisting of a fixed size (often 8 KB in SQL Server). Taking a lock on the page level can be beneficial in certain scenarios because it allows for more granular control over concurrency and resource utilization.

Imagine a situation where a batch process needs to update specific sections of a large inventory table. To prevent interference from other transactions and maintain data consistency, the PAGLOCK hint is utilized:

BEGIN TRANSACTION;
-- Start the transaction and acquire page-level locks for the specified pages
SELECT *
FROM InventoryTable
WITH (PAGLOCK)
WHERE Category = 'Electronics';
-- Perform the necessary data updates or modifications
COMMIT TRANSACTION;
PRINT 'Inventory updates for electronics completed successfully.';

By incorporating the PAGLOCK hint, exclusive locks are acquired at the page level for the specified sections of the inventory table. This prevents other transactions from accessing or modifying those specific pages until the updates or modifications are completed. However, it’s crucial to use PAGLOCK judiciously, considering its potential impact on concurrency and ensuring that the duration of the locks is minimized to avoid blocking other transactions unnecessarily.

ROWLOCK

This hint specifies that a lock is taken on the row and held until the end of the statement or transaction.

Consider a situation where an e-commerce platform processes orders, and each order involves updating the order status in the database. To prevent interference from other transactions and ensure data consistency, the ROWLOCK hint is utilized:

BEGIN TRANSACTION;
-- Start the transaction and acquire row-level locks for the selected rows
SELECT *
FROM Orders
WITH (ROWLOCK)
WHERE OrderStatus = 'Pending';
-- Update the status of selected orders
UPDATE Orders
SET OrderStatus = 'Processing'
WHERE OrderStatus = 'Pending';
COMMIT TRANSACTION;
PRINT 'Order processing completed successfully.';

By incorporating the ROWLOCK hint, exclusive locks are acquired at the row level for the selected orders with a ‘Pending’ status. This ensures that only the rows being updated have exclusive locks, allowing other transactions to access and modify other rows concurrently, thereby improving concurrency and reducing contention. However, it’s essential to use ROWLOCK judiciously, considering its impact on concurrency and ensuring that the duration of the locks is minimized to avoid blocking other transactions unnecessarily.

NOWAIT

This hint specifies that the Database Engine not wait on locks and instead return a message as soon as a lock is encountered.

Imagine a situation where multiple users attempt to update the same record simultaneously in a banking system. To prevent the possibility of one transaction waiting indefinitely for a lock, the NOWAIT hint is employed:

BEGIN TRANSACTION;
-- Attempt to acquire an exclusive lock on the record without waiting
SELECT *
FROM Account
WITH (NOWAIT)
WHERE AccountNumber = @AccountNumber;
-- Perform updates or modifications if the lock is acquired successfully
-- If NOWAIT cannot acquire the lock immediately, an error will be raised
COMMIT TRANSACTION;
PRINT 'Transaction completed successfully.';

By incorporating the NOWAIT hint, the query attempts to acquire an exclusive lock on the specified record without waiting. If the lock cannot be acquired immediately, the query fails with an error, preventing indefinite waits and ensuring that transactions do not hang indefinitely due to lock contention. However, it’s crucial to handle the error gracefully in the application code and retry the operation if necessary. Additionally, NOWAIT should be used judiciously, as it may lead to increased contention.

READPAST

This hint specifies that the Database Engine not read rows that are locked by other transactions. When READPAST is specified, row-level locks are skipped. That is, the Database Engine skips past the rows instead of blocking the current transaction until the locks are released.

Consider a situation where multiple users attempt to read data from a table concurrently, but some rows are locked by other transactions. To prevent waiting for locked rows and instead read only the unlocked rows, the READPAST hint is employed:

BEGIN TRANSACTION;
-- Read data from the table, skipping over locked rows
SELECT *
FROM TableName
WITH (READPAST)
WHERE Condition = @Condition;
COMMIT TRANSACTION;
PRINT 'Data retrieval completed successfully.';

By incorporating the READPAST hint, the query skips over rows that are currently locked by other transactions and reads only the unlocked rows. This allows the transaction to continue processing without waiting for the locked rows to be released, improving concurrency and reducing contention. However, it’s important to consider potential data consistency issues when using READPAST, as it may lead to skipping over important data that is temporarily locked.

XLOCK

This hint specifies that exclusive locks are to be taken and held until the transaction completes.

Imagine a situation where critical data integrity needs to be maintained during a complex data update or transaction. To ensure exclusive access to the rows being modified and prevent interference from other transactions, the XLOCK hint is employed:

BEGIN TRANSACTION;
-- Start the transaction and acquire exclusive locks on the specified rows
SELECT *
FROM TableName
WITH (XLOCK, ROWLOCK)
WHERE Condition = @Condition;
-- Perform updates or modifications within the transaction
COMMIT TRANSACTION;
PRINT 'Transaction completed successfully.';

By incorporating the XLOCK hint, exclusive locks are acquired on the specified rows, ensuring that no other transactions can access or modify them until the current transaction completes. This prevents interference and maintains data integrity during critical database operations. However, it’s essential to use XLOCK judiciously, considering its impact on concurrency and ensuring that the duration of the locks is minimized to avoid blocking other transactions unnecessarily.

SNAPSHOT

This hint specifies that data read by any statement in a transaction will be the transactionally consistent version of the data that existed at the start of the transaction.

Consider a situation where a reporting system needs to generate reports based on data that remains consistent throughout the report generation process, regardless of concurrent transactions modifying the data. To achieve this, the SNAPSHOT isolation level is employed:

BEGIN TRANSACTION;

-- Set the transaction isolation level to SNAPSHOT using a hint
SELECT *
FROM TableName WITH (SNAPSHOT)
WHERE Condition = @Condition;

-- Perform additional queries or operations within the transaction

COMMIT TRANSACTION;

By setting the transaction isolation level to SNAPSHOT, the database engine generates a consistent snapshot of the database at the start of the transaction. This snapshot ensures that the data remains consistent and does not change during the transaction, even if other transactions modify the data concurrently. This allows the reporting system to generate reports based on a stable and consistent view of the data, improving data integrity and accuracy. However, it’s essential to consider the overhead associated with maintaining snapshot versions of data, especially in high-concurrency environments.

SQL Server’s locking hints are powerful tools. Each locking hint serves specific purposes and should be applied judiciously based on the requirements of the application and the desired balance between data consistency and performance.

Please note that these are just possible use cases and the actual use of these hints will depend on the specific requirements of your database and application. Always test thoroughly before using hints in a production environment.

--

--

Aleh Belausau
Towards Data Engineering

Data and Software Engineer specializing in using cloud technologies for business growth. https://www.linkedin.com/in/aleh-belausau/