Understanding TempDB: Spot Problems and Relieve the Load
TempDB is the shared workspace of the entire instance. Every database uses it: for temporary tables, for sorts that do not fit into memory, for row versions, and for much that does not appear as TempDB in the execution plan at all. If TempDB fills up, it hits all applications on the server at the same time, including those that did not cause the problem.
The good news: TempDB is not a black hole. SQL Server knows exactly which kind of work occupies how much space and which session or transaction is holding on to it. This page explains the consumers, shows the current state with a read-only SQL query, and leads to the deep dives. You do not need any PSG tool for this.
Who is using TempDB
SQL Server distinguishes four areas. The columns in the query below follow exactly this breakdown.
| Area | Typical consumers | In the query | Deep dive |
|---|---|---|---|
| User objects | #temp tables, table variables, global ##temp tables, index builds with SORT_IN_TEMPDB | user_objects_mb | – |
| Internal objects | Sorts and hash joins that spill, spools, cursors, large intermediate results | internal_objects_mb | Spills and spools |
| Version store | Read Committed Snapshot, snapshot isolation, triggers, online index operations, if Accelerated Database Recovery (ADR) is off for the database | version_store_mb | Version store and transaction log |
| TempDB log | Changes to temporary objects; open transactions prevent it from being freed again | log_mb, log_wait_reason | Version store and transaction log |
The first three areas are in the data files of TempDB, the fourth in the log. A full TempDB log (error 9002) and full data files (error 1105) therefore have different causes.
The deep dives
Setting up TempDB
“How many files, where do they go, and how big?”
In depth, with labRead article →02Version store and transaction log
“TempDB grows for hours although hardly anything is running.”
In depth, with SQL query and labRead article →03Spills and spools
“There’s a spool in the plan, and TempDB keeps growing.”
In depth, with SQL query and labRead article →Which observation matches yours?
| Your observation | First look | Deep dive |
|---|---|---|
| You are setting up TempDB or reviewing files and size | Check the number, size, and placement of the files | Setting up TempDB |
| TempDB is growing, and you don't know why | Snapshot, result 1: which area is growing | Depends on the area, see above |
| A query writes a lot to TempDB, and the plan shows a warning or a spool | Get the actual execution plan | Spills and spools |
| The version store grows over hours | Snapshot, result 3: oldest snapshot transaction | Version store and transaction log |
The TempDB log is full (error 9002, ACTIVE_TRANSACTION) | Snapshot, result 4: open transactions | Version store and transaction log |
| Everything is fine again after a restart | Save the evidence before the next restart | Why does restarting SQL Server help? |
Snapshot: What is using TempDB right now?
The query returns a snapshot in four results: the usage by area, the sessions with the largest space requirements, the version store with the oldest snapshot transactions, and the state of the TempDB log. Run it while the problem exists, and if needed several times at intervals of a few minutes.
Query: TempDB snapshot
/* PSG Stack · SQL Server TempDB · Query 1
TempDB snapshot: Who is using space right now, and what is holding on to it?
- Read-only. Changes nothing, ends no session.
- Every part runs with MAXDOP 1.
- Returns four results:
1 Usage of the TempDB data files by type
2 Sessions with the largest TempDB usage
3 Version store: size per database and oldest snapshot transactions
4 TempDB log: fill level, wait reason and open transactions
- Permissions: VIEW SERVER STATE
(from SQL Server 2022, VIEW SERVER PERFORMANCE STATE is enough).
Without this permission, every part fails with error 300 (VIEW DATABASE STATE alone is not enough).
- SQL Server 2017 and later. Tested in the PSG lab on 2017, 2019, 2022, and 2025.
- Runs in any database: TempDB values always come from TempDB.
- Snapshot: values change continuously. Run it several times if needed.
*/
/* 1 · Usage of the TempDB data files by type (MB) */
SELECT
SUM(fsu.user_object_reserved_page_count) * 8 / 1024 AS user_objects_mb,
SUM(fsu.internal_object_reserved_page_count) * 8 / 1024 AS internal_objects_mb,
SUM(fsu.version_store_reserved_page_count) * 8 / 1024 AS version_store_mb,
SUM(fsu.mixed_extent_page_count) * 8 / 1024 AS mixed_extents_mb,
SUM(fsu.unallocated_extent_page_count) * 8 / 1024 AS free_mb,
COUNT(*) AS data_files
FROM tempdb.sys.dm_db_file_space_usage AS fsu
OPTION (MAXDOP 1);
/* 2 · Sessions with the largest TempDB usage
Completed tasks (session_space_usage) plus running tasks (task_space_usage).
User objects: #temp tables, table variables, index builds with SORT_IN_TEMPDB.
Internal objects: sort, hash, spools, cursors, LOB intermediate results.
The counters apply per session: if another session drops an object (e.g., a global ## table),
the space stays charged to the creating session until it ends. The sum can therefore
exceed result 1. */
WITH space_used AS (
SELECT ssu.session_id,
ssu.user_objects_alloc_page_count - ssu.user_objects_dealloc_page_count AS user_pages,
ssu.internal_objects_alloc_page_count - ssu.internal_objects_dealloc_page_count AS internal_pages
FROM sys.dm_db_session_space_usage AS ssu
WHERE ssu.database_id = 2
UNION ALL
SELECT tsu.session_id,
tsu.user_objects_alloc_page_count - tsu.user_objects_dealloc_page_count,
tsu.internal_objects_alloc_page_count - tsu.internal_objects_dealloc_page_count
FROM sys.dm_db_task_space_usage AS tsu
WHERE tsu.database_id = 2
), per_session AS (
SELECT session_id,
SUM(user_pages) AS user_pages,
SUM(internal_pages) AS internal_pages
FROM space_used
GROUP BY session_id
)
SELECT TOP (20)
js.session_id,
es.login_name,
es.host_name,
es.program_name,
es.status,
CAST(CASE WHEN js.user_pages > 0 THEN js.user_pages ELSE 0 END * 8 / 1024.0 AS decimal(12,1)) AS user_objects_mb,
CAST(CASE WHEN js.internal_pages > 0 THEN js.internal_pages ELSE 0 END * 8 / 1024.0 AS decimal(12,1)) AS internal_objects_mb,
r.command,
r.wait_type,
r.granted_query_memory * 8 / 1024 AS memory_grant_mb,
DB_NAME(r.database_id) AS database_name,
LEFT(st.text, 4000) AS sql_text
FROM per_session AS js
JOIN sys.dm_exec_sessions AS es ON es.session_id = js.session_id
LEFT JOIN sys.dm_exec_requests AS r ON r.session_id = js.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS st
WHERE es.is_user_process = 1
AND (js.user_pages > 0 OR js.internal_pages > 0)
ORDER BY (CASE WHEN js.user_pages > 0 THEN js.user_pages ELSE 0 END
+ CASE WHEN js.internal_pages > 0 THEN js.internal_pages ELSE 0 END) DESC
OPTION (MAXDOP 1);
/* 3a · Version store in TempDB per database (MB) */
SELECT
DB_NAME(vs.database_id) AS database_name,
vs.reserved_page_count * 8 / 1024 AS version_store_mb
FROM sys.dm_tran_version_store_space_usage AS vs
WHERE vs.reserved_page_count > 0
ORDER BY vs.reserved_page_count DESC
OPTION (MAXDOP 1);
/* 3b · Oldest snapshot transactions: they hold on to the version store
snapshot_isolation = 0: transaction without SNAPSHOT isolation that creates row versions
(e.g., writers under READ_COMMITTED_SNAPSHOT). */
SELECT TOP (10)
ast.session_id,
ast.elapsed_time_seconds AS open_s,
ast.is_snapshot AS snapshot_isolation,
es.login_name,
es.host_name,
es.program_name,
es.status,
es.last_request_end_time AS last_request_end
FROM sys.dm_tran_active_snapshot_database_transactions AS ast
LEFT JOIN sys.dm_exec_sessions AS es ON es.session_id = ast.session_id
ORDER BY ast.elapsed_time_seconds DESC
OPTION (MAXDOP 1);
/* 4a · TempDB log: size, fill level and wait reason */
SELECT
CAST(ls.total_log_size_in_bytes / 1048576.0 AS decimal(12,1)) AS log_mb,
CAST(ls.used_log_space_in_percent AS decimal(5,1)) AS used_percent,
d.log_reuse_wait_desc AS log_wait_reason
FROM tempdb.sys.dm_db_log_space_usage AS ls
CROSS JOIN sys.databases AS d
WHERE d.database_id = 2
OPTION (MAXDOP 1);
/* 4b · Open transactions with TempDB log usage, largest first */
SELECT TOP (10)
st.session_id,
dt.database_transaction_begin_time AS began_at,
DATEDIFF(SECOND, dt.database_transaction_begin_time, SYSDATETIME()) AS open_s,
CAST(dt.database_transaction_log_bytes_used / 1048576.0 AS decimal(12,1)) AS log_used_mb,
CAST(dt.database_transaction_log_bytes_reserved / 1048576.0 AS decimal(12,1)) AS log_reserved_mb,
es.login_name,
es.program_name,
es.status
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_session_transactions AS st ON st.transaction_id = dt.transaction_id
LEFT JOIN sys.dm_exec_sessions AS es ON es.session_id = st.session_id
WHERE dt.database_id = 2
ORDER BY dt.database_transaction_log_bytes_used DESC
OPTION (MAXDOP 1);How to read the result:
- Result 1 shows which area is growing. If the space is in
internal_objects_mb, look in result 2 for running queries with a large memory grant or large sorts. If it is inversion_store_mb, result 2 usually does not help: versions are not attributed to any session. - Result 2 names the sessions with their own space requirements. A sleeping session with many user objects often holds a large
#temptable that only disappears with the connection. The counters apply per session: if another session drops an object, such as a global##table, the space stays charged to the creating session until it ends. The sum can therefore exceed result 1. - Result 3 shows for which database versions are being created and which snapshot transaction has held them the longest.
open_sin the range of hours with status “sleeping” is a typical holding transaction. - Result 4 shows why the TempDB log is not being freed. With
ACTIVE_TRANSACTION, the cause is in the list of open transactions. If the log is still only slightly filled,NOTHINGoften still appears despite an open transaction; the list in 4b is then what counts.
Sample output from a reproduced lab run (SQL Server 2025, synthetic data, names chosen freely, columns shortened). Session 56 filled a #temp table and left its transaction open, session 54 holds an open cursor, session 53 reads in SNAPSHOT isolation without finishing:
1 · Usage by type (MB)
user_objects_mb internal_objects_mb version_store_mb free_mb
40 48 53 127
2 · Sessions with the largest usage
session_id status user_objects_mb internal_objects_mb
54 sleeping 0.0 42.4
56 sleeping 39.2 0.0
60 running 0.0 6.0
3b · Oldest snapshot transactions
session_id open_s snapshot_isolation status
53 25 1 sleeping
4a · TempDB log
log_mb used_percent log_wait_reason
136.0 99.5 ACTIVE_TRANSACTION
4b · Open transactions with TempDB log usage
session_id open_s log_used_mb status
56 20 78.0 sleepingReading it: the version store (53 MB) is not attributed to any session in result 2; it is held by session 53 in result 3b. The log is almost full because session 56 does not end its transaction. Both sessions are asleep: they are not working, they only hold on.
Three common assumptions that are not true
“Temporary tables are not logged.” Changes in TempDB are logged, just to a lesser extent than in user databases: they have to be rolled back, but not recovered after a crash. A long transaction that writes to #temp tables can therefore fill the TempDB log just like any other.
“The session with the most TempDB space is the culprit.” For user and internal objects, that is usually true. For the version store, it is not: the versions are created by the writing sessions, but they are held by the oldest transaction that could still read them. That transaction often has not occupied a single page itself.
“Shrinking solves the problem.” As long as the cause persists, TempDB grows again. Shrinking during operation can also fail because of pages in use. Microsoft describes the options and limits in a separate article (see Sources). First find and end whatever occupies the space.
What provides lasting relief
- Set up TempDB appropriately. Several data files of equal size and enough space from the start avoid waits during space allocation and growth at the wrong moment. How many files, where, and how large is shown in Setting up TempDB. This does not replace knowing the consumers.
- Avoid work instead of making space. A suitable index saves sorts and temporary indexes, and a better estimate prevents spills. What this looks like in the execution plan is shown in Spills and spools.
- Keep transactions short. Every open transaction holds on to versions and log. More in Version store and transaction log.
- Use temporary tables deliberately. A
#temptable with a few filtered rows can simplify a query considerably. A#temptable that copies half a table only shifts work to TempDB. - Widen your view. If TempDB is only a symptom and the server as a whole is slow, the SQL Server performance guide leads you through the matching diagnostic paths.
Sources
- Microsoft: tempdb Database
- Microsoft: Shrink the tempdb Database
- Microsoft:
sys.dm_db_task_space_usage(Transact-SQL) - Microsoft: Transaction Locking and Row Versioning Guide