Edit: Paul has pointed out some significant gaps, so I will be reworking this post in the future. Locks are accurate for this particular scenario, but apparently there are other behaviors to consider.
Sometimes I hit a weird query where I wonder if, just maybe, an indexed view would help. Then a tingling sensation crawls down my spine and I run screaming in terror, because indexed views have a reputation. Ghastly deadlocks, phantom issues, and sinister edge cases haunt my DBA sensibilities, scaring me away.
But I prefer understanding – I prefer science. So I investigated and experimented, poked and prodded. Great thanks to Paul White and Erik Darling. Erik somewhere spelled out the key idea: one/many-to-many joins in the view cause serializable locks.
My setup script (full script at the bottom) creates two identical tables A and B of even numbers 2 to 10, with an indexed view AB that joins on A>B in a one-to-many relationship.
CREATE OR ALTER VIEW dbo.AB WITH SCHEMABINDING AS SELECT A.ID AS AID, COUNT_BIG(*) AS bigcount FROM dbo.A JOIN dbo.B ON B.ID < A.ID GROUP BY A.ID
A modification to a table here has three objects involved, with associated locks: the modified table, the joined table, and the view. First, the locks associated with the original modification are taken, then, the locks on the other table as the engine figures out which rows in the view are affected, and finally the rows in the view.
But that was hard to keep all in my head, so I drew it out. Here are the objects (keys only).
A is the “one” side of the one-to-many, so a modification there involves “normal” locks. But a modification to B, since it can be referenced by multiple rows of A, will involve the super special magical serializable locks.
Let’s look at an insert:
The row inserted into B has an X lock. Next, it will need to search through A to find matching rows of AB, meaning transient S locks in A.
The S locks are released after rows are read, but when the engine arrives at AB, we see range locks (and please note the order of S and Range locks can be interleaved, but I separate them for simplification).
But why range locks? Because the many-ness of the relationship means there are potentially conflicting inserts that wouldn’t be covered by just key locks.
Yikes, no wonder these produce deadlocks! Also, the engine is taking more range locks on the the view than it strictly needs to – probably to prevent complicated programming around edge cases.
Maybe it’s just how my brain works, but drawing these out is what finally made the locking strategy click for me. I don’t consider indexed view locking as mysterious anymore (yay science!), but I do find it monstrous. Be warned!
--Setup CREATE TABLE A (ID INT PRIMARY KEY, junk CHAR(10)) CREATE TABLE B (ID INT PRIMARY KEY, junk CHAR(10)) INSERT A SELECT TOP (5) (ROW_NUMBER() OVER(ORDER BY 1/0))*2, 'data' FROM sys.columns INSERT B SELECT TOP (5) (ROW_NUMBER() OVER(ORDER BY 1/0))*2, 'data' FROM sys.columns GO CREATE OR ALTER VIEW dbo.AB WITH SCHEMABINDING AS SELECT A.ID AS AID, COUNT_BIG(*) AS bigcount FROM dbo.A JOIN dbo.B ON B.ID < A.ID GROUP BY A.ID GO CREATE UNIQUE CLUSTERED INDEX CX_AIDs ON dbo.AB (AID) GO
Interesting reading, Forrest! I’m looking forward to the update now 🙂