Get Yourself Together
This is one of the most common patterns that I see in stored procedures. I’m going to simplify things a bit, but hopefully you’ll get enough to identify it when you’re looking at your own code.
Here’s the stored procedure:
CREATE OR ALTER PROCEDURE dbo.you_are_not_clever ( @cd DATETIME NULL) AS BEGIN DECLARE @id INT; SELECT TOP 1 @id = u.Id FROM dbo.Users AS u WHERE ( u.CreationDate >= @cd OR @cd IS NULL ) ORDER BY u.Reputation DESC; SELECT p.OwnerUserId, pt.Type, SUM(p.Score) AS ScoreSum FROM dbo.Posts AS p JOIN dbo.PostTypes AS pt ON p.PostTypeId = pt.Id WHERE p.OwnerUserId = @id GROUP BY p.OwnerUserId, pt.Type; END;
There’s a lot of perceived cleverness in here.
The problem is that SQL Server doesn’t think you’re very clever.
Pattern 1: Assigning The Variable
Leaving aside that optional parameters aren’t SARGable, something even stranger happens here.
And I know what you’re thinking (because you didn’t read the post I just linked to), that you can get the “right” plan by adding a recompile hint.
Let’s test that out.
DECLARE @id INT; DECLARE @cd DATETIME = '2017-01-01'; SELECT TOP 1 @id = u.Id --Assigning the id to a variable FROM dbo.Users AS u WHERE ( u.CreationDate >= @cd OR @cd IS NULL ) ORDER BY u.Reputation DESC; SELECT TOP 1 u.Id -- Not sssigning the id to a variable FROM dbo.Users AS u WHERE ( u.CreationDate >= @cd OR @cd IS NULL ) ORDER BY u.Reputation DESC OPTION ( RECOMPILE );
Here are the plans, for all you nice people NOT WORKING AT WORK.
Just like in the post I linked up there (that you still haven’t read yet — for SHAME), the first plan refuses to recognize that an index might make things better.
The second plan does, but as usual, the missing index request is rather underwhelming.
But what’s more interesting, is that even with a recompile hint, The Optimizer doesn’t ‘sniff’ the variable value in the first query that assigns to @id.
This only happens in queries that test if a variable is null, like the pattern above, and it becomes more obvious with a better index in place.
CREATE INDEX ix_helper ON dbo.Users(CreationDate, Reputation DESC)
We’ve got it all! A seek vs a scan, a bad estimate! Probably some other stuff!
Pattern 2: Using That Assigned Variable
The Great And Powerful Kendra blogged about a related item a while back. But it seems like you crazy kids either didn’t get the message, or think it doesn’t apply in stored procedures, but it does.
If we look at the plan generated, the cardinality estimate gets the density vector treatment just like Kendra described.
In my histogram, the values plug in below, and give us 79.181327288022
SELECT (38485046 * 2.057457E-06) AS lousy
This gets worse when a user with a lot of data, where other parts of the plan start to go downhill.
How To Avoid These Problems
Both of the problems in these patterns can be avoided with dynamic SQL, or sub stored procedures.
Unfortunately, IF branching doesn’t work the way you’d hope.
If you’re ever confused by bad query plans that crop up in a stored procedure, sneaky stuff like this can totally be to blame.
Thanks for reading!
Applications are open now for our 2019 scholarship program.