The benefits and drawbacks of recompilation
How to identify the statements being recompiled
Analyzing the causes of recompilation
Methods to avoid recompilation
Benefits and Drawbacks of Recompilation
Compiling and recompiling execution plans is an expensive operation. However, as data changes over time, statistics will also change. The optimizer can find better execution and processing strategies as the data distribution and statistics get updated. The addition of new indexes and constraints on a given table may also benefit from a new execution plan.
Since the recompiles are always done at the statement level instead of at the procedure, two things happen. First, you’re likely to see a higher overall recompile count than if entire procedures were recompiled. However, this is offset by the second thing, which is reduced time and processing by only recompiling the individual statement that needs it.
The standard recompile process is modified when plan forcing is enabled from the Query Store. The recompile process will still occur, but the recompiled plan will not be used. Instead, the forced plan will be substituted. In addition, if the Query Store is forcing a query hint, then that hint will be applied when the recompile process occurs. However, if the forced plan has been marked as invalid, due to structural changes or something else, then the recompiled plan will be used. For more on the Query Store, refer back to Chapter 6.
Creating the dbo.WorkOrder procedure
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig1_HTML.png)
A screenshot represents the execution plan in the S Q L server for the d b o. work order procedure.
Execution plan with runtime metrics for the dbo.WorkOrder procedure
Creating a new index on the WorkOrder table
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig2_HTML.png)
A screenshot represents the resulting execution plan in the S Q L server for the d b o. work order procedure.
New execution plan for dbo.WorkOrder
The principal reason for a recompile comes after statistics get updated. SQL Server will then automatically mark queries that used those statistics as needing to be recompiled. Any time a statement gets marked, it will get recompiled at its next execution, and the application calling the query will wait until the recompile is complete.
Removing the new index on the WorkOrder table
Creating the procedure dbo.WorkOrderAll
I’m going to create the procedure, but I don’t want to execute it just yet. When I do execute it, because it’s returning all rows and all columns, the best way to satisfy the query will always be a Clustered Index Scan of Production.WorkOrder (we’ll talk about why in Chapter 9). Even if I was to try adding a nonclustered index, it’s not going to help the query, and it’s going to lead to a recompile.
CREATE statement for QueryAndRecompile session
Executing the procedure and creating an index
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig3_HTML.png)
A screenshot depicts the recompile cause, batch text, statement, and timestamp of the s q l statement recompile.
A useless statement recompile
I’ve highlighted the recompile event so we can see why it happened. The recompile_cause column informs us that the reason was “Schema changed.” That was me creating the nonclustered index before executing the procedure again.
In this case, the recompile served no useful purpose. The execution plan is exactly the same. Because the conditions were met for a recompile, a recompiled occurred. This means extra processing was done to remove the plan from cache, create a new plan, and then add that plan to cache, all before executing the query. This is why excessive or unnecessary recompiles can be a major performance issue. Before we proceed, run Listing 8-3 again to remove the test index.
Identifying the Statement Being Recompiled
Within a large, stored procedure, consisting of a number of individual statements, knowing exactly which statements are being recompiled could be a challenge. This is why I created the Extended Events session in Listing 8-5.
The code there is taking advantage of the various events related to batch and procedure execution, individual statements in both batches and procedures, and the recompile event, and tying it all together through Causality Tracking.
- 1.
sql_batch_starting: The start of the batch process.
- 2.
sql_statement_starting: The batch in this case consists only of the execution of the procedure, so that’s what we see here in the statement column.
- 3.
sp_statement_starting: The procedure consists of just one statement, and now it is starting to execute. However, the plan used by that statement has been marked for recompile due to the change in the schema.
- 4.
sql_statement_recompile: The recompile event occurs as described.
- 5.
sp_statement_starting: Now our stored procedure statement starts again because it was interrupted by the recompile event.
- 6.
sp_statement_completed: The stored procedure statement finishes.
- 7.
sql_statement_completed: Now the statement in the batch, started up at #2 in this list, is completed.
- 8.
sql_batch_completed: The batch itself can now complete.
You don’t have to do all of this in order to capture recompile events. You can simply capture just the event itself. In most cases, you’ll see the individual statement as part of the recompile event. However, this case illustrates an example where that’s not true. So setting up a system where we can observe the start and stop of each step allows us to better understand which individual statement is causing the recompile.
Because my system isn’t under load, the order of the timestamp values is enough to see the sequence I outlined. However, on a busy system, I would take advantage of the Causality Tracking to group and order events. All this lets me identify exactly which statement was recompiled.
Analyzing Causes of Recompilation
Schema changes: If a table, temporary table, or view referred to by a statement changes, including structure, metadata, and indexes, a recompile is necessary.
Binding changes: When a binding, like a default, of a column in a table or temporary table changes.
Statistics updates: Whether automatic or manual, when statistics used by a query get updated.
Deferred object resolution: If an object necessary for a query gets created as part of the execution of a batch, a recompile is necessary. A query can compile without an object, but then, when that object gets created, a recompile for the referencing statement(s) is required.
SET options: If the SET options of a given query change.
Sp_recompile: An explicit call to the system procedure sp_recompile will result in a recompile.
RECOMPILE hint: The use of the RECOMPILE query hint does what’s in the name.
Parameter Sensitive Plans: When a multi-plan query has one of the plans recompile or when the dispatcher plan changes.
Querying sys.dm_xe_map_values
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig4_HTML.png)
A screenshot depicts the reasons for the recompilation. The schema changed reason is highlighted at the top.
All the reasons for a plan recompile
This list grows regularly as new functionality is added to SQL Server over time. We simply don’t have room to discuss every possible cause for a recompile event. Most of them are relatively self-explanatory. However, we can walk through a few examples so that you can see how to use the Extended Events to understand recompile causes.
Deferred Object Resolution
It’s very common to have a batch that dynamically creates database objects and then uses them in subsequent statements. When such a batch is executed for the first time, the initial plan won’t contain the information about the objects being created. The processing strategy is deferred until the runtime of the query. When a DML statement referring to the objects being created is executed, the query recompiles in order to generate a new plan.
Both tables and local temporary tables can be created within a batch in order to hold intermediate result sets. The recompilation of the statement because of deferred object resolution behaves differently for a table when compared to a local temporary table.
Recompilation on a Table
Stored procedure that creates a table
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig5_HTML.png)
A screenshot depicts the recompile causes of the s q l statement recompile from two executions of the procedure. It includes deferred compile and schema changed.
Multiple recompile events from two executions of the procedure
I’ve highlighted the first recompile event with the recompile_cause listed as Deferred compile. When the procedure is executed the second time, another recompile occurs. When we drop the table at the end of the batch within the stored procedure, the execution plan that was recompiled for that table doesn’t get dropped. It’s still there and can be referenced the next time the query executes. However, because the table gets recreated, SQL Server considers that a change to the schema. All of that is neatly laid out for us by the Extended Events.
Recompilation on a Temporary Table
Stored procedure that creates a temporary table
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig6_HTML.png)
A screenshot depicts the statement level recompile by the deferred recompile cause.
Statement-level recompile caused by deferred compilation
You can see the deferred compile explanation in the recompile_cause column. The first statement in the procedure creates a temporary table, and then the second inserts data into it. That second statement had to be deferred until the object was actually created. However, unlike the table in the example in the previous section, the temporary table being created isn’t considered a change to the schema. Therefore, no second recompile is necessary, and the execution plan is reused.
Avoiding Recompiles
Avoid interleaving DDL and DML statements.
Reduce recompilation caused by statistics changes.
Use the KEEPFIXED PLAN hint.
Disable automatic statistics maintenance on a table.
Use table variables.
Use temporary tables across multiple scopes.
Avoid changing SET options within a batch.
Avoid Interleaving DDL and DML Statements
Procedure with interleaved DDL and DML
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig7_HTML.png)
A screenshot depicts the interleaved D D L and D M L changes.
Multiple recompiles from interleaved DDL and DML
It’s good to remember every statement in a procedure gets a plan initially. However, because objects are referenced that don’t exist, additional work is necessary at every execution of this procedure.
Reduce Recompilation Caused by Statistics Changes
Use the KEEPFIXED PLAN hint.
Disable the automatic statistics maintenance on a table.
Use the KEEPFIXED PLAN Hint
Script to create a table and procedure for query hint
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig8_HTML.png)
A screenshot depicts the results of the extended events.
Results of the KEEPFIXED PLAN query hint
You can see that the procedure executed the first time and didn’t have a recompile. Then, the script in Listing 8-11 modified the data sufficiently that a recompile would absolutely be necessary under normal circumstances. However, you can see that we didn’t get a recompile on the second execution of the procedure, despite the change in data and statistics. This is due to the KEEPFIXED PLAN hint.
You do see one recompile, but that was from the work done to load the table with data, not the result of the statistics update.
Using any query hint should only come after extensive testing has proven that it is the best solution. KEEPFIXED PLAN could result in a poor execution plan being kept on the system when a better one would result in superior performance.
Another query hint that might be useful here is KEEP PLAN. This hint is specific to temporary tables. It will keep the plan in place until the 500-row threshold is met for statistics updates. It can help reduce the amount of recompiles you see when using temporary tables. It does come with the same caveats though.
Disable Automatic Statistics Maintenance on a Table
Disabling automatic statistics maintenance on one table
Now, regardless of data changes, statistics are not updated on this table. That means that no queries will be marked for recompile because of changes in data.
Once again, we are in a situation where this could prove extremely problematic. Extensive testing to validate that this won’t hurt other queries should be undertaken before implementing this. Also, if you do choose to disable the automatic statistics updates, you should plan to have a manual process to update those statistics and then deal with the recompile in a more controlled manner.
Use Table Variables
Updating a temporary table
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig9_HTML.png)
A screenshot depicts the recompile caused by the statistics change.
A recompile caused by a statistics change
Updating a table variable
In this case, there is no statistics update. I’m not going to display the Extended Events output here because there is nothing to show. No recompile occurs in this instance.
For very small data sets like this one, table variables are preferable because the lack of statistics avoids recompiles. As the data set grows, however, statistics become helpful to the optimizer, and temporary tables will perform better, despite the recompile.
Use Temporary Tables Across Multiple Scopes
You can declare a temporary table in one procedure and then use that same temporary table in a second procedure, called by the first procedure. Prior to SQL Server 2019, and outside of Azure SQL Database, this resulted in a recompile, every single time the query was called. However, new changes to the engine mean you won’t see those recompiles.
Nested procedures to avoid recompiles
Creating a procedure like dbo.OuterProc, you will receive a warning that it has a dependency on dbo.InnerProc. However, both procedures can be created, and then, when the outer procedure gets executed, you won’t see recompiles from the inner procedure.
Avoid Changing SET Options Within a Batch
ARITHABORT
CONCAT_NULL_YIELDS_NULL
QUOTED_IDENTIFIER
ANSI_NULLS
ANSI_PADDING
ANSI_WARNINGS
NUMERIC_ROUNDABORT should be set to OFF
Changing environment settings within a procedure
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig10_HTML.png)
A screenshot depicts the results. It includes name, timestamp, recompile cause, batch text, and statement.
Recompiles caused by SET option changes
The first execution of the queries results in recompiles for the statements after the SET option changes. However, you’ll note that the second execution doesn’t show any recompile. That’s because the SET options are now a part of the execution plan, so no further recompiles are necessary. However, for identical queries, there are now three plans in cache.
Worth noting, changing the SET NOCOUNT environment setting doesn’t cause recompiles.
Controlling Recompile Results
Plan forcing
Plan guides
Query hints
Hint forcing
Plan Forcing
We already talked a little about plan forcing in Chapter 6 when we talked about the Query Store. We’ll cover it some more in Chapter 14 when we talk about dealing with parameter-sensitive execution plans. However, it’s worth a little detail right here in this chapter too.
Plan forcing will not prevent a recompile from occurring. Should any of the listed criteria be met, the plan will recompile. However, plan forcing allows you to control the outcome of the recompile. Instead of an all new plan, you’ll get the plan chosen for forcing. This does assume that the plan was not rendered invalid due to code or structure changes. Otherwise, this is one way to control the results of a recompile.
Query Hints
I introduced the concept of query hints earlier in the chapter when we used KEEPFIXED PLAN to eliminate recompiles and KEEP PLAN to reduce the occurrence of recompiles for temporary tables. Many of the query hints available are directly related to forcing choices on the optimizer. We’ll address a number of different hints throughout the book. However, there’s one in particular I’d like to bring up here: OPTIMIZE FOR.
A parameter-sensitive stored procedure with a query hint
Using a query hint like I do in Listing 8-17 does require that the code gets modified. There are possible ways around that, discussed in the next two sections. However, regardless of the recompile process, the query in this procedure will always get one plan, based on the value provided to @CustomerID within the OPTIMIZE FOR hint.
Executing the CustomerList procedure with different values
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig11_HTML.png)
A screenshot represents the execution plans for query 1 and query 2 at the top and bottom.
Two identical execution plans
You can see that both execution plans are the same. However, since I have runtime metrics, you can see substantial differences that would have, under other circumstances, resulted in different execution plans. The first query shows that it’s returning 121,317 rows out of 121,317 rows expected. In short, that plan is correct for the parameter passed in. However, the second plan is returning only 289 rows. That suggests pretty strongly a different execution plan could have been used for the second parameter value.
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig12_HTML.jpg)
A screenshot of the parameter list and the customer I D.
Parameter list from the first operator
So while the query was executed with the value of 30118, the compile value was 1. This, despite the fact that the query was recompiled as it was executed.
Plan Guides
The CustomerList procedure without the query hint
Creating a plan guide
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig13_HTML.jpg)
A screenshot of the parameter list and the customer I D.
The PlanGuideName property in an execution plan
The property PlanGuideName is added to an execution plan, in the properties of the first operator, only when a plan guide is used. You can see this one right there, by name, MyGuide.
Listing 8-20 represents an object plan guide. This means the plan guide itself is associated only with the defined module, CustomerList. You can also create a plan guide just for queries, like ad hoc batches. This is known as a SQL plan guide. Just like with the object plan guide, you have to get the white space and everything just right.
An ad hoc query in need of a query hint
Forcing an index seek through a plan guide
![](https://imgdetail.ebookreading.net/2023/10/9781484288917/9781484288917__9781484288917__files__images__323849_6_En_8_Chapter__323849_6_En_8_Fig14_HTML.png)
A screenshot depicts the execution plan with a query hint through the plan guide.
The execution plan after applying a hint through a plan guide
That’s a radically different execution plan, despite compiling from a value that should result in the plan in Figure 8-11.
Removing plan guides
Using sp_create_plan_guide_from _handle
This is very similar to plan forcing but clearly involves quite a few more steps. However, it is a way to control the output of a recompile.
I want to emphasize that using plan guides can be a dangerous approach to fixing problems. As data changes over time, the plans that were working well for you may no longer be. If you do choose to implement plan guides, ensure you have a regularly scheduled reassessment of whether or not they are appropriate.
Hint Forcing
In Azure SQL Database and SQL Server 2022, you have one final option for controlling the output of a recompile: hint forcing. We covered how this works in Chapter 6.
Summary
Most of the time, recompiles are helping us. Data, structures, or code has changed, and a fresh execution plan will help enhance performance. However, as we discussed throughout the chapter, this isn’t always the case. Extended Events are a great way to understand why a given statement is being recompiled. You now have several methods to reduce the volume of recompiles you experience on your system. Finally, we walked through several ways to control the results of the recompile. With your ability to understand why recompiles are occurring, it will make it possible to avoid unnecessary recompiles.
The last several chapters were all about the internals of how SQL Server generates and manages execution plans. The next several chapters are going to talk about specific database structures that can hurt, or help, your performance. We’re going to start with the most important: indexes.