Skip to content

Shared locks in standard transaction isolations

While there are many types of locks in SQL Server 2005 issues generally revolve around Shared (read), Exclusive (write) and Update (read now to write later) locks on resources. And given that the majority of operations on a database are read operations, the taking and holding of shared locks is often the centre of attention.

An Exclusive or Update lock taken during a transaction at any level of transaction isolation is held until the end of the transaction. Even under READ UNCOMMITTED isolation level where uncommitted rows from other transactions can be read. Shared locks however may be taken and released at different times during a query depending on the isolation level of the transaction.

Shared locks are taken for two reasons:

  • First to implement ACID isolation from existing operations, they are taken to ensure that the rows are not currently being modified as part of an uncommitted transaction.
  • Second to implement ACID isolation from future operations, locks are held for varying lengths of time to prevent rows from been modified another transaction.

I guess it’s really just one reason, as the holding of the locks impacts on the taking of the locks. But I find it easier to think of things in this way.

The sys.dm_tran_locks view shows all the locks held on a server; so I’ve created a view that filters the results by the connection ID to see what happening. The Lock modes page in Books Online discusses the various locks SQL Server uses and their Lock Compatibility.

I think this article post would be too long (to write and read) if I took a detailed look at the implications of the different locks taken. Hopefully I’ll find time soon to drill down into some of the situations and look at what concurrent operations are prohibited by the locks taken. The locks taken by the system will vary depending on the amount of data, its distribution, the operations performed and the load on the system. So the test scenario I have used below is deliberately simple and contrived as I just wanted to see what happened in the simplest case I could imagine; a single row in a single table with a clustered index.

You may also noticed I have not covered any of the snapshot isolation levels; again for simplicity I’ve stuck with the old pessimistic modes.

First to setup the test environment.

create database locks;
go

use locks;
go

create table data
    ( col1 char(1) primary key)

insert into
    data
values
    ('a');
go

--drop view vwLocks
create view
    vwLocks
as
    select 
        cast(request_mode as varchar(5)) as request_mode ,
        cast(request_status as varchar(10)) as request_status ,
        cast(resource_type as varchar(15)) as resource_type,
        case 
            when resource_type = 'OBJECT' then
                cast(object_name(resource_associated_entity_id) as varchar(20)) 
            else
                cast(resource_associated_entity_id as varchar(20))
        end as resource_name,
        cast(resource_description as varchar(15)) as resource_description
    from 
        sys.dm_tran_locks
    where
        request_session_id = @@spid
go

-- drop and re-establish the connection to close any locks 
-- hanging around from creating the db.

The view vwLocks returns just the information I need for now to see what locks have been taken. To see the change in the locking levels I’ll:

  1. Begin a transaction
  2. View the locks before performing a read
  3. View the locks during a read by joining the data table and vwLocks
  4. View the locks after the read
  5. Commit the transaction
  6. View the locks after the transaction

All the queries look like the one below with the isolation level changed.

set transaction isolation level serializable
begin transaction

select * from vwLocks
select * from data cross apply vwLocks
select * from vwLocks
commit
select * from vwLocks

All the queries take a Share (S) lock on the Database to let others know they are using it. They also take varying locks on vwLocks to ensure its stability for the duration of the query.

SERIALIZABLE ISOLATION

Before the read, only the Database and vwLocks have locks taken on them. The transaction takes an Intent Shared (IX) lock on vwLocks to protect it during the query. I would have thought Shared (s) or Schema Stability (Sch-S) would be the lock of choice, so I’ll try to take a deeper look into this lock decision later.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                                                     

During the read the query takes an Intent Shared (IS) lock on the table (OBJECT, data); then on the page that contains the data (PAGE, 1:143) and finally a RANGES-S (R) lock over the PKID values (KEY). The locks are taken in that order as the intent locks are used to signal that the transaction may take a certain lock at a lower level.

According to the Lock Compatibility documentation an Intent Shared lock and an Intent Exclusive lock are compatible. As these locks can only be taken down to the Page level another lock is required to protect the rows read by the query.

The RANGES-S lock is a shared lock that protects a range of values in the key, the range is described by a hash value in the resource_description column. For a table with a clustered index the rows are locked through KEY locks rather then ROW locks. The RANGES-S lock could not be taken if there were any existing Update, Exclusive or Range modification lock on the key.

