You’ve probably heard about parameter sniffing
But there’s an even more insidious menace out there: Parameter Snorting.
It goes beyond ordinary parameter sniffing, where SQL at least tried to come up with a good plan for something once upon a compile. In these cases, it just plain gives up and throws a garbage number at you. You’ve seen it happen countless times with Table Variables, Local Variables, non-SARGable queries, catch-all queries, and many more poorly thunked query patterns.
While Scalar and Multi-Statement Table Valued Functions get lot of abuse around here (and rightly so), Inline Table Valued Functions aren’t perfect either. In fact, they can snort your parameters just as hard as all the rest.
Heck, they may even huff them.
First, let’s get Jon Skeet and his impersonators
In the Stack Overflow database export, there are four people in the Users table that have a DisplayName like Jon Skeet. Note that this query is most definitely not SARGable, but it gets the job done:
SELECT u.Id, u.DisplayName, u.Reputation FROM dbo.Users AS u WHERE u.DisplayName LIKE '%Jon%Skeet%';
The results:
If we run a query like that, it turns out pretty alright. No problems here; at least none that couldn’t be solved if I could be bothered to create a covering index that starts with DisplayName. The real Jon Skeet is obvious enough. He’s the one that has a Reputation that looks like a PowerBall jackpot.
Put that query in an inline function
Let’s look at a function I use in a few demos. Awkwardly, I use it to demonstrate how much better Inline Table Valued Functions are. I never said perfect! Call my lawyer. Whatever.
CREATE FUNCTION dbo.BC_ITVF ( @uid INT ) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT COUNT_BIG(*) AS BadgeCount FROM dbo.Badges AS b WHERE b.UserId = @uid GROUP BY b.UserId; GO
Simple enough, right? Return a count from the Badges table based on UserId. There’s only one statement here, so this function goes inline – the best kind of function.
Let’s go on a date, just me and Jon Skeet. Let’s feed the function literal values because parameters are lovingly tended to, and they get their own special fancy plan.
SELECT * --Jon Skeet FROM dbo.BC_ITVF(22656) AS bi SELECT * -- His Mentor FROM dbo.BC_ITVF(4338144) AS bi
The first plan (the real Jon Skeet) has a plan that includes a stream aggregate because he has a boatload of badges.
Jon Skeet’s Mentor has, uh, two. Which is still probably more than you have, so stop snickering. He gets a slightly different plan that doesn’t include a stream aggregate.
Uh oh – that sounds like parameter sniffing
One query, two plans depending on parameters – ah, it’s our old friend, parameter sniffing. When you see that, you should also try running the query with local variables to see another potential problem:
DECLARE @Id INT = 22656 SELECT * FROM dbo.BC_ITVF(@Id) AS bi GO DECLARE @Id INT = 4338144 SELECT * FROM dbo.BC_ITVF(@Id) AS bi GO
Then our plans look like this:
We’ve been snorted. Snorted real hard. Both Skeets – the big one and the little one – are getting the local variable treatment. SQL Server’s optimizing for a relatively small number of badges, and neither plan includes the stream aggregate.
That means we can use a RECOMPILE hint to go back to the original plans with literals. We can also use unsafe dynamic SQL.
DECLARE @Id INT = 22656 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = N' SELECT * FROM dbo.BC_ITVF( ' + CONVERT(NVARCHAR(10), @Id) + ') AS bi' EXEC sp_executesql @SQL GO DECLARE @Id INT = 4338144 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = N' SELECT * FROM dbo.BC_ITVF( ' + CONVERT(NVARCHAR(10), @Id) + ') AS bi' EXEC sp_executesql @SQL GO
If we use parameterized SQL, we used the a cached plan for whichever value goes in first. This is a lot like what happens with dynamic SQL and filtered indexes.
DECLARE @Id INT = 22656 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = N' SELECT * FROM dbo.BC_ITVF(@i_Id) AS bi' EXEC sp_executesql @SQL, N'@i_Id INT', @i_Id = @Id GO DECLARE @Id INT = 4338144 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = N' SELECT * FROM dbo.BC_ITVF(@i_Id) AS bi' EXEC sp_executesql @SQL, N'@i_Id INT', @i_Id = @Id GO
Icky
While I’d much rather see you using Inline Table Valued Functions, because they are better than the alternatives somewhere in the neighborhood of 99% of the time, you should be aware of this potential performance hit.
Thanks for reading!
Psst - use coupon code BeMyValentine for 50% off our online classes this week.