Quantcast
Channel: Brent Ozar Unlimited®
Viewing all articles
Browse latest Browse all 3155

Inline Table Valued Functions: Parameter Snorting

$
0
0

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:

Gross

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

Chicken taco. Steak taco. Missing index.

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:

Same little plan

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

Same big plan this time

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.


Viewing all articles
Browse latest Browse all 3155