Back in October 2007 I blogged about partition-level lock escalation in SQL Server 2008 (see here) and I promised to do a follow-up once CTP-5 came out with syntax etc. So here it is.
A brief recap – lock escalation in SQL Server 2005 and before only allowed table-level lock escalation. If you have a partitioned table with queries going against different partitions, then table-level escalation is a pain because the whole table is suddenly locked and concurrent queries against distinct partitions can’t run. SQL Server 2008 gives the ability to escalate to a parttition lock, which won’t affect the queries on the other partitions.
The lock escalation policy can only be set with ALTER TABLE after a table has been created, and the policy can only be set at the table level. The syntax is
ALTER TABLE TableName SET (LOCK_ESCALATION = TABLE | AUTO | DISABLE);
The options mean:
-
TABLE – escalation will always be to the table level. This is the default.
-
AUTO – escalation will be to the partition level if the table is partitioned; otherwise it will be to the table level
-
DISABLE – escalation will be disabled. This does not guarantee that it will NEVER occur – there are some cases where it is necessary (Books Online gives the example of scanning a heap in the SERIALIZABLE isolation level)
The only way I could find to check what the escalation policy for a table is set to is to use the sys.tables catalog view:
SELECT lock_escalation_desc FROM sys.tables WHERE name = ‘TableName’;
Let’s try it out. Here’s a script that creates a database with an example table with 3 partitions. The partition ranges are negative infinity to 7999, 8000 to 15999, 16000 to positive infinity.
CREATE DATABASE LockEscalationTest;
GO
USE LockEscalationTest;
GO
— Create three partitions: -7999, 8000-15999, 16000+
CREATE PARTITION FUNCTION MyPartitionFunction (INT) AS RANGE RIGHT FOR VALUES (8000, 16000);
GO
CREATE PARTITION SCHEME MyPartitionScheme AS PARTITION MyPartitionFunction
ALL TO ([PRIMARY]);
GO
— Create a partitioned table
CREATE TABLE MyPartitionedTable (c1 INT);
GO
CREATE CLUSTERED INDEX MPT_Clust ON MyPartitionedTable (c1)
ON MyPartitionScheme (c1);
GO
— Fill the table
SET NOCOUNT ON;
GO
DECLARE @a INT = 1;
WHILE (@a < 17000)
BEGIN
INSERT INTO MyPartitionedTable VALUES (@a);
SELECT @a = @a + 1;
END;
GO
Now I’m going to explicitly set the escalation to TABLE and start a transaction that should cause lock escalation.
ALTER TABLE MyPartitionedTable SET (LOCK_ESCALATION = TABLE);
GOBEGIN TRAN
UPDATE MyPartitionedTable SET c1 = c1 WHERE c1 < 7500;
GO
We should be able to see the locks being held:
SELECT [resource_type], [resource_associated_entity_id], [request_mode],
[request_type], [request_status] FROM sys.dm_tran_locks WHERE [resource_type] <> ‘DATABASE’;
GOresource_type resource_associated_entity_id request_mode request_type request_status
————— —————————– ————– ————– —————-
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
OBJECT 2105058535 X LOCK GRANT
Just as we expected – an X table lock. Trying any query against the table fails now. Now I’ll rollback that transaction, set the escalation to partition-level and try again.
ROLLBACK TRAN;
GOALTER TABLE MyPartitionedTable SET (LOCK_ESCALATION = AUTO);
GOBEGIN TRAN
UPDATE MyPartitionedTable SET c1 = c1 WHERE c1 < 7500;
GOSELECT [partition_id], [object_id], [index_id], [partition_number]
FROM sys.partitions WHERE object_id = OBJECT_ID (‘MyPartitionedTable’);
GOSELECT [resource_type], [resource_associated_entity_id], [request_mode],
[request_type], [request_status] FROM sys.dm_tran_locks WHERE [resource_type] <> ‘DATABASE’;
GOpartition_id object_id index_id partition_number
——————– ———– ———– —————-
72057594039042048 2105058535 1 1
72057594039107584 2105058535 1 2
72057594039173120 2105058535 1 3resource_type resource_associated_entity_id request_mode request_type request_status
————— —————————– ————– ————– —————-
HOBT 72057594039042048 X LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
OBJECT 2105058535 IX LOCK GRANT
Excellent – the object lock is now IX rather than X, and the X lock is at the partition (HOBT) level for partition 1 (see the bold highlighting to match the partition ID with the lock resource). (For an explanation of HOBTs, see my post Inside The Storage Engine: IAM pages, IAM chains, and allocation units.) So now we should be able to do something with another partition – let’s see if we can cause another partition level X lock in another connection:
USE LockEscalationTest;
GOBEGIN TRAN
UPDATE MyPartitionedTable set c1 = c1 WHERE c1 > 8100 AND c1 < 15900;
GOSELECT [partition_id], [object_id], [index_id], [partition_number]
FROM sys.partitions WHERE object_id = OBJECT_ID (‘MyPartitionedTable’);
GOSELECT [resource_type], [resource_associated_entity_id], [request_mode],
[request_type], [request_status] FROM sys.dm_tran_locks WHERE [resource_type] <> ‘DATABASE’;
GOpartition_id object_id index_id partition_number
——————– ———– ———– —————-
72057594039042048 2105058535 1 1
72057594039107584 2105058535 1 2
72057594039173120 2105058535 1 3resource_type resource_associated_entity_id request_mode request_type request_status
————— —————————– ————– ————– —————-
HOBT 72057594039107584 X LOCK GRANT
HOBT 72057594039042048 X LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
METADATA 0 Sch-S LOCK GRANT
OBJECT 2105058535 IX LOCK GRANT
OBJECT 2105058535 IX LOCK GRANT
Now we have two partition X locks, for partitions 1 and 2 (as expected – use the color coding above to match up the IDs), plus two table-level IX locks (one for each connection, as expected). Very cool!
Now I’m going to force a deadlock – by having each connection try to read a row from the other locked partition:
Connection 1:
SELECT * FROM MyPartitionedTable WHERE c1 = 8500;
GOConnection 2:
SELECT * FROM MyPartitionedTable WHERE c1 = 100;
GO
Connection 2 succeeds but on connection 1 we get (as expected)
(local)\SQLDEV01(SQLHAVPC\Administrator): Msg 1205, Level 13, State 18, Line 1
Transaction (Process ID 51) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
This illustrates a potential problem with this new mechanism – applications that used to rely on the blocking nature of X table locks may now exhibit deadlocks if partition-level escalation is turned on in production without any testing, this mode was specifically chosen NOT to be the default setting for new tables because some trial workloads exhibited deadlocks during testing. Don’t just turn it on in production without testing – as with any other option or feature.
7 thoughts on “SQL Server 2008: Partition-level lock escalation details and examples”
What if row versioning is enabled? Do we still get the deadlock problem. I will test it but I doubt that there will be deadlocks.
Row versioning won’t affect lock escalation for updates – it helps a different scenario.
I noticed that you used sys.mytables instead of sys.tables in
SELECT lock_escalation_desc FROM sys.mytables WHERE name = ‘TableName’;
I’m guessing you have a personal view on tables. Just curious as to its definition.
sys.mytables is a MS-defined catalog view that just shows you table that you own.
Hello sir,
It is very useful article for beginners like me.
I created partitions on table instead index,it’s behaving as AUTO even though the lock_escalation mode for the table is TABLE.
Could you tell me the reason.
Thanks,
Ashokd
To get more information about Isolation levels in SQL Server including Locks and its impact on database in detail with examples, refer the below link:
http://www.sqllion.com/2009/07/transaction-isolation-levels-in-sql-server/
Is locking behaviour affected by the data type used in the partition function? Change the data type from INT to CHAR(5) creates IX locks on the other partitions which blocks concurrency.
changes below:
CREATE PARTITION FUNCTION MyPartitionFunction (CHAR(5)) AS RANGE RIGHT FOR VALUES (‘08000’, ‘16000’);
GO
CREATE PARTITION SCHEME MyPartitionScheme AS PARTITION MyPartitionFunction
ALL TO ([PRIMARY]);
GO
— Create a partitioned table
CREATE TABLE MyPartitionedTable (c1 CHAR(5));
GO
CREATE CLUSTERED INDEX MPT_Clust ON MyPartitionedTable (c1)
ON MyPartitionScheme (c1);
GO
— Fill the table
SET NOCOUNT ON;
GO
DECLARE @a INT = 1;
DECLARE @b CHAR(5);
WHILE (@a < 17000)
BEGIN
SET @b = RIGHT(‘00000’ + CAST(@a AS VARCHAR), 5)
INSERT INTO MyPartitionedTable VALUES (@b);
SELECT @a = @a + 1;
END;
GO
ALTER TABLE MyPartitionedTable SET (LOCK_ESCALATION = AUTO);
GO
BEGIN TRAN
UPDATE MyPartitionedTable SET c1 = c1 WHERE c1 < ‘07500’;
GO
OBJECT 178099675 IX LOCK GRANT
HOBT 72057594135904256 IX LOCK GRANT
HOBT 72057594135838720 IX LOCK GRANT
HOBT 72057594135773184 X LOCK GRANT