Writes, optimizes, and debugs T-SQL queries. Explains SQL Server internals, troubleshoots performance issues, and guides database administration tasks including backup/restore, high availability, security, and index design. Use when the user asks about T-SQL syntax, SQL Server administration, query performance, stored procedures, indexes, locking, transactions, backup/restore, high availability, security, or any MSSQL-related topic — even without saying 'SQL Server' explicitly. Also trigger on terms like SSMS, tempdb, bcp, sqlcmd, MSSQL, sp_executesql, NOLOCK, columnstore, Hekaton, RCSI, param sniffing, or execution plan.
100
100%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Load this file when the user asks about:
PAGELATCH_EX / PAGELATCH_SH on 2:1:1, 2:1:2, 2:1:3)memory-optimized tempdb metadata (SQL Server 2019+)TempDB is a global, shared, single-instance system database that is recreated from scratch every time SQL Server starts. It cannot be backed up or restored — its contents are always ephemeral.
| Property | Value |
|---|---|
| Database ID | 2 (always) |
| Recovery model | Simple (always — cannot change) |
| Recreated on | Every SQL Server service restart |
| Shared by | All databases on the instance |
| Scope of objects | Session-local (# temp tables), global (## tables), permanent until drop |
TempDB contains three categories of pages:
#, ##), table variables, indexes on temp tables, spillsThe critical insight: allocation bitmaps are shared across all sessions. Every object creation acquires a latch on the relevant bitmap page. Under high concurrency this becomes the bottleneck.
| Category | Examples |
|---|---|
| User temp tables | CREATE TABLE #foo, SELECT INTO #foo |
| Global temp tables | ##GlobalFoo — visible to all sessions until creator disconnects |
| Table variables | DECLARE @t TABLE (...) |
| Index spills | Sort/Hash spills when memory grant is exhausted |
| Row version store | RCSI, Snapshot Isolation, triggers with OUTPUT, MARS, online index builds |
| Online index rebuild | Intermediate pages during REBUILD ... ONLINE |
| DBCC | CHECKDB with ESTIMATEONLY uses TempDB for internal structures |
| Statistics | Some in-memory statistics operations |
| Service Broker | Internal queue work tables |
| Cursors (STATIC) | Full snapshot of cursor result set |
When many concurrent sessions create or drop temp objects, they all compete to update the same GAM/SGAM/PFS pages in TempDB data files. These are:
2:1:1 — PFS (Page Free Space) for the first 8,088 pages2:1:2 — GAM (Global Allocation Map) — tracks uniform extents2:1:3 — SGAM (Shared Global Allocation Map) — tracks mixed extentsThe contention shows up as PAGELATCH_EX or PAGELATCH_SH wait types with resource names like 2:1:1.
[!NOTE] The "latch" in
PAGELATCH_EXis a buffer pool page latch protecting the in-memory page — this is not an I/O latch (PAGEIOLATCH_*). The fix is more files, not faster disks.
SQL Server uses a proportional fill algorithm — it distributes object allocation across all TempDB data files proportionally to their free space. More equally-sized files = fewer sessions competing for the same bitmap page.
Each file has its own set of GAM/SGAM/PFS pages, so contention is divided across files.
-- Look for PAGELATCH_EX/SH waits on TempDB pages
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
resource_description
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'PAGELATCH%'
ORDER BY wait_time_ms DESC;
-- Cross-reference resource_description to identify TempDB
-- Resource format: DatabaseId:FileId:PageId
-- TempDB = database_id 2
-- Check currently waiting requests
SELECT
r.session_id,
r.wait_type,
r.wait_resource,
r.wait_time,
t.text AS sql_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.wait_type LIKE 'PAGELATCH%'
AND r.wait_resource LIKE '2:%';Microsoft's guidance (revised with SQL Server 2016 improvements):
| Logical CPU count | Recommended TempDB files |
|---|---|
| ≤ 8 cores | 1 file per core (equal to core count) |
| > 8 cores | Start with 8, add in groups of 4 if contention persists |
| Very high concurrency | More files reduce contention but add management overhead; 16 is usually the ceiling |
[!NOTE] SQL Server 2016 introduced improved PFS page latch contention reduction using a round-robin allocation hint, making the file count formula less aggressive than the old "1 per core" rule. However, multiple equally-sized files is still the correct approach.
-- Check current TempDB file configuration
SELECT
name,
physical_name,
size * 8 / 1024 AS size_mb,
growth,
is_percent_growth,
max_size
FROM tempdb.sys.database_files
WHERE type = 0; -- data files only
-- Alter files to equal size (example: 4 files at 4 GB each)
-- Run this for each file; adjust paths as needed
ALTER DATABASE tempdb
MODIFY FILE (NAME = 'tempdev', SIZE = 4096 MB, FILEGROWTH = 512 MB);
ALTER DATABASE tempdb
ADD FILE (NAME = 'tempdev2', FILENAME = 'D:\tempdb\tempdev2.ndf', SIZE = 4096 MB, FILEGROWTH = 512 MB);
ALTER DATABASE tempdb
ADD FILE (NAME = 'tempdev3', FILENAME = 'D:\tempdb\tempdev3.ndf', SIZE = 4096 MB, FILEGROWTH = 512 MB);
ALTER DATABASE tempdb
ADD FILE (NAME = 'tempdev4', FILENAME = 'D:\tempdb\tempdev4.ndf', SIZE = 4096 MB, FILEGROWTH = 512 MB);[!WARNING] TempDB file count and initial size changes survive restarts because they are stored in master — but you cannot reduce file count (SQL Server cannot remove data files from TempDB while running). Restarts are required for size changes to take effect in some configurations. Use
mssql-confor SQL Server Configuration Manager to set startup parameters.
TempDB has only one log file. Frequent autogrowth events on the log file indicate:
[!NOTE] SQL Server 2019
SQL Server 2019 introduced memory-optimized TempDB metadata, which moves the internal system tables that track temp object allocation into In-Memory OLTP structures. This eliminates latch contention on those system objects entirely for many workloads.
-- Check current state
SELECT SERVERPROPERTY('IsTempDbMetadataMemoryOptimized');
-- Enable (requires restart)
ALTER SERVER CONFIGURATION SET MEMORY_OPTIMIZED TEMPDB_METADATA = ON;
-- Verify after restart
SELECT SERVERPROPERTY('IsTempDbMetadataMemoryOptimized');
-- Returns 1 when activeThe following system tables are moved to memory-optimized structures:
sys.sysobjvaluessys.sysschobjssys.sysallocunitssys.sysfiles1sys.syshobtcolumnssys.syshobtssys.sysidxstatsPAGELATCH_EX / PAGELATCH_SH on 2:1:1 family is the dominant waitThis is one of the most frequently misunderstood performance topics.
| Dimension | Temp Table (#t) | Table Variable (@t) | CTE |
|---|---|---|---|
| Storage location | TempDB | TempDB (usually) | None — re-evaluated inline |
| Statistics | Yes (auto-created) | No (fixed cardinality estimate) | None — uses source statistics |
| Cardinality estimate | From statistics | Fixed: 1 row (pre-2019) or deferred (2019+) | Depends on the source tables |
| Index support | Yes (all index types) | Limited (inline PRIMARY KEY, UNIQUE only) | No |
| Parallelism | Yes | Yes | Yes |
| Transaction scope | Survives COMMIT/ROLLBACK of outer transaction | Same as temp table — contents rolled back | N/A |
| DDL rollback | Table dropped on rollback of CREATE | Table dropped on rollback | N/A |
| Scope | Current proc + called procs | Current batch only | Current statement only |
| Recompile trigger | Column changes, statistics updates | Rarely | N/A |
| INSERT logging | Minimal (SELECT INTO from base tables, under Simple/Bulk-Logged) | Same as temp table | N/A |
Use a temp table when:
Use a table variable when:
Use a CTE when:
[!WARNING] A common myth is that "table variables are stored in memory." They are stored in TempDB exactly like temp tables, but they do not generate statistics. The real difference is cardinality estimation, not storage.
[!NOTE] SQL Server 2019
With database compatibility level 150+, SQL Server defers the compilation of statements using table variables until the table variable has been populated. This allows the optimizer to use the actual row count for cardinality estimation, largely eliminating the "1 row" estimate problem.
-- Verify deferred compilation is active
SELECT name, value
FROM sys.database_scoped_configurations
WHERE name = 'DEFERRED_COMPILATION_TV';
-- Default is ON for compat 150+
-- Disable if causing regressions
ALTER DATABASE SCOPED CONFIGURATION SET DEFERRED_COMPILATION_TV = OFF;SQL Server caches temp tables in stored procedures to avoid repeated object creation overhead. Conditions for caching:
When a cached temp table is reused, a truncate-equivalent happens silently, and the structure is reused without going through full allocation. This makes temp tables inside stored procedures much cheaper than perceived.
The version store is located in TempDB and is used by:
| Feature | When versions are generated |
|---|---|
RCSI (READ_COMMITTED_SNAPSHOT ON) | Every UPDATE/DELETE on tables in that database |
Snapshot Isolation (ALLOW_SNAPSHOT_ISOLATION ON) | During snapshot transactions |
Triggers with OUTPUT INTO | During trigger execution |
| MARS (Multiple Active Result Sets) | Open result sets across batches |
| Online index operations | During the index rebuild |
| Accelerated Database Recovery (2019+) | PVS in user database, but version store in TempDB still used |
Each row version is stored as a copy of the before-image with a 14-byte version tag appended. Versions are chained:
Current row → version 3 → version 2 → version 1
(in version store)Versions are cleaned up by a background version cleanup process once no active transaction needs them.
-- Current version store size and cleanup rate
SELECT
reserved_page_count * 8.0 / 1024 AS version_store_mb,
reserved_extent_count,
elapsed_time_seconds,
rows_returned,
cleanup_version
FROM sys.dm_tran_version_store_space_usage;
-- Active snapshot transactions holding old versions
SELECT
transaction_id,
elapsed_time_seconds,
transaction_sequence_num,
is_snapshot,
session_id
FROM sys.dm_tran_active_snapshot_database_transactions
ORDER BY elapsed_time_seconds DESC;
-- How much of the version store does each database contribute?
SELECT
database_name,
reserved_space_kb / 1024.0 AS reserved_mb,
reserved_space_kb / 1024.0 / 1024.0 AS reserved_gb
FROM sys.dm_tran_version_store_space_usage
ORDER BY reserved_space_kb DESC;| Cause | Symptom | Fix |
|---|---|---|
| Long-running snapshot/RCSI transaction | Single old transaction_sequence_num holds all cleanup | Find and kill the session; fix the application |
| Abandoned open transactions | elapsed_time_seconds grows indefinitely | Detect via sys.dm_exec_sessions — open_transaction_count > 0 |
| High UPDATE/DELETE rate without RCSI | Rapid version store growth | Review whether RCSI is needed; tune the workload |
| Blocking on cleanup thread | Version store grows faster than cleanup can keep up | Typically caused by the above; fix the root cause |
-- Top latch waits by total wait time (last reset or restart)
SELECT TOP 20
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
CASE
WHEN wait_type LIKE 'PAGELATCH%' THEN 'Potential TempDB contention — check resource_description'
WHEN wait_type LIKE 'PAGEIOLATCH%' THEN 'I/O-related — check disk'
ELSE ''
END AS diagnosis
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE '%LATCH%'
ORDER BY wait_time_ms DESC;-- Overall TempDB space usage by category
SELECT
SUM(user_object_reserved_page_count) * 8 AS user_objects_kb,
SUM(internal_object_reserved_page_count) * 8 AS internal_objects_kb,
SUM(version_store_reserved_page_count) * 8 AS version_store_kb,
SUM(unallocated_extent_page_count) * 8 AS free_space_kb,
SUM(mixed_extent_page_count) * 8 AS mixed_extents_kb
FROM sys.dm_db_file_space_usage
WHERE database_id = 2;
-- Per-session TempDB usage (who is using the most space?)
SELECT TOP 20
s.session_id,
s.login_name,
s.host_name,
s.program_name,
t.user_objects_alloc_page_count * 8 AS user_obj_kb,
t.internal_objects_alloc_page_count * 8 AS internal_obj_kb,
t.user_objects_alloc_page_count * 8
+ t.internal_objects_alloc_page_count * 8 AS total_kb
FROM sys.dm_db_session_space_usage t
JOIN sys.dm_exec_sessions s ON t.session_id = s.session_id
WHERE t.user_objects_alloc_page_count + t.internal_objects_alloc_page_count > 0
ORDER BY total_kb DESC;
-- Per-task (request-level) TempDB usage
SELECT TOP 10
r.session_id,
r.request_id,
t.user_objects_alloc_page_count * 8 AS user_obj_kb,
t.internal_objects_alloc_page_count * 8 AS internal_obj_kb,
q.text AS sql_text
FROM sys.dm_db_task_space_usage t
JOIN sys.dm_exec_requests r
ON t.session_id = r.session_id AND t.request_id = r.request_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) q
ORDER BY (t.user_objects_alloc_page_count + t.internal_objects_alloc_page_count) DESC;-- TempDB file sizes and free space
SELECT
name,
physical_name,
size * 8 / 1024 AS size_mb,
FILEPROPERTY(name, 'SpaceUsed') * 8 / 1024 AS used_mb,
(size - FILEPROPERTY(name, 'SpaceUsed')) * 8 / 1024 AS free_mb
FROM tempdb.sys.database_files
WHERE type = 0;sys.dm_db_file_space_usage still works to check usageALTER DATABASE tempdbTempDB is not replicated to secondary replicas — each node in an AG has its own TempDB. This has important implications:
# Set TempDB file count and size via mssql-conf
mssql-conf set sqlagent.errorlogfile /var/opt/mssql/log/sqlagent.out
mssql-conf set tempdb.numberoffiles 8
mssql-conf set tempdb.filegrowth 256
mssql-conf set tempdb.maxsize 0 # 0 = unlimited
mssql-conf set tempdb.filesize 4096 # MBSQL Server derives some TempDB file defaults from the model database at startup. However, file count and path are controlled by startup parameters and the last-known TempDB configuration stored in master.
-- Set max size on TempDB files to prevent runaway growth
-- (also set alert on low disk space — do not rely on max size alone)
ALTER DATABASE tempdb
MODIFY FILE (NAME = 'tempdev', MAXSIZE = 10240 MB);
-- Set up an alert using SQL Server Agent for low disk space
-- Or use a monitoring tool (see 50-sql-server-agent.md)-- Shrink TempDB data file (only works if space is truly free)
USE tempdb;
DBCC SHRINKFILE (tempdev, 1024); -- target size in MB
-- Check why shrink is not working — active version store or temp objects
SELECT * FROM sys.dm_db_file_space_usage WHERE database_id = 2;[!WARNING] DBCC SHRINKFILE on TempDB causes significant fragmentation of internal allocation structures and is not recommended in production. The only safe approach to reduce TempDB file size is to restart SQL Server with a smaller initial file size configured.
-- List all TempDB files
SELECT name, type_desc, physical_name,
size * 8 / 1024 AS size_mb,
growth,
is_percent_growth
FROM tempdb.sys.database_files;
-- Temp objects currently in TempDB (user-visible)
SELECT
name,
object_id,
type_desc,
create_date
FROM tempdb.sys.objects
WHERE is_ms_shipped = 0
ORDER BY create_date DESC;
-- Active version store transactions (what's preventing version cleanup?)
SELECT
s.session_id,
s.login_name,
s.open_transaction_count,
t.elapsed_time_seconds,
t.transaction_sequence_num,
t.is_snapshot
FROM sys.dm_tran_active_snapshot_database_transactions t
LEFT JOIN sys.dm_exec_sessions s ON t.session_id = s.session_id
ORDER BY t.elapsed_time_seconds DESC;
-- Check if memory-optimized TempDB metadata is enabled
SELECT SERVERPROPERTY('IsTempDbMetadataMemoryOptimized') AS is_memory_optimized;
-- PFS/GAM/SGAM latch waits — the TempDB contention signature
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_type IN ('PAGELATCH_EX', 'PAGELATCH_SH', 'PAGELATCH_UP')
ORDER BY wait_time_ms DESC;CREATE OR ALTER PROCEDURE dbo.usp_ProcessOrders
@DateFrom DATE,
@DateTo DATE
AS
BEGIN
SET NOCOUNT ON;
-- Temp table: statistics created, indexes supported
CREATE TABLE #staging (
OrderID INT NOT NULL,
OrderDate DATE NOT NULL,
TotalAmt DECIMAL(9,2) NOT NULL,
INDEX ix_staging_date (OrderDate)
);
INSERT INTO #staging (OrderID, OrderDate, TotalAmt)
SELECT o.OrderID, o.OrderDate, o.TotalAmt
FROM dbo.Orders o
WHERE o.OrderDate BETWEEN @DateFrom AND @DateTo;
-- Further processing using #staging
SELECT * FROM #staging WHERE TotalAmt > 1000;
END;-- Find the session holding the oldest snapshot transaction
SELECT TOP 1
s.session_id,
s.login_name,
s.host_name,
s.program_name,
t.elapsed_time_seconds,
t.transaction_sequence_num
FROM sys.dm_tran_active_snapshot_database_transactions t
JOIN sys.dm_exec_sessions s ON t.session_id = s.session_id
ORDER BY t.elapsed_time_seconds DESC;
-- If confirmed: KILL <session_id>-- Create a session to catch hash/sort spills (see 33-extended-events.md)
CREATE EVENT SESSION [TempDB_Spills] ON SERVER
ADD EVENT sqlserver.hash_warning,
ADD EVENT sqlserver.sort_warning
ADD TARGET package0.ring_buffer (SET max_memory = 51200)
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);
ALTER EVENT SESSION [TempDB_Spills] ON SERVER STATE = START;
-- Read spill events
SELECT
CAST(target_data AS XML).query('
//RingBufferTarget/event[@name="hash_warning" or @name="sort_warning"]
') AS spill_events
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
WHERE s.name = 'TempDB_Spills';-- Check TempDB free space percentage and alert if below threshold
-- (Run as SQL Agent job every 5 minutes)
DECLARE @free_pct FLOAT;
SELECT @free_pct =
100.0 * SUM(unallocated_extent_page_count)
/ NULLIF(SUM(total_page_count), 0)
FROM sys.dm_db_file_space_usage
WHERE database_id = 2;
IF @free_pct < 20
BEGIN
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'DBA Alerts',
@recipients = 'dba@example.com',
@subject = 'TempDB low space',
@body = 'TempDB free space is below 20%.';
END;PAGELATCH ≠ disk I/O problem. PAGELATCH_EX on TempDB is a concurrency problem, not a storage performance problem. Adding faster disks will not fix it. Add equally-sized data files.
Unequal file sizes defeat proportional fill. If one TempDB file is 8 GB and another is 1 GB, almost all allocations go to the 8 GB file. All files must be the same size.
Table variables are not always in memory. They live in TempDB just like temp tables. The difference is statistics, not location.
CTE re-execution. A CTE referenced twice in a query executes twice. If it reads from a base table, that scan happens twice. Use a temp table to materialize when the CTE is expensive and referenced multiple times. See references/04-ctes.md.
Version store holds TempDB space until cleanup. Even if you commit all your transactions, the version store is not immediately released — background cleanup runs asynchronously. Long-running snapshot transactions on any database on the instance delay cleanup for the entire instance.
TempDB objects do not survive restart. Global temp tables (##) and regular temp tables are all dropped on restart. Do not rely on them for cross-session persistence beyond a running instance.
Temp table caching only works inside stored procedures. Temp tables created in ad hoc batches are not cached and incur full creation overhead every time.
SHRINKFILE degrades TempDB allocation structures. Shrinking causes extent fragmentation in the allocation bitmaps, worsening future latch contention. Avoid it except in emergencies.
Memory-optimized TempDB metadata requires a restart. Enabling it with ALTER SERVER CONFIGURATION takes effect only after the next SQL Server service restart.
Online index operations use TempDB. Large online index rebuilds can consume significant TempDB space. Plan and monitor accordingly, especially on busy instances.
Readable secondary TempDB is separate. Queries on an AG secondary that spill to TempDB use the secondary's own TempDB. Size both primary and secondary TempDB appropriately if you use readable secondaries.
#temp tables in dynamic SQL share session scope. A temp table created inside sp_executesql is visible in the calling scope. This is by design (TempDB temp tables are session-scoped) but can be surprising.
references/13-transactions-locking.md — RCSI, Snapshot Isolation, version store backgroundreferences/18-in-memory-oltp.md — memory-optimized tables do not use TempDBreferences/29-query-plans.md — detecting hash/sort spills in plansreferences/33-extended-events.md — capturing spill events with XE sessionsreferences/31-intelligent-query-processing.md — memory grant feedback reduces spillsreferences