Secret Squirrel
When you modify a table with multiple indexes, SQL Server may choose either a narrow plan, if it doesn’t think all that many rows are going to change, or a wide plan if it thinks many will.
In narrow plans, the work SQL Server has to do to modify many indexes is hidden from you. However, these plan choices are prone to the same issues with estimates that any other plan choices are. During a conversation about when temp tables or table variables are appropriate, it came up that table variables are better for modification queries, because not all the indexes had to be updated at once.
When we looked at the plan together, we all had a good laugh and no one wept into their lumbar supports.
Pantsburner
I created some nonclustered indexes on the Posts table that all had the Score column in them, somewhere. Without them, there wouldn’t be much of a story.
When we use this query to update…
BEGIN TRAN DECLARE @bad_idea TABLE (id INT NOT NULL) INSERT @bad_idea ( id ) SELECT TOP 1 u.Id FROM dbo.Users AS u ORDER BY u.Reputation DESC UPDATE p SET p.Score += 1000 FROM dbo.Posts AS p JOIN @bad_idea AS bi ON bi.id = p.OwnerUserId --ROLLBACK
We get this plan…
If you’re playing along at home, the single row estimate that comes out of the Hash Match persists along the plan path right up to the Clustered Index Update.
Since a one row modification likely won’t qualify for a per-index update, all of the updated objects are stashed away behind the Clustered Index Update.
It’s a cover up, Scully. This one goes all the way up the plan tree.
Crackdown
Swapping our table variable out, and running this query…
BEGIN TRAN CREATE TABLE #better_idea (id INT NOT NULL) INSERT #better_idea ( id ) SELECT TOP 1 u.Id FROM dbo.Users AS u ORDER BY u.Reputation DESC UPDATE p SET p.Score += 1000 FROM dbo.Posts AS p JOIN #better_idea AS bi ON bi.id = p.OwnerUserId --ROLLBACK
We get a much more honest plan…
The estimates are accurate, so the optimizer chooses the wide plan.
I can see why this would scare some people, and they’d want to use the table variable.
The thing is, they both have to do the same amount of work.
Warnings
If you use our First Responder Kit, you may see warnings from sp_BlitzCache about plans that modify > 5 indexes. In sp_BlitzIndex, we warn about this in a bunch of different ways. Aggressive locking, unused indexes, indexes with a poor read to write ratio, tables with > 7 indexes, etc.
You can validate locking issues by sp_BlitzFirst and looking at your wait stats. If you see lots of LCK_ waits piling up, you’ve got some work to do, and I don’t mean adding NOLOCK to all your queries.