Status: Reviewed (deep-dive v1)
Stack note: Warehouse procs (SPIKE boards, evaluate/ack) and PowerShell-fed loads often stage rows mid-proc — pick the right temp structure or you inherit bad estimates.
Pairs with: SQL-11 (mTVF @table pitfalls), SQL-12 (procs — next on shortlist), SQL-07 (mystery plans).
Goal: Know when #temp / ##temp / @table / table types win, and the cardinality traps.
| Situation | Prefer | Why |
|---|---|---|
| Medium/large rowsets, need stats + indexes | #temp |
Statistics; can add indexes; better plans |
| Tiny lookup (few rows), simple pass-through | @table |
Light; no tempdb name churn; OK when estimates don’t matter |
| Reuse shape across procs / TVP from Blazor or PS | User-defined table type + TVP | Typed contract; still table-var semantics inside |
| Share across sessions (rare) | ##global temp |
Almost never — prefer real table or Service Broker |
| Multi-statement TVF return | @table inside TVF |
Required by TVF syntax — consider proc + #temp instead for heavy work |
#t)CREATE TABLE #Orders (
OrderId int NOT NULL PRIMARY KEY,
CustomerId int NOT NULL,
Total money NOT NULL
);
CREATE INDEX IX_Orders_Customer ON #Orders(CustomerId);
INSERT #Orders (…)
SELECT … FROM #Orders WHERE CustomerId = @Id;
-- dropped at end of session/scope (proc: end of proc unless ##)
Pros: Optimizer can use statistics; indexes (incl. after load); ALTER/TRUNCATE; behaves like a real table for plans.
Cons: Tempdb allocation/logging; name uniqueness per session; recompiles when stats change.
Use: Staging in usp_* boards, joins of hundreds+ rows, “load then index then query.”
@t)DECLARE @Orders TABLE (
OrderId int PRIMARY KEY,
CustomerId int NOT NULL,
Total money NOT NULL
-- indexes mostly via constraints at declare time (version-dependent extras exist)
);
INSERT @Orders …;
SELECT … FROM @Orders; -- often fixed/low cardinality guess historically
Pros: Scoped cleanly; less locking metadata noise; fine for small sets.
Cons: Weak/missing stats historically → bad estimates, under/over memory grants; harder to index after the fact; no TRUNCATE like #temp.
Use: A handful of IDs, parameter unpack, tiny maps. Not your 50k-row heat-map staging.
-- type once
CREATE TYPE dbo.OrderIdList AS TABLE (OrderId int PRIMARY KEY);
-- proc
CREATE PROC dbo.usp_Orders_ByIds @Ids dbo.OrderIdList READONLY AS
SELECT o.* FROM Sales.Orders o JOIN @Ids i ON i.OrderId = o.OrderId;
Pros: Clean Blazor/PS → SQL contract (no CSV string splitting).
Cons: Readonly inside proc; still table-variable-like estimates — for big TVPs, dump to #temp then join.
##t) — usually skipVisible to other sessions until last user disconnects. Prefer permanent staging table with a run id for Agent jobs.
#temp #temp #temp so estimates aren’t 1-row guesses INSERT #t SELECT * FROM @TVP then query #t @table — rewrite as proc + #temp when possible SELECT * INTO #t in a loop — one create, batched inserts | Workload | Pattern |
|---|---|
| SPIKE evaluate gathering findings | #Findings then merge to Alert |
| Backup heat / capacity snapshots | #Stage from collectors → merge procs |
| Blazor multi-select filters | TVP of ids → optional dump to #temp if list can be large |
| PowerShell bulk load | Bulk insert to staging table (permanent or #) — not row-by-row @table |
| UDF returning many rows | Prefer inline TVF or proc; avoid fat mTVF @table |
@table to save tempdb” myths (both use tempdb) | Symptom | Likely cause |
|---|---|
| Nested loop + huge outer, tiny estimate on “temp” | @table / TVP / mTVF estimate |
| Spills / wrong memory grant | Same |
| Tempdb full during nightly jobs | Fat #temp + parallelism + many sessions; check growth |
| “It was fast in SSMS with 10 rows” | Plan for small @table doesn’t scale |
@t and #t; join each to a big table — compare estimates and duration #t after load; retry #temp unless proven tiny @table semantics — reason we push iTVF / procs #temp TRY/CATCH around staging + cleanup #temp for staging; @table/TVP for small or typed input @table joins SQL Dude — SQL-13 Temp Tables vs Table Variables v1