col1 request_mode request_status resource_type   resource_name        resource_description
---- ------------ -------------- --------------- -------------------- --------------------
a    S            GRANT          DATABASE        0                                   
a    IS           GRANT          PAGE            72057594038321152    1:143          
a    RangeS-S     GRANT          KEY             72057594038321152    (6100bc414817) 
a    IS           GRANT          OBJECT          data                                
a    IS           GRANT          OBJECT          vwLocks

After the read the same locks are in place with the addition of a new RANGES-S lock on the KEY. SERIALIZABLE isolation needs to protect against phantom reads; if I were to execute the same select again the in transaction it must return exactly the same rows (i.e. no new ones). Given the nature of my select this means no other rows can be added to the table until my transaction completes.

As the hash value “ffffffffffff” is used for the new RANGES-S lock on the table key it seems reasonable to assume this lock covers all possible key values. Preventing new rows been added to the table.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          PAGE            72057594038321152    1:143          
RangeS-S     GRANT          KEY             72057594038321152    (6100bc414817) 
IS           GRANT          OBJECT          data                                
RangeS-S     GRANT          KEY             72057594038321152    (ffffffffffff) 
IS           GRANT          OBJECT          vwLocks                             

After the transaction has committed we’re back to the same locks we had at the start.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

REPEATABLE READ ISOLATION

Before the read the same locks are taken as in SERIALIZABLE.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

During the read Intent Shared locks are taken on the table and then the page as in the SERIALIZABLE transaction. Once the query reaches the KEY object though it takes a Share lock rather than a Range lock. The resource_description for this lock is the same as the when the SERIALIZABLE transaction took a RangeS-S lock out on Key.

At the moment I don’t know why a Shared lock was chosen for the KEY rather than a Range lock. As you might expect though a Shared lock cannot be taken if there is any form of Update or Exclusive lock existing on the object.

col1 request_mode request_status resource_type   resource_name        resource_description
---- ------------ -------------- --------------- -------------------- --------------------
a    S            GRANT          DATABASE        0                                   
a    IS           GRANT          PAGE            72057594038321152    1:143          
a    S            GRANT          KEY             72057594038321152    (6100bc414817) 
a    IS           GRANT          OBJECT          data                                
a    IS           GRANT          OBJECT          vwLocks                             

After the read lock all of the same locks are in place. No new locks have been added.

REPEATABLE READ protects against the rows that were read in the original query been modified, so the Shared lock is maintained on the Key. It does not protect against new rows been added to the table that would then be returned in subsequent queries so a new RANGE lock is not added to prevent rows been added.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          PAGE            72057594038321152    1:143          
S            GRANT          KEY             72057594038321152    (6100bc414817) 
IS           GRANT          OBJECT          data                                
IS           GRANT          OBJECT          vwLocks                             

After committing the transaction we return to the initial same state.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

READ COMMITTED ISOLATION

Before the read the same locks are in place.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

During the read Intent Shared locks are taken on the table and the page, there is no locking on the key objects though.

READ COMMITTED must ensure that any rows been read have not been modified by an as yet uncommitted transaction, as does REPEATABLE READ and SERIALIZABLE. As I found above an Intent Shared lock can be taken if there is an existing Intent Exclusive lock, however it cannot be taken if there is an existing Update or Exclusive lock.

So my assumption here is that to modify a row a transaction must acquire an Update or Exclusive lock on the page. That would limit the granularity of the database to the page level, so I need to do some reading and testing to see what really happens.

Also, I think I read once the under READ COMMITTED the row / page locks taken to ensure the row does not have an uncomitted change are held only as long as it takes to read the data. Rather then been held for the entire duration of the query. I’ve added this to my list of additional things to look into; what would happen if the data was spread out over 2 pages?

col1 request_mode request_status resource_type   resource_name        resource_description
---- ------------ -------------- --------------- -------------------- --------------------
a    S            GRANT          DATABASE        0                                   
a    IS           GRANT          PAGE            72057594038321152    1:143          
a    IS           GRANT          OBJECT          data                                
a    IS           GRANT          OBJECT          vwLocks                             

After the read all locks taking during the read have been released. READ COMMITTED does not guarantee that a two identical reads during a transaction will return the same value, so locks do not need to be maintained for the duration of the transaction.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

After committing the transaction we return to the initial same state.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
IS           GRANT          OBJECT          vwLocks                             

READ UNCOMMITTED ISOLATION

Before the read only a Schema Stability (Sch-S) lock is taken to protect the view that is to be read. My guess here is that weakest lock that can be taken to protect the view from been dropped while the query is executing. Again more research is needed here.

 
request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
Sch-S        GRANT          OBJECT          vwLocks                             

During the read the transaction takes a Schema Stability lock on the table. Looking at the Lock Compatibility guide the lock that is incompatible with this is the Schema Modification (Sch-M) lock. Which means any existing Exclusive or Update locks held on the table by another transaction will not stop this transaction reading data from the table. This fits with the READ UNCOMMITTED mode of reading data that other transactions have yet to commit.

Also as the query does not takes locks any lower than the table level, it is effectively ignoring any other inflight transactions. Nor is it making any changes to the lock state of the database that would prevent any other transactions from reading or modifying data at the same time.

This is the not the greatest idea in the world. If a query uses multiple indexes on a table or some other mechanism that requires consistency between reads of the same table in the same Select it can experience 601 (”Could not continue scan with NOLOCK due to data movement.”) errors. These happen when say, data is read from a Non Clustered index and then a lookup is performed to obtain the rest of the row (using RID or PKID) however the row has been moved or deleted from the Heap or Clustered index before the read can complete. This scenario comes about because the READ UNCOMMITTED query does not use any locks during its query.

col1 request_mode request_status resource_type   resource_name        resource_description
---- ------------ -------------- --------------- -------------------- --------------------
a    S            GRANT          DATABASE        0                                   
a    Sch-S        GRANT          OBJECT          data                                
a    Sch-S        GRANT          OBJECT          vwLocks                             

After the read no locks are held.

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
Sch-S        GRANT          OBJECT          vwLocks                             

After the transaction is committed no locks are held

request_mode request_status resource_type   resource_name        resource_description
------------ -------------- --------------- -------------------- --------------------
S            GRANT          DATABASE        0                                   
Sch-S        GRANT          OBJECT          vwLocks                             

Hopefully this post is just the start of several where I drill down into the locking strategies used.

Apologies for the poor page rendering

Yesterday I noticed that a number of pages were rendering rather badly. The problem stems from modifying the blog to allow me to write and publish in markdown using Text Mate.

I thought the plugin would convert the markdown to html when it was published, it actually renders the html from markdown when the page is requested. So it was taking html content and transforming it to html as if it was markdown.

Again sorry for the unreadable pages, they should all be fixed now.

Lightning Speed

Lightening Speed

And there I was prepared to accept an inferior, thunder class, analysis product.

Snapshot Isolation Levels and Phantom Reads

I’m trying to study for the next MS SQL Server 2005 exam and keep getting annoyed by practice questions that are wrong. The latest one to bother me was this statement in one of the answers:

“Both read committed snapshot isolation and snapshot isolation can produce phantom records.”

Wikipedia has a nice definition of the problem of Phantom Reads:

“A phantom read occurs when, in the course of a transaction, two identical queries are executed, and the collection of rows returned by the second query is different from the first.”

The practice question is wrong; under READ COMMITTED SNAPSHOT isolation phantom reads may occur but they will not occur under full SNAPSHOT isolation. The simple difference between the two modes of snapshot isolation is the starting point and duration for which they guarantee a transactionally consistent view of the data.

  • READ COMMITTED SNAPSHOT isolation presents a view of the data at the point that the statement started, this view is consistent for the duration of the statement.
  • SNAPSHOT isolation presents a view of the data from point that the transaction started that is consistent for the duration of the transaction. The transaction is not considered to have started until the first statement of the transaction starts and it then finishes with either COMMIT or ROLLBACK (or an error under XACT_ABORT I guess).

With a definition like this (that I just made up) there is no way that SNAPSHOT isolation can allow phantom reads to occur. If it did it would not be presenting a consistent view of data throughout the duration of the transaction in the way that SERIALIZABLE transaction does. With the exception of the problem of updates SNAPSHOT isolation is essentially a row version based implementation of the SERIALIZABLE level.

Examples of Phantom Reads

Below are three queries that demonstrate phantom reads occurring under READ COMMITTED SNAPSHOT and not occurring under SNAPSHOT isolation. Follow along by running query 1 to setup the initial data, then jumping between the three queries to perform the required reads and insert the phantom row.

-- query 1 - setup the db and initial data.
create database snap ;
go

use snap;
go

-- turn on both read committed snapshot and full snapshot 
-- isolation modes. 
alter database
    snap
set 
    allow_snapshot_isolation on;
alter database
    snap
set 
    read_committed_snapshot on;

create table 
    snap_test
(
    id  char(1) not null
)
on [primary]

insert into snap_test values ('a');
insert into snap_test values ('b');
insert into snap_test values ('c');

-- go to query 2 and then 3 to perform the initial read

-- a quick check of the snapshot transactions and the version store

-- we have two transactions using the version store, one is full snapshot
select 
    transaction_id,
    transaction_sequence_num ,
    is_snapshot, 
    first_snapshot_sequence_num, 
    max_version_chain_traversed
from 
    sys.dm_tran_active_snapshot_database_transactions

/*
transaction_id       transaction_sequence_num is_snapshot first_snapshot_sequence_num max_version_chain_traversed
-------------------- ------------------------ ----------- --------------------------- ---------------------------
391601               274                      1           0                           0
391612               275                      0           0                           0
*/

-- the version store is empty
select 
    * 
from 
    sys.dm_tran_version_store

-- insert the phantom row
insert into
    snap_test
values 
    ('z');

-- go to query 2 and then 3 to check for phantom rows

-- back from checking for phantom rows

-- the two transactions have not needed to visit the version store
select 
    transaction_id,
    transaction_sequence_num ,
    is_snapshot, 
    first_snapshot_sequence_num, 
    max_version_chain_traversed
from 
    sys.dm_tran_active_snapshot_database_transactions
    
/*
transaction_id       transaction_sequence_num is_snapshot first_snapshot_sequence_num max_version_chain_traversed
-------------------- ------------------------ ----------- --------------------------- ---------------------------
391601               274                      1           0                           0
391612               275                      0           0                           0
*/

-- as the version store is still empty

select 
    * 
from 
    sys.dm_tran_version_store
-- query 2 - snapshot isolation
use snap;

set transaction isolation level snapshot;

begin transaction

-- initial read
select * from snap_test

/*
id
----
a
b
c

(3 row(s) affected)
*/

-- go to query 3 and perform the initial read committed read

-- after the phantom row has been inserted, perform the read again
select * from snap_test

-- same result, no phantom records have appeared in the result set.
/*
id
----
a
b
c

(3 row(s) affected)
*/

-- query 3 - read commited snapshot isolation
use snap;

set transaction isolation level read committed;

begin transaction

-- initial read
select * from snap_test

/*
id
----
a
b
c

(3 row(s) affected)
*/

-- back to query 1

-- after the phantom row has been inserted, read again

select * from snap_test

-- phantom row (-1) is now in the result set.
-- read committed snapshot only gurantees consistency for the duration of the statement
-- not the transaction.

/*
id
----
a
b
c
z

(4 row(s) affected)
*/

-- go back to query 1

Under the SNAPSHOT level the ‘z’ row was not returned, just what I thought would happen. But then I started wondering, how does SQL Server know not to return the ‘z’ row.

Pseudo Range Locks under SNAPSHOT isolation

Under SERIALIZATION isolation a transaction is protected from phantom reads by taking Key-Range Locks that prevent another transaction from inserting (or updating or deleting) a row that would have a key value in the locked range. Clearly this approach is not taken under SNAPSHOT isolation as a second transaction can insert rows that qualify for the first transactions read; the first transaction just ignores them.

Thinking about it: ignoring a record because it would result in a phantom read is not that different to ignoring a record because it was modified after the transaction or statement started. In this case the query will examine the version store for the record and return the row at the appropriate version. The appropriate version for a record is determined by using the new Transaction Sequence Number (XSN) associated with each transaction and added to each record modified after either of the snapshot modes have been enabled for the database.

So my theory is that a SNAPSHOT isolation transaction uses the same mechanism to ignore records in the table that were created after the transaction started.

After snapshots have been enabled each record that is modified has 14 bytes appended to it to maintain row versioning. The first 8 bytes are used to describe the file, page and row in the tempdb that contains the previous version and the next 6 bytes are used to store the XSN of the transaction that created/modified this version. The previous record in the version store will contain the same information; its version number and a pointer to the previous version if there is one. So a query can walk this chain of versions until it finds the latest version that was created before the current transaction or statement started.

The XSN for the two transactions started in my example above were shown in the transaction_sequence_num column when querying the sys.dm_tran_active_snapshot_database_transactions dmv. To view the XSN associated with the records I’ve used the technique described in Inside Microsoft SQL Server 2005: The Storage Engine to view the contents of a data page. (The DBCC calls used here are unsupported and super secret so are not listed in the Books Online.)

-- first call to list the pages in the table heap.
-- pages of type 1 are data pages, there should be a single page of this 
-- type. The PagePID column is the page number we need below, 73 in my case.
dbcc ind (snap, snap_test, -1)
-- this trace flag is needed to use the DBCC PAGE method below
dbcc traceon(3604)
-- view the contents of the single data page, 
-- params are db_name, file_number, page_number and print_options (0 to 3)
dbcc page (snap, 1,73,3)

/*
-- some results omitted

Slot 0 Offset 0x60 Length 22
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
Memory Dump @0x4D5DC060

00000000:   50000500 610100fe 00000000 00000000 †P...a...........         
00000010:   0f010000 0000††††††††††††††††††††††††......                   

Version Information = 
    Transaction Timestamp: 271
    Version Pointer: Null

Slot 0 Column 0 Offset 0x4 Length 1
id = a     

Slot 1 Offset 0x76 Length 22
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
Memory Dump @0x4D5DC076

00000000:   50000500 620100fe 00000000 00000000 †P...b...........         
00000010:   10010000 0000††††††††††††††††††††††††......                   

Version Information = 
    Transaction Timestamp: 272
    Version Pointer: Null

Slot 1 Column 0 Offset 0x4 Length 1
id = b                               

Slot 2 Offset 0x8c Length 22
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
Memory Dump @0x4D5DC08C

00000000:   50000500 630100fe 00000000 00000000 †P...c...........         
00000010:   11010000 0000††††††††††††††††††††††††......                   

Version Information = 
    Transaction Timestamp: 273
    Version Pointer: Null

Slot 2 Column 0 Offset 0x4 Length 1
id = c                               

Slot 3 Offset 0xa2 Length 22
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
Memory Dump @0x4D5DC0A2

00000000:   50000500 7a0100fe 00000000 00000000 †P...z...........         
00000010:   15010000 0000††††††††††††††††††††††††......                   

Version Information = 
    Transaction Timestamp: 277
    Version Pointer: Null

Slot 3 Column 0 Offset 0x4 Length 1
id = z                   
*/

My SNAPSHOT isolation transaction above had a XSN of 274. Looking at the first row in the data page at slot 0, the output from DBCC says this record has a Transaction Timestamp of 271. There for the record was created or modified before my transaction was started and should be included in the result set. The same can be said for the next two records at slots 1 and 2, which were created by transactions 272 and 273. As a side, this is a nice demonstration of implicit system transactions been created for each of the three INSERT statements used to populate the initial data.

When we get to fourth row at slot 3 its easy to see a mechanism for the SNAPSHOT transaction to ignore it; its XSN is 277 which is greater than the 274 assigned to the transaction. In this way the system can provide keyset stability for a SNAPSHOT transaction without requiring locks to prevent other transactions from going about their business.

The DAC and SQL Browser

In a previous post I spent some time looking at which ports are used by SQL Server 2005 including what port the Dedicated Administrator Connection uses. At the time it looked like it was possible to connect to the DAC for a server without the SQL Browser service running.

Since then as I’ve been studying for Microsoft exams I’ve answered practice and exam questions about not been able to connect to the DAC on a server. The answerers usually revolve around remote DAC connections been disabled by default; or the SQL Browser service no running which is contrary to what I discovered in my previous post on Port Madness.

With remote DAC connections enabled on a server with as single default instance, I was able to connect to the DAC from another machine using SSMS with the SQL Browser service disabled. As noted in the previous post the connection appeared to be established on TCP/IP 1434.

According to the documentation when Using a Dedicated Administrator Connection the SQL Browser service is only needed sometimes.

“The DAC port is assigned dynamically by SQL Server during startup. When connecting to the default instance, the DAC avoids using a SQL Server Resolution Protocol (SSRP) request to the SQL Server Browser Service when connecting. It first connects over TCP port 1434. If that fails, it makes an SSRP call to get the port. If SQL Server Browser is not listening for SSRP requests, the connection request returns an error. Refer to the error log to find the port number DAC is listening on.”

So in a default installation the Browser should not be needed for either local or remote DAC connections. However if TCP 1434 has already been claimed when the server starts the Browser service can be of assistance, otherwise the port must be specified when connection to the DAC.

For a named instance again the connection can be made without the Browser service but the port number must be known, and it will most likely not be TCP 1433. Otherwise the Browser service must be running to allow the client to discover the port number.

The questions make sense from a certain perspective; if you cannot connect to the DAC then make sure the SQL Browser service is running because it should take care of finding the DAC port. If its still not working, look in the ERRORLOG for the SQL server instance to discover the port the DAC endpoint was bound to at start up and try connecting directly.