Last, but not least, here’s one final look at parameter sniffing.
In part 1 of this mini-series I wrote about data skew and the problems it can cause with parameters. In part 2, I looked at what happens when the optimiser can’t sniff values. In this post, I’m going to look at a third thing that can cause parameter sniffing problems. It’s fairly obvious once you know about it, but the first time it was pointed out to me I was a little surprised.
So, to recap. When the query optimiser gets a stored procedure to compile, it knows the values of any parameter passed to that procedure. It will then compile the procedure and optimise the queries based upon the value of those parameters. The optimiser cannot sniff the values of variables, because the values of the variables have not been set at the time that the procedure is optimised.
I’m going to use the same sample code and data as in the first article, to keep things consistent.
From the tests that were done before, I know that the query
select * from largetable where somestring = 'zzz'
executes optimally with an index seek and returns 9 rows. Likewise, I know that the query
select * from largetable where somestring = 'abc'
executes optimally with a clustered index scan and returns 1667 rows.
Now, let’s see if I can get the optimiser to make the wrong choice.
CREATE PROCEDURE TestSniffing3 @StringVar VARCHAR(10) AS IF @StringVar not like 'a%' SET @StringVar = 'abc' SELECT * from largetable where somestring = @StringVar GO
Looks simple enough. It the parameter isn’t within a certain range, set it to some default value. This kind of stored proc construction isn’t unusual. I often see it in search procs. Pass in a NULL if you want all rows to match. Let’s see what the optimiser makes of it.
EXEC TestSniffing3 'zzz'
It returns 1667 rows, as expected. Now take a look at the execution plan. It’s doing an index seek. Not what was expected and not optimal. On my machine the key lookup’s in at 87% of the query.
So, what went wrong?
A look at the properties of the index seek give a clue. Estimated rows – 9 actual rows – 1667. The xml form of the execution plan give another clue.
<ColumnReference Column="@StringVar" ParameterCompiledValue="'zzz'" ParameterRuntimeValue="'abc'" />
When this procedure was compiled, the value of the parameter was ‘zzz’. The optimiser compiled a plan optimal for that value. It didn’t and couldn’t know that the parameter was going to change within the stored procedure before the query was executed.
This is something that can really bite badly. There often doesn’t appear to be a good reason for the plan to be wrong. Most people don’t go reading the raw XML of the execution plans, for good reason. It esspecially needs to be ketp in mind on procs that have multiple optional parameters, like the following
create procedure DoSearch @var1 varchar(10) = NULL, @var2 varchar(10) = NULL AS IF @var1 is null @var1 = '%' IF @var2 is null @var2 = '%' SELECT * FROM SomeBigTable WHERE Col1 like @var1 AND Col2 LIKE @var2 GO
If, when the proc is first compiled, either of the parameters is NULL, SQL will compile a plan optimal for no rows because a LIKE NULL will not match anything. If the query then goes on to match the entire table, the plan is not going to be very optimal at all.
That I think concludes the short series on parameter sniffing. As always, I’m interested in hearing other people’s take on these problems and their favourite solutions.