SQL - Locking and Concurrency


Two Simultaneous Transactions in Snapshot Isolation That Cannot Be Run Serially 



USE pubs  
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
DECLARE @price money 
BEGIN TRAN


SELECT @price = price  
FROM titles  
WHERE title_id = 'BU1032'


UPDATE titles  
SET price = @price 
WHERE title_id = 'PS7777'


COMMIT TRAN


USE pubs 
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
DECLARE @price money 
BEGIN TRAN


SELECT @price = price  
FROM titles  
WHERE title_id = 'PS7777'


UPDATE titles  
SET price = @price 
WHERE title_id = 'BU1032'


COMMIT TRAN




-- We can see those locks by first running this code:


USE Adventureworks2008; 
BEGIN TRAN 
UPDATE  Sales.SalesOrderHeader 
SET ShipDate = ShipDate + 1 
WHERE SalesOrderID = 43666;




-- I can look at those locks using the sys.dm_tran_locks view:


SELECT resource_type, resource_description,  
        resource_associated_entity_id, request_mode, request_status 
FROM sys.dm_tran_locks 
WHERE resource_associated_entity_id > 0




-- We can verify what table it is by using the following query:


SELECT object_name(722101613)




-- For example, we can issue the following query within an explicit transaction in 
-- the AdventureWorks2008 database:


BEGIN TRAN 
SELECT * FROM Sales.SalesOrderHeader 
WHERE CustomerID BETWEEN 100 and 110;




-- Assume that we are in TRANSACTION ISOLATION LEVEL SERIALIZABLE and we issue this 
-- SELECT statement:


SELECT * FROM Person.Person 
WHERE LastName BETWEEN 'Freller' AND 'Freund';




-- I will use this query in many examples later in this chapter, so I’ll create a VIEW based 
-- on the SELECT and call it DBlocks: 


CREATE VIEW DBlocks AS 
SELECT request_session_id as spid,  
    db_name(resource_database_id) as dbname,  
    CASE  
   WHEN resource_type = 'OBJECT' THEN  
         object_name(resource_associated_entity_id) 
      WHEN resource_associated_entity_id = 0 THEN 'n/a' 
   ELSE object_name(p.object_id)  
    END as entity_name, index_id, 
       resource_type as resource,  
       resource_description as description,  
       request_mode as mode, request_status as status 
FROM sys.dm_tran_locks t LEFT JOIN sys.partitions p 
   ON p.hobt_id = t.resource_associated_entity_id 
WHERE resource_database_id = db_id();




-- Example 1: SELECT with Default Isolation Level


USE Adventureworks2008; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 
BEGIN TRAN 
SELECT * FROM Production.Product 
WHERE Name = 'Reflector'; 
SELECT * FROM DBlocks WHERE spid = @@spid; 
COMMIT TRAN




-- Example 2: SELECT with Repeatable Read Isolation Level


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; 
BEGIN TRAN 
SELECT * FROM Production.Product 
WHERE Name LIKE 'Racing Socks%'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'Product'; 
COMMIT TRAN




-- Example 3: SELECT with Serializable Isolation Level


USE AdventureWorks2008 ; 
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
BEGIN TRAN 
SELECT * FROM Production.Product 
WHERE Name LIKE 'Racing Socks%'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'Product'; 
COMMIT TRAN




-- Example 4: Update Operations


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 
BEGIN TRAN 
UPDATE Production.Product 
SET ListPrice = ListPrice * 0.6 
WHERE Name LIKE 'Racing Socks%'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'Product'; 
COMMIT TRAN




-- Example 5: Update with Serializable Isolation Level Using an Index


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
BEGIN TRAN 
UPDATE Production.Product 
SET ListPrice = ListPrice * 0.6 
WHERE Name LIKE 'Racing Socks%'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'Product'; 
COMMIT TRAN




-- Example 6: Update with Serializable Isolation Not Using an Index


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
BEGIN TRAN 
UPDATE Production.Product 
SET ListPrice = ListPrice * 0.6 
WHERE Color = 'White'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'Product'; 
COMMIT TRAN




-- Example 7: Creating a Table


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 
BEGIN TRAN 
SELECT *  
INTO newProducts 
FROM Production.Product 
WHERE ListPrice between 1 and 10; 
SELECT * FROM DBlocks  
WHERE spid = @@spid; 
COMMIT TRAN




-- Example 8: Row Locks


USE AdventureWorks2008; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED 
BEGIN TRAN 
UPDATE newProducts 
SET ListPrice = 5.99 
WHERE name = 'Road Bottle Cage'; 
SELECT * FROM DBlocks  
WHERE spid = @@spid 
AND entity_name = 'newProducts'; 
COMMIT TRAN




-- I’ll then look at the rsc_bin column in syslockinfo for key locks, page locks, and 
-- table locks.


USE AdventureWorks2008 
GO 
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 
GO 
BEGIN TRAN 
SELECT * FROM Person.Person  
WHERE BusinessEntityID = 249 
GO 
SELECT rsc_bin, rsc_type  
FROM master..syslockinfo 
WHERE rsc_type IN (5,6,7) 
GO




-- I can translate this to an object name as follows:


SELECT object_name(1509580416)




-- To see which object this partition belongs to, I can query the sys.partitions view:


SELECT object_name(object_id)  
FROM sys.partitions 
WHERE partition_ID = 72057594045333504;




-- Consider the following example, which creates a clustered and nonclustered index on a 
-- tiny table, and then selects the %%lockres%% value for each row, first using the clustered 
-- index and then using the nonclustered index:


CREATE TABLE lockres (c1 int, c2 int);
GO
INSERT INTO lockres VALUES (1,10);
INSERT INTO lockres VALUES (2,20);
INSERT INTO lockres VALUES (3,30);
GO
CREATE UNIQUE CLUSTERED INDEX lockres_ci ON lockres(c1);
CREATE UNIQUE NONCLUSTERED INDEX lockres_nci ON lockres(c2);
GO
SELECT %%lockres%% AS lock_resource, * FROM lockres WITH (INDEX = lockres_ci);
SELECT %%lockres%% AS lock_resource, * FROM lockres WITH (INDEX = lockres_nci);
GO




-- I could find which row that resource corresponds to with the following query:


SELECT * FROM lockres
WHERE %%lockres%% = '(010086470766)'




-- The value returned looks just like the special value %%physloc%% that was mentioned in 
-- Chapter 5, “Tables”:


CREATE TABLE lockres_on_heap (c1 int, c2 int);
GO
INSERT INTO lockres_on_heap VALUES (1,10);
INSERT INTO lockres_on_heap VALUES (2,20);
INSERT INTO lockres_on_heap VALUES (3,30);
GO
SELECT %%lockres%% AS lock_resource, * FROM lockres_on_heap;




-- Here is an example of disabling lock escalation on the TransactionHistory table:


ALTER TABLE TransactionHistory
SET (LOCK_ESCALATION = DISABLE);




-- Execute the following batch for process A:


BEGIN TRAN 
UPDATE  Production.Product  
    SET ListPrice = ListPrice * 0.9 
WHERE ProductID  = 922;




-- 2. Open a second window, and execute this batch for process B:


BEGIN TRAN 
UPDATE  Purchasing.PurchaseOrderDetail 
    SET OrderQty = OrderQty + 200 
    WHERE ProductID  = 922 
    AND PurchaseOrderID = 499;




-- 3. Go back to the first window, and execute this UPDATE statement:


UPDATE  Purchasing.PurchaseOrderDetail 
    SET OrderQty = OrderQty - 200 
    WHERE ProductID  = 922 
    AND PurchaseOrderID = 499;




-- 4. Go back to the second window, and execute this UPDATE statement:


UPDATE  Production.Product  
    SET ListPrice = ListPrice * 1.1 
    WHERE ProductID  = 922;




-- Table 10-9 A SELECT Running in RCSI
-- Transaction 1


BEGIN TRAN 
UPDATE Production.Product 
SET ListPrice = 10.00 
WHERE ProductID  = 922; 


COMMIT TRAN




-- Transaction 2


BEGIN TRAN


SELECT ListPrice 
FROM Production.Product 
WHERE ProductID  = 922; 
-- SQL Server returns 8.89 


SELECT ListPrice 
FROM Production.Product 
WHERE ProductID  = 922; 
-- SQL Server returns 10.00


COMMIT TRAN




-- RCSI is enabled and disabled with the ALTER DATABASE command, as shown in this command 
-- to enable RCSI in the AdventureWorks2008 database:


ALTER DATABASE AdventureWorks2008 
   SET READ_COMMITTED_SNAPSHOT ON




-- The blocking can be avoided by specifying a TERMINATION clause for the ALTER command, 
-- as discussed in Chapter 3.


ALTER DATABASE AdventureWorks2008 
   SET READ_COMMITTED_SNAPSHOT ON WITH NO_WAIT




-- For a session-level option to take effect, you must also allow the database to use SI 
-- by altering the database:


ALTER DATABASE AdventureWorks2008 
   SET ALLOW_SNAPSHOT_ISOLATION ON;




-- Table 10-10 A SELECT Running in a SNAPSHOT Transaction
-- Transaction 1


BEGIN TRAN


UPDATE Production.Product 
SET ListPrice = 12.00 
WHERE ProductID = 922;


COMMIT TRAN




-- Transaction 2


SET TRANSACTION ISOLATION 
LEVEL SNAPSHOT


BEGIN TRAN


SELECT ListPrice 
FROM Production.Product 
WHERE ProductID = 922; 
-- SQL Server returns 10.00 
-- This is the beginning of 
-- the transaction


SELECT ListPrice 
FROM Production.Product 
WHERE ProductID = 922; 
-- SQL Server returns 10.00 
-- Return the committed 
-- value as of the beginning 
-- of the transaction


COMMIT TRAN 


SELECT ListPrice 
FROM Production.Product 
WHERE ProductID = 922; 
-- SQL Server returns 12.00




-- You can see the values of each of these snapshot states for all your databases with 
-- the following query:


SELECT name, snapshot_isolation_state_desc, 
         is_read_committed_snapshot_on , * 
FROM sys.databases;




-- Table 10-13 An Update Conflict in SNAPSHOT Isolation
-- Transaction 1


BEGIN TRAN 
UPDATE Production.ProductInventory 
SET Quantity=Quantity + 200 
WHERE ProductID  = 872;  
-- Quantity is now 524


COMMIT TRAN




-- Transaction 2


SET TRANSACTION ISOLATION  
LEVEL SNAPSHOT


BEGIN TRAN


SELECT Quantity 
FROM Production.ProductInventory 
WHERE ProductID  = 872; 
-- SQL Server returns 324 
-- This is the beginning of 
-- the transaction


UPDATE Production.ProductInventory 
SET Quantity=Quantity + 300 
WHERE ProductID = 872;  
-- Process will block


--  Process receives error 3960




-- In Table 10-13, Transaction 2 could use UPDLOCK on its initial SELECT as follows:


SELECT Quantity 
FROM Production.ProductInventory WITH (UPDLOCK) 
WHERE ProductID  = 872;




-- Table 10-14 DDL Inside a SNAPSHOT Transaction
-- Transaction 1


SET TRANSACTION ISOLATION  
LEVEL SNAPSHOT


BEGIN TRAN


SELECT count(*)  
FROM Production.Product 
-- This is the beginning of 
-- the transaction 


CREATE TABLE NewProducts 
( <column definitions>) 
-- This DDL is legal


CREATE INDEX PriceIndex 
   ON Production.Product 
     (ListPrice) 
-- This DDL will generate an  
-- error




-- Transaction 2


BEGIN TRAN 


INSERT Production.Product 
  VALUES (9999, .....) 

-- A new row is insert into 
--   the Product table


COMMIT TRAN




-- Table 10-15 Concurrent DDL Outside the SNAPSHOT Transaction 
-- Transaction 1


SET TRANSACTION ISOLATION  
LEVEL SNAPSHOT


BEGIN TRAN 


SELECT TOP 10 * 
FROM Production.Product; 
-- This is the start of  
-- the transaction


SELECT TOP 10 * 
FROM Production.Product; 
-- Succeeds 
-- The ALTER to a different 
--  table does not affect  
--  this transaction


SELECT TOP 10 * FROM Production.Product; 
-- ERROR 




-- Transaction 2


BEGIN TRAN 
ALTER TABLE Purchasing.Vendor 
   ADD notes varchar(1000); 
COMMIT TRAN


BEGIN TRAN 
ALTER TABLE Production.Product 
   ADD LowestPrice money; 
COMMIT TRAN




-- You might need to close any active transactions currently using AdventureWorks2008: 


USE AdventureWorks2008 
SELECT * INTO NewProduct 
FROM Production.Product; 
GO 
ALTER DATABASE ADVENTUREWORKS2008 SET ALLOW_SNAPSHOT_ISOLATION ON; 
GO 
SELECT name, snapshot_isolation_state_desc,  
      is_read_committed_snapshot_on  
FROM sys.databases  
WHERE name= AdventureWorks2008; 
GO 
SELECT COUNT(*) FROM sys.dm_tran_version_store;
GO




-- So now run this UPDATE statement on the NewProduct table, and look at the version store again:


UPDATE  NewProduct 
SET ListPrice = ListPrice * 1.1; 
GO 
SELECT COUNT(*) FROM sys.dm_tran_version_store; 
GO




-- The output shows only one of the rows inserted:


CREATE TABLE T1 (T1ID char(1), T1name char(10)) 
GO 
INSERT T1 SELECT 'A', 'aaaaaaaaaa' 
INSERT T1 SELECT 'B', 'bbbbbbbbbb' 
GO 
DBCC IND (AdventureWorks2008, 'T1',-1) -- page 6709 
DBCC TRACEON (3604) 
DBCC PAGE('AdventureWorks2008', 1, 6709, 1) 




-- If I update one of these rows, the previous row is written to the version store and 
-- the XSN is reflected in the row versioning information:


UPDATE T1 SET T1name = '2222222222' where T1ID = 'A'; 
GO 
DBCC PAGE('AdventureWorks2008', 1, 6709, 1) 
GO 




-- Here’s an example of a ghost record under versioning:


DELETE T1 WHERE T1ID = 'B' 
DBCC PAGE('AdventureWorks2008 ', 1, 6709, 1) 
GO 




-- I’ll use the AdventureWorks2008 database, which has ALLOW_SNAPSHOT_ISOLATION set to ON, 
-- and I’ll create a simple table:


CREATE TABLE t1 
(col1 int primary key, col2 int); 
GO 
INSERT INTO t1 SELECT 1,10; 
INSERT INTO t1 SELECT 2,20; 
INSERT INTO t1 SELECT 3,30;




-- Change the session’s isolation level, start a snapshot transaction, and examine some of the metadata: 


SET TRANSACTION ISOLATION LEVEL SNAPSHOT 
GO 
BEGIN TRAN 
SELECT * FROM t1; 
GO 
select * from sys.dm_tran_current_transaction;  
select * from sys.dm_tran_version_store; 
select * from sys.dm_tran_transactions_snapshot;




-- In another connection (Connection 2), run an update and examine some of the metadata for 
-- the current transaction:


BEGIN TRAN 
 UPDATE T1 SET col2 = 100 
   WHERE col1 = 1; 
SELECT * FROM sys.dm_tran_current_transaction;




-- It is almost identical to the first, but there is an important difference in the metadata results:


SET TRANSACTION ISOLATION LEVEL SNAPSHOT 
GO 
BEGIN TRAN 
SELECT * FROM t1; 
GO 
select * from sys.dm_tran_current_transaction;  
select * from sys.dm_tran_transactions_snapshot;




-- Now let’s go back to the first SELECT transaction in Connection 1 and rerun the SELECT statement, 
-- staying in the same transaction:


SELECT * FROM t1;




-- We can examine the sys.dm_tran_active_ snapshot_database_transactions view with this query:


SELECT transaction_sequence_num, commit_sequence_num,  
   is_snapshot, session_id,first_snapshot_sequence_num, 
   max_version_chain_traversed, elapsed_time_seconds 
FROM  sys.dm_tran_active_snapshot_database_transactions;




-- You could actually run the preceding query from any connection; it shows ALL active snapshot 
-- transactions in the SQL Server instance, and because it includes the session_id, you can join it 
-- to sys.dm_exec_sessions to get information about the connection that is running the transaction:


SELECT transaction_sequence_num, commit_sequence_num,  
    is_snapshot, t.session_id,first_snapshot_sequence_num, 
    max_version_chain_traversed, elapsed_time_seconds,  
    host_name, login_name, transaction_isolation_level 
FROM  sys.dm_tran_active_snapshot_database_transactions t 
   JOIN sys.dm_exec_sessions s 
     ON t.session_id = s.session_id;




-- Even though the BEGIN TRAN and COMMIT TRAN are not necessary for a single statement transaction, 
-- I am including them to make it clear that this transaction is complete.


BEGIN TRAN 
 UPDATE T1 SET col2 = 300 
   WHERE col1 = 1 
COMMIT TRAN;




-- Examine the version store if desired, to see rows being added.


SELECT * 
 FROM sys.dm_tran_version_store;




-- This example sets the LOCK_TIMEOUT value to 5 seconds and then retrieves that value for display:


SET LOCK_TIMEOUT 5000; 
SELECT @@LOCK_TIMEOUT;




-- 1. In a new query window, execute the following batch to lock one row in the HumanResources.Department 
-- table:


USE AdventureWorks2008; 
BEGIN TRAN; 
UPDATE HumanResources.Department 
SET ModifiedDate = getdate() 
WHERE DepartmentID = 1;




-- 2. Open a second connection, and execute the following statements:


USE AdventureWorks2008; 
SET LOCK_TIMEOUT 0; 
SELECT * FROM HumanResources.Department; 
SELECT * FROM Sales.SalesPerson;




-- 3. Open a third connection, and execute the following statements:


USE AdventureWorks2008 ; 
SELECT * FROM HumanResources.Department (READPAST); 
SELECT * FROM Sales.SalesPerson;




-- 4. Open a fourth connection, and execute the following statements:


USE AdventureWorks2008 ; 
SELECT * FROM HumanResources.Department (READUNCOMMITTED); 
SELECT * FROM Sales.SalesPerson;