Understand the parameter sniffing
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN 11000 AND 30118
GO
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN 11000 AND 11002
The first query is done withe clustered index scan and the second one is done with index seek with key look up operation. These are the optimal execution plan. Let us see how it will work if these statement are converted into a procedure/parameterized query.
GO
EXEC sp_executesql N'SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID between @Start and @End;',N'@Start INT,@End INT',@Start=11000,@End=11002
GO
Surprisingly the execution plans of both statements are same! Right click on the left most of operator of the second execution plan and select properties.we can see the below information.
Plan Re-usability
ANSI_NULL_DFLT_OFF
DATEFIRST
ANSI_NULL_DFLT_ON
DATEFORMAT
ANSI_NULLS
FORCEPLAN
ANSI_PADDING
LANGUAGE
ANSI_WARNINGS
NO_BROWSETABLE
ARITHABORT
NUMERIC_ROUNDABORT
CONCAT_NULL_YIELDS_NULL
QUOTED_IDENTIFIER
The above SET options are plan-reuse-affecting because SQL Server performs "constant folding" (evaluating a constant expression at compile time to enable some optimizations) and because settings of these options affects the results of such expressions
.Let us see an example. @FromCustid INT,
@ToCustid INT ) AS
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @FromCustid AND @ToCustid
Run this procedure from one of the SSMS window
SET ARITHABORT OFF
EXEC
Get_SalesOrderHeader 11000,11002
GORun below query to find out the plan details which will return one row with usecounts value 1
SELECT OBJECT_NAME(CONVERT(INT,PvtAttr.objectid)),plan_handle, usecounts, PvtAttr.set_optionsFROM (
SELECT plan_handle, usecounts,f.value,f.attribute
FROM sys.dm_exec_cached_plans
OUTER APPLY sys.dm_exec_plan_attributes(plan_handle) f
WHERE cacheobjtype = 'Compiled Plan') AS PlanAttr
PIVOT (MAX(PlanAttr.value) FOR PlanAttr.attribute IN ("set_options" "objectid") AS PvtAttrWHERE PvtAttr.objectid = OBJECT_ID('dbo.get_SalesOrderHeader')
In another SSMS window, execute the procedure in same way and run the query to find the plan details which will again will return only one row with usecounts value 2.In one of the SSMS window change the ARITHABORT setting to ON and run the procedure.Run the query to get the plan details which will return two rows with different set_options value.ADO.NET and SSMS have different default SET options and it might cause to use two different execution plan by the application and SSMS.
Possible Solutions for Parameter Sniffing
Optimize for a specific parameter
ALTER PROCEDURE Get_SalesOrderHeader (
@FromCustid INT, @ToCustid INT ) AS
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @FromCustid AND @ToCustid OPTION (OPTIMIZE FOR (@FromCustid = 11000,@ToCustid = 11002))
Recompile for all execution
This option will force to recompile the procedure for every execution and generate optimal plan for the current parameter. It has a overhead of recompilation .If the procedure has many statements, the cost of recompilation will increase.This can be achieved in two ways.
ALTER PROCEDURE Get_SalesOrderHeader
( @FromCustid INT, @ToCustid INT )
AS
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @FromCustid AND @ToCustid OPTION (RECOMPILE)GO
ALTER PROCEDURE Get_SalesOrderHeader
( @FromCustid INT, @ToCustid INT ) WITH RECOMPILE
AS
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @FromCustid AND @ToCustid
An interesting difference between two of this approach is : WITH RECOMPILE , the plan is never put into the plan cache, whereas this happens with OPTION (RECOMPILE) and it use the plan cache in following situations.
The procedure is called with a very high frequency, and the compilation overhead hurts the system.
The query is very complex and the compilation time has a noticeable negative impact on the response time.
Local Variable or option for unknown
This can be implemented as given below:
ALTER PROCEDURE Get_SalesOrderHeader ( @FromCustid INT, @ToCustid INT ) AS
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @FromCustid AND @ToCustid OPTION (OPTIMIZE FOR UNKNOWN)
GO
ALTER PROCEDURE Get_SalesOrderHeader ( @FromCustid INT, @ToCustid INT )AS
DECLARE
DECLARE @L_FromCustid INT
DECLARE @L_ToCustid INT
SET @L_FromCustid =@FromCustidSET @L_ToCustid =@ToCustid
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID BETWEEN @L_FromCustid AND @L_ToCustid
Apart from this we can implement a decision tree procedure where it will call two separate procedure based on difference between two parameter value.For example one for fetching less than 50 customer and other one for fetching more than 50 customers.
Thank you for reading this post.If you liked this post, do like my page on Facebook : http://www.facebook.com/practicalSqlDba