This post is also available at SQL Server Temporary Object Caching.
Creating a table is a relatively resource-intensive and time-consuming operation. The server must locate and allocate storage space for the new data and index structures, and make the corresponding entries in multiple system metadata tables. All this work has to be done in ways that will always work correctly under high concurrency, and which meet all of the ACID guarantees expected of a relational database.
In SQL Server, this means taking the right kinds of locks and latches, in the correct sequence, while also ensuring that detailed transaction log entries are safely committed to persistent storage in advance of any physical changes to the database. These log entries ensure the system can bring the database back to a consistent state in the event of a transaction rollback or system crash.
Dropping a table is a similarly expensive operation. Luckily, most databases do not create or drop tables with any great frequency. The obvious exception to this is the system database tempdb. This single database contains the physical storage, allocation structures, system metadata, and transaction log entries for all temporary tables and table variables across the entire SQL Server instance.
It is in the nature of temporary tables and table variables to be created and dropped much more frequently than other database object types. When this naturally high frequency of creation and destruction is combined with the concentrating effect of all temporary tables and table variables being associated with a single database, it is hardly surprising that contention can arise in the allocation and metadata structures of the tempdb database.
Temporary Object Caching
To reduce the impact on tempdb structures, SQL Server can cache temporary objects for reuse. Instead of dropping a temporary object, SQL Server retains the system metadata, and truncates the table data. If the table is 8MB or smaller, the truncation is performed synchronously; otherwise deferred drop is used. In either case, truncation reduces the storage requirement to a single (empty) data page, and the allocation information to a single IAM page.
Caching avoids almost all of the allocation and metadata costs of creating the temporary object next time around. As a side-effect of making fewer changes to the tempdb database than a full drop and recreate cycle, temporary object caching also reduces the amount of transaction logging required.
Achieving Caching
Table variables and local temporary tables are both capable of being cached. To qualify for caching, a local temporary table or table variable must be created in a module:
- Stored procedure (including a temporary stored procedure)
- Trigger
- Multi-statement table-valued function
- Scalar user-defined function
The return value of a multi-statement table-valued function is a table variable, which may itself be cached. Table-valued parameters (which are also table variables) can be cached when the parameter is sent from a client application, for example in .NET code using SqlDbType.Structured
. When the statement is parameterized, table-valued parameter structures can only be cached on SQL Server 2012 or later.
The following cannot be cached:
- Global temporary tables
- Objects created using ad-hoc SQL
- Objects created using dynamic SQL (e.g. using
EXECUTE
orsys.sp_executesql
)
To be cached, a temporary object additionally must not:
- Have named constraints (constraints without explicit names are perfectly fine)
- Perform "DDL" after object creation
- Be in a module defined using the
WITH RECOMPILE
option - Be called using the
WITH RECOMPILE
option of theEXECUTE
statement
To address some common misconceptions explicitly:
TRUNCATE TABLE
does not prevent cachingDROP TABLE
does not prevent cachingUPDATE STATISTICS
does not prevent caching- Automatic creation of statistics does not prevent caching
- Manual
CREATE STATISTICS
will prevent caching
All temporary objects in a module are assessed for caching suitability separately. A module that contains one or more temporary objects that cannot be cached may still qualify for caching of other temporary objects within the same module.
A common pattern that disables caching for temporary tables is the creation of indexes after the initial table creation statement. In most cases this can be worked around using primary key and unique constraints. In SQL Server 2014 and later, we have the option of adding non-unique nonclustered indexes directly in the table creation statement using the INDEX
clause.
Monitoring and Maintenance
We can see how many temporary objects are currently cached using the cache counters DMV:
SELECT
DOMCC.[type],
DOMCC.pages_kb,
DOMCC.pages_in_use_kb,
DOMCC.entries_count,
DOMCC.entries_in_use_count
FROM sys.dm_os_memory_cache_counters AS DOMCC
WHERE
DOMCC.[name] = N'Temporary Tables & Table Variables';
An example result is:
A cache entry is considered to be in use for as long as any part of the containing module is executing. Concurrent executions of the same module will result in multiple cached temporary objects being created. Multiple execution plans for the same module (perhaps due to differing session SET
options) will also lead to multiple cache entries for the same module.
Cache entries can be aged out over time in response to competing needs for memory. Cached temporary objects can also be removed (asynchronously, by a background system thread) when the parent module's execution plan is removed from the plan cache.
While not supported (or in any way recommended) for production systems, the temporary object cache store can be manually completely cleared for testing purposes with:
DBCC FREESYSTEMCACHE('Temporary Tables & Table Variables')
WITH MARK_IN_USE_FOR_REMOVAL;
WAITFOR DELAY '00:00:05';
The five second delay allows time for the background clean up task to run. Note that this command is actually dangerous. You should only employ it (at your own risk) on a test instance you have exclusive access to. Once you have finished testing, restart the SQL Server instance.
Caching Implementation Details
Table variables are implemented by a 'real' user table in the tempdb database (though not a table we can query directly). The name of the associated table is "#" followed by the eight digit hexadecimal representation of the object id. The following query shows the relationship:
-- A table variable
DECLARE @Z AS table (z integer NULL);
-- Corresponding sys.tables entry
SELECT
T.[name],
ObjIDFromName = CONVERT(integer, CONVERT(binary(4), RIGHT(T.[name], 8), 2)),
T.[object_id],
T.[type_desc],
T.create_date,
T.modify_date
FROM tempdb.sys.tables AS T
WHERE
T.[name] LIKE N'#[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]';
A sample result is shown below. Notice how the object id calculated from the object name matches the actual object id:
Running that script as ad-hoc SQL will produce a different tempdb object ID (and object name) on each execution (no caching). Placing the same script inside a module (e.g. a stored procedure) will allow the table variable to be cached (as long as dynamic SQL is not used), so that the object ID and name will be the same on each execution.
When the table variable is not cached, the underlying table is created and dropped each time. When temporary object caching is enabled, the table is truncated at the end of the module instead of being dropped. There are no changes to system metadata when a table variable is cached. The impact on allocation structures and transaction logging is limited to deleting the rows in the table and removing any excess data and allocation pages when the module ends.
Temporary Tables
When a temporary table is used instead of a table variable, the basic mechanism is essentially the same, with just a couple of extra renaming steps: When a temporary table is not cached, it is visible in tempdb with the familiar user-supplied name, followed by a bunch of underscores and the hexadecimal representation of the object id as a final suffix. The local temporary table remains until it is explicitly dropped, or until the scope in which it was created ends. For ad-hoc SQL, this means when the session disconnects from the server.
For a cached temporary table, the first time the module is run, the temporary table is created just as for the non-cached case. At the end of the module, instead of being dropped automatically (as the scope in which it was created ends), the temporary table is truncated and then renamed to the hexadecimal representation of the object ID (exactly as seen for the table variable). The next time the module runs, the cached table is renamed from the hexadecimal format to the user-supplied name (plus underscores plus hex object id).
The extra renaming operations at the start and end of the module involve a small number of system metadata changes. Cached temporary tables can therefore still experience at least some metadata contention under very high rates of reuse. Nevertheless, the metadata impact of a cached temporary table is much lower than for the non-cached case (creating and dropping the table each time).
More details and examples of how temporary object caching works can be found in my previous article.
Statistics on cached temporary tables
As mentioned earlier, statistics may be automatically created on temporary tables without losing the advantages of temporary object caching (as a reminder, manually creating statistics will disable caching).
An important caveat is that the statistics associated with a cached temporary table are not reset when the object is cached at the end of the module, or when the cached object is retrieved from cache at the start of the module. As a consequence, statistics on a cached temporary table may be left over from an unrelated prior execution. In other words, the statistics may bear absolutely no relation to the current contents of the temporary table.
This is obviously undesirable, given that the main reason to prefer a local temporary table over a table variable is the availability of accurate distribution statistics. In mitigation, the statistics will be automatically updated when (if) the accumulated number of changes to the underlying cached object reaches the internal Recompilation Threshold. This is difficult to assess in advance, because the details are complex and somewhat counterintuitive.
The most comprehensive workaround, while retaining the benefits of temporary object caching, is to:
- Manually
UPDATE STATISTICS
on the temporary table within the module; and - Add an
OPTION (RECOMPILE)
hint to statements that reference the temporary table
Naturally there is a cost involved in doing this, but this is most often acceptable. Indeed, by choosing to use a local temporary table in the first place, the module author is implicitly saying that plan selection is likely to be sensitive to the contents of the temporary table, so recompilation can make sense. Updating statistics manually ensures that the statistics used during the recompilation reflect the current contents of the table (as we would surely expect).
For more details on exactly how this works please see my previous article on the topic.
Summary and Recommendations
Temporary object caching within a module can greatly reduce the pressure on shared allocation and metadata structures in the tempdb database. The greatest reduction will occur when using table variables because caching and reusing these temporary objects does not involve modifying metadata at all (no renaming operations). Contention on allocation structures may still be seen if the single cached data page is insufficient to hold all the table variable's data at runtime.
The impact on plan quality due to the lack of cardinality information for table variables may be mitigated by using OPTION(RECOMPILE)
or trace flag 2453 (available from SQL Server 2012 onwards). Note that these mitigations only give the optimizer information about the total number of rows in the table.
To generalize, table variables are best used when the data is small (ideally fitting within a single data page for maximum contention benefits) and when plan selection does not depend on the values present in the table variable.
If information about the data distribution (density and histograms) is important for plan selection, use a local temporary table instead. Be sure to meet the conditions for temporary table caching, which most often means not creating indexes or statistics after the initial table creation statement. This is made more convenient from SQL Server 2014 onward due to the introduction of the INDEX
clause of the CREATE TABLE
statement.
An explicit UPDATE STATISTICS
after data is loaded into the temporary table, and OPTION (RECOMPILE)
hints on statements that reference the table may be needed to produce all the expected benefits of cached temporary tables within a module.
It is important to only use temporary objects when they produce a clear benefit, most often in terms of plan quality. Excessive, inefficient, or unnecessary use of temporary objects can lead to tempdb contention, even when temporary object caching is achieved.
Optimal temporary object caching may not be enough to reduce tempdb contention to acceptable levels in all cases, even where temporary objects are only used when fully justified. Using in-memory table variables or non-durable in-memory tables can provide targeted solutions in such cases, though there are always trade offs to be made, and no single solution currently represents the best option in all cases.
Nice in-depth analysis…
Hello,
Just curious if you are aware of any way of determining what cached execution context owns a specific cached temp table?
The last time someone asked me that, I could not find anything. I will take another look.
You are superb
Are temp tables created using SELECT…INTO cached as well (assuming no additional DDL)?
Yes.
Paul, given the superb depth of analysis you always provide in your articles, have you written any books on SQL Server yet?
Thank you, Bill. No, I have not.
Excellent article, Paul, but I'm confused about one thing. You said:
An explicit UPDATE STATISTICS after data is loaded into the temporary table, and OPTION (RECOMPILE) hints on statements that reference the table may be needed to produce all the expected benefits of cached temporary tables within a module.
And
manually creating statistics will disable caching
Don't these statements contradict one another? If you use UPDATE STATISTICS on a temporary table, won't it disable caching?
Manually creating statistics disables caching, but updating existing (cached) statistics is fine.
Paul,
I'm interested in how fast the space used in tempdb by the temp table is released.
I created a procedure that selects from a table into a temp table, and then TRUNCATEs the temp table. In a script, I then execute a query on tempdb.sys.dm_db_file_space_usage, execute the procedure, start a new batch, execute the query of tempdb.sys.dm_db_file_space_usage again, and end the script with "GO 40".
I found that it takes about a half second after the procedure ends before tempdb.sys.dm_db_file_space_usage returns to what it was before the proc started. If I remove the TRUNCATE statement, it takes between 2.25 and 4.5 seconds before the space is released. Why the delays? Why the difference in the delays?
Answered at https://dba.stackexchange.com/a/188727/1192
Nice article Paul.
I had a question, if you could help out please.
Will the use of "IF OBJECT_ID('tempdb..#tempResult')IS NOT NULL" before dropping temp table at the end of module cause additional tempdb contention?
It might https://support.microsoft.com/en-us/help/4131193/performance-issues-occur-in-form-of-pagelatch-ex-and-pagelatch-sh-wait but why do this? There's no point dropping the temporary table explicitly.
Thanks a lot for quick reply.
We have our legacy code that has these statements, which was even part of the coding standards for some reasons. I guess, it is because of misconception that dropping temp tables in the procedure reduces the amount of work to be done latter in background (which is usually delayed by 5 seconds).
Do you think it is specific to SQL Server 2016 as mentioned in the link? We never had issues of tempdb contention before, but after recent upgrade client started facing these issues. There has been few issues of deadlocks on system tables (sys.objvalues, sys.rowsets).
Thank you again.
I don't know – there's no information on this beyond what is written in that new KB. It is certainly possible, since there have been other tempdb-related regressions in SQL Server 2016. In any case, the explicit drop is pretty pointless.
Paul, your article above currently states that "DROP TABLE does not prevent caching". But this MSDN blog post states that "temp tables that are … explicitly dropped within the stored procedure will be marked for deletion since they can't be reused":
https://blogs.msdn.microsoft.com/sql_server_team/tempdb-files-and-trace-flags-and-updates-oh-my/
Am I misunderstanding something? I've been telling people that dropping temp tables at the end of stored procedures is simply unnecessary, but in fact it seems to be actively bad. (And the MSDN article gives no indication that this is a new behavior.)
Hi Sam,
That seems to be an error in the linked blog. I will reach out to Pam to see if it can be corrected or otherwise clarified.
It is easy to test and see that
DROP TABLE
does not prevent caching. See the scripts in my linked articles – the results are as I reported (retested just now on SQL Server 2017 RTM CU10).Paul-
Pam Lahoud’s presentation on tempdb as PASS 2018 indicated that explicitly dropping a temp table and truncating a temp table would impact caching of the temp table in contrast to your points above concerning misconceptions.
So which is correct? Any clarification on this would be much appreciated. Thanks!!!
Alex,
I have now heard back from Pam. The information in my article above is correct. Pam's article at https://blogs.msdn.microsoft.com/sql_server_team/tempdb-files-and-trace-flags-and-updates-oh-my/ will be updated shortly to correct the error.
Sam,
I have now heard back from Pam. The information in my article above is correct. Pam's article at https://blogs.msdn.microsoft.com/sql_server_team/tempdb-files-and-trace-flags-and-updates-oh-my/ will be updated shortly to correct the error.
Thank you for following up on this!
Paul,
If I am seeing blocking between session (1) inserting into temp table (which has a primary key) and a session (2) that is creating a clustered index as a separate DDL, could this be fixed by caching? Do you have a blog or stackover flow answer that would explain when object tables gets locked in tempdb. Thanks for all you great and detailed information you produce.
Hi Robert,
Are you talking about a local (#temp) or global (##temp) table? Actually, it might be easier if you could ask this as a question on https://dba.stackexchange.com/ with a repro.
Cheers