We were seeing some unexpected locking in some of our SQL statements and couldn’t find any documentation explaining the behavior. After some back-and-forth with someone on Microsoft’s support team, we got an explanation from a member of the SQL Server team.
In short, we were seeing key-range locks on cascade updates and deletes despite the fact that the isolation level for our transactions was set to read committed – not serializable, which is when you would expect to see key-range locks. For example, consider the following:
CREATE TABLE Foo (
FooId INT NOT NULL PRIMARY KEY
)
CREATE TABLE Bar (
FooId INT NOT NULL,
BarId INT NOT NULL
)
ALTER TABLE Bar
ADD CONSTRAINT PK_Bar
PRIMARY KEY (FooId, BarId)
ALTER TABLE Bar
ADD CONSTRAINT FK_Bar_Foo
FOREIGN KEY (FooId) REFERENCES Foo(FooId)
ON DELETE CASCADE
INSERT INTO Foo VALUES (1)
INSERT INTO Bar VALUES (1, 1)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
DELETE FROM Foo WHERE FooId = 1
COMMIT TRAN
When the delete from the Foo table occurs and triggers the cascade delete from the Bar table, key-range locking occurs on the PK_Bar primary key, even when the delete statement is within a transaction having an isolation level explicitly set to read committed.
The reason the key-range locks occur is because SQL Server implicitly escalates some of the locks in the cascade delete to key-range locks in order to block any other operation that might violate the foreign key property – for instance, it prevents another operation from inserting a foreign key value while the delete is occurring. Unlike typical key-range locks, these locks are only held for the duration of the statement, not until the completion of the entire transaction.