+44 (0)20 3051 3595 | info@coeo.com | Client portal login

Exploring SQL Server query plan SET options

Raul Gonzalez

Every time SQL Server is requested to execute a query, it needs to go through different phases to complete that request.

During one of those phases the query optimiser generates an execution plan which will determine how the different objects that participate in that request will be used, in which order and how they will interact to satisfy the user's request.

This process is usually very CPU intensive because as the number of objects involved increases, the different possible ways to get the job done grow exponentially and figuring out which one would be not just be best, only good enough might be complicated.

So in order to avoid having to repeat that costly process again and again for maybe the same queries, SQL Server will store them in the plan cache, which lives in memory.

The plan cache will keep these plans in memory to reuse them when required in a way that the more frequently they are used, the longer they would stay in memory.

Background 

Working on a client's system, I was troubleshooting some performance issues on a specific stored procedure. When I queried the plan cache to see what the plan was for this procedure, I found not one but two plans there. 

Why SQL Server would need two different plans for the same stored procedure? Let me show you.

In this example I'm going to use the sample database [AdventureWorks2014], but you can use what you have available if you prefer. 

Setting up the playground

We are going to execute one stored procedure and see how SQL Server will store the plan in the plan cache 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
USE [AdventureWorks2014]
GO
 
DECLARE @StartProductID int = 750
DECLARE @CheckDate datetime = '2010-08-05'
 
EXECUTE [dbo].[uspGetBillOfMaterials]
   @StartProductID
  ,@CheckDate
GO
 
SELECT st.database_id
        , (st.database_id) AS database_name
        , st.
        , OBJECT_SCHEMA_NAME(st., st.database_id)
            + '.' + (st., st.database_id) AS
        , st.cached_time
        , qp.query_plan
    FROM sys.dm_exec_procedure_stats AS st
        OUTER APPLY sys.dm_exec_query_plan(st.plan_handle) AS qp
    WHERE st. = ('AdventureWorks2014.dbo.uspGetBillOfMaterials')
GO
exploring_set_options_01

 

So far so good, we can see that the procedure was compiled and cached, on the rightmost column we can see the estimated plan that lives in the plan cache.

Now I'm going to generate a new plan for the same procedure that will coexists in the plan cache with the previous one, allow me a second :)

[Something happens here]

And here I'm back, let's have another look at the plan cache.

exploring_set_options_02

If we run the previous query, you can see a second plan was generated some time after the first one.

Each plan is generated under the influence of specific ANSI settings which determine the behaviour of the SQL Server Query Optimiser.

You might have seen this behaviour when working with filtered indexes, indexed views and computed columns.

How to know which settings apply to each query plan

As I said, each plan is generated for specific ANSI settings, but which are those?

SQL Server keep track of those settings in a DMV called sys.dm_exec_plan_attributes where a row is stored per plan and attribute, we are looking at the attribute 'set_options' and the possible values are defined in Books Online.

The value is a bit value so in order to display the different settings we need to compare the value bit-wise, let me show you how.

First we'll build a temp table to store the possible values and then compare to the value stored in the column explained above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
CREATE TABLE #set_options ([option] SYSNAME, [value] INT)
INSERT INTO #set_options
    VALUES ('ANSI_PADDING', 1)
    ,('Parallel Plan', 2)
    ,('FORCEPLAN', 4)
    ,('CONCAT_NULL_YIELDS_NULL', 8)
    ,('ANSI_WARNINGS', 16)
    ,('ANSI_NULLS', 32)
    ,('QUOTED_IDENTIFIER', 64)
    ,('ANSI_NULL_DFLT_ON', 128)
    ,('ANSI_NULL_DFLT_OFF', 256)
    ,('NoBrowseTable', 512)
    ,('TriggerOneRow', 1024)
    ,('ResyncQuery', 2048)
    ,('ARITH_ABORT', 4096)
    ,('NUMERIC_ROUNDABORT', 8192)
    ,('DATEFIRST', 16384)
    ,('DATEFORMAT', 32768)
    ,('LanguageID', 65536)
    ,('UPON', 131072)
    ,('ROWCOUNT', 262144)    
GO
 
SELECT st.database_id AS
        , (st.database_id) AS database_name
        , st.
        , OBJECT_SCHEMA_NAME(st., st.database_id)
            + '.' + (st., st.database_id) AS
        , st.cached_time
        --, qp.query_plan
        , pa.value AS set_options
        , STUFF((SELECT ', ' + s.[option]
                    FROM #set_options AS s
                    WHERE CONVERT(INT, pa.value) & s.value <> 0
                    ORDER BY s.[option] ASC
                    FOR XML PATH('')), 1, 2, '') AS set_options
    FROM sys.dm_exec_procedure_stats AS st
        OUTER APPLY sys.dm_exec_query_plan(st.plan_handle) AS qp
        OUTER APPLY sys.dm_exec_plan_attributes(st.plan_handle) AS pa
    WHERE st. = ('AdventureWorks2014.dbo.uspGetBillOfMaterials')
        AND pa.attribute = 'set_options'
    ORDER BY cached_time ASC
GO

exploring_set_options_03 And you can see what the trick was the the second run, I specified SET ARITHABORT OFF and therefore the plan that existed in the plan cache couldn't be reused.

Conclusion

ANSI settings might make your troubleshooting a challenge, as you can get the best plan possible in SSMS, but your application users wouldn't just because the application use different settings.

Probably the next step is to go check if you have multiple plans for the same procedure and start asking questions and compare the performance to know how to improve it.

Thanks for reading!

 Read more from the Coeo blog

Subscribe to Email Updates

Back to top