SQLServerCentral Article

Dealing with High Concurrency with Table Hints

,

Recently I’ve read this post on how to handle hot rows in the upcoming version 8.0.1 of MySQL: http://mysqlserverteam.com/mysql-8-0-1-using-skip-locked-and-nowait-to-handle-hot-rows/. Also, while writing this article, I’ve come across this question on SQLServerCentral: https://www.sqlservercentral.com/Forums/1874362/Concurrency-data-access-with-UPDLOCK-and-READPAST. Given that these situations can definitely be considered real life scenarios for most highly concurrent applications, I decided to do some testing and documenting using Microsoft SQL Server.

First, let’s create a table for the seat booking scenario and put some rows on it:

-- create table
CREATE TABLE seats
(   seat_no INT PRIMARY KEY,
    booked  BIT
        DEFAULT 0
);
-- generate 100 sample rows
DECLARE @i INT = 0;
WHILE @i < 100
BEGIN
    SET @i = @i + 1;
    INSERT INTO seats (seat_no) VALUES (@i);
END;

Now let’s run a transaction using the updlock table hint, which means I want a select for update:

BEGIN TRANSACTION;
SELECT *
FROM seats WITH (UPDLOCK)
WHERE
    seat_no
    BETWEEN 2 AND 3
    AND booked = 0;
--release the locks
--commit

Check the locks:

You have an update intent page lock, as well as two-row locks. Run a new query with another transaction for different records on the same table:

BEGIN TRANSACTION;
SELECT *
FROM seats WITH (UPDLOCK)
WHERE
    seat_no
    BETWEEN 1 AND 7
    AND booked = 0;
--release the locks
--commit

Both result sets appeared instantly, the locks will be as follows:

Run the commit statements to release the locks. Execute both statements, but this time include all records on the second query statement:

BEGIN TRANSACTION;
SELECT *
FROM seats WITH (UPDLOCK)
WHERE
    seat_no
    BETWEEN 1 AND 7
    AND booked = 0;

Find blocked processes and command text:

SELECT
    DBName = db.name,
    tl.request_session_id,
    wt.blocking_session_id,
    BlockedObjectName = OBJECT_NAME(p.object_id),
    tl.resource_type,
    RequestingText = h1.text,
    BlockingTest = h2.text,
    tl.request_mode
FROM
    sys.dm_tran_locks tl
    INNER JOIN sys.databases db
        ON db.database_id = tl.resource_database_id
    INNER JOIN sys.dm_os_waiting_tasks wt
        ON tl.lock_owner_address = wt.resource_address
    INNER JOIN sys.partitions p
        ON p.hobt_id = tl.resource_associated_entity_id
    INNER JOIN sys.dm_exec_connections ec1
        ON ec1.session_id = tl.request_session_id
    INNER JOIN sys.dm_exec_connections ec2
        ON ec2.session_id = wt.blocking_session_id
    CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) h1
    CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) h2;
GO

You will observe that the second statement is being blocked by the locks on the first transaction:

For the sake of argument, let’s create the same table with a different name and set the ALLOW_PAGE_LOCKS option to OFF. Maybe if we get just some row level locking, the query will return some rows:

CREATE TABLE dbo.seats2
(   seat_no INT NOT NULL,
    booked  BIT NULL,
    PRIMARY KEY CLUSTERED (seat_no ASC)
    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
             IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
             ALLOW_PAGE_LOCKS = OFF
         ) ON [PRIMARY]
) ON [PRIMARY];
GO
ALTER TABLE dbo.seats2 ADD DEFAULT ((0)) FOR booked;
GO

Let’s, again, issue two separate transactions for the same set of rows. Run the first one and watch the difference on the lock types:

Great! The page lock is gone so I should be able to get some records? Not really, the second transaction is still blocked by the first because it needs rows currently held for the first transaction. And it’s the intended behavior because it should not let me alter records where I currently have pending transactions. If I had paid attention to the blocking process query, the lock type on the result is KEY, so I knew in advance that getting rid of page locks wouldn’t help.

For a hot rows scenario, where I really want to get the free rows and ignore the ones with pending transactions, I’ll have to resort to another t-sql table hint, namely READPAST.

Run the commit statements to release the locks. Execute the first statement again. Then execute the second statement, but this time use the READPAST hint:

BEGIN TRANSACTION;
SELECT *
FROM seats2 WITH (UPDLOCK, READPAST)
WHERE
    seat_no
    BETWEEN 1 AND 7
    AND booked = 0;

And there you go, the rows are returned instantly, leaving out the one locked on the first still pending transaction:

Using table hints you can override the default behavior of the query optimizer for the duration of the data manipulation language (DML) statement by specifying a locking method. Table hints are specified in the FROM clause of the DML statement and affect only the table or view referenced in that clause.

Reference: https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table

Rate

4.8 (5)

You rated this post out of 5. Change rating

Share

Share

Rate

4.8 (5)

You rated this post out of 5. Change rating