CTE vs Temp Table vs Subquery in SQL Server: Which Is Faster?

"Should I use a CTE or a temp table?" is one of the most common SQL Server questions, and the answer is often misunderstood. The key fact: a CTE is just a named query, not a stored result. Once you know that, choosing becomes straightforward.

CTE vs temp table vs subquery in SQL Server

The three options

-- 1. Subquery (derived table)
SELECT d.DepartmentId, d.TotalPay
FROM (SELECT DepartmentId, SUM(Amount) AS TotalPay
      FROM dbo.Payment GROUP BY DepartmentId) AS d
WHERE d.TotalPay > 100000;

-- 2. Common table expression (CTE)
WITH DeptPay AS (
    SELECT DepartmentId, SUM(Amount) AS TotalPay
    FROM dbo.Payment GROUP BY DepartmentId
)
SELECT DepartmentId, TotalPay
FROM DeptPay
WHERE TotalPay > 100000;

-- 3. Temporary table
SELECT DepartmentId, SUM(Amount) AS TotalPay
INTO #DeptPay
FROM dbo.Payment GROUP BY DepartmentId;

SELECT DepartmentId, TotalPay
FROM #DeptPay
WHERE TotalPay > 100000;

CTEs and subqueries perform the same

SQL Server expands a non-recursive CTE into the main query before optimizing, exactly as it does with a derived table. So the first two queries above normally get the same execution plan. A CTE is not faster or slower than a subquery; it is simply easier to read, especially when you chain several steps.

A CTE is not materialized

This is the part that catches people out. SQL Server does not store a CTE's result. If you reference the same CTE twice, the underlying query is usually evaluated twice:

WITH Expensive AS (
    SELECT EmployeeId, SUM(Amount) AS Total
    FROM dbo.Payment
    GROUP BY EmployeeId
)
SELECT a.EmployeeId, a.Total, b.Total AS OtherTotal
FROM Expensive a
JOIN Expensive b ON b.EmployeeId = a.EmployeeId + 1;   -- aggregation can run twice

If that inner query is expensive, a temp table that computes it once is often much faster.

When a temp table wins

  • The result is reused several times, in one query or across statements.
  • The query is very complex, with many joins. Breaking it into steps gives the optimizer smaller problems and better estimates.
  • You need statistics or an index on the intermediate result. Temp tables get statistics and can be indexed; CTEs cannot.
  • You want to debug step by step, by checking row counts in the middle.

The cost is writing the rows to tempdb, which is worth it for expensive intermediate results but wasteful for small, simple ones.

When a CTE or subquery wins

  • The intermediate result is used once.
  • The query is simple enough for the optimizer to handle in one go. Keeping it as one statement lets SQL Server push filters down and skip work that a temp table would have done in full.
  • You are inside a view or inline table-valued function, where temp tables are not allowed.

Recursive CTEs

CTEs have one unique feature: recursion, which is ideal for hierarchies such as managers and reports or account trees:

WITH OrgChart AS (
    SELECT EmployeeId, ManagerId, FullName, 0 AS Level
    FROM dbo.Employee
    WHERE ManagerId IS NULL                 -- the top of the tree
    UNION ALL
    SELECT e.EmployeeId, e.ManagerId, e.FullName, o.Level + 1
    FROM dbo.Employee e
    JOIN OrgChart o ON e.ManagerId = o.EmployeeId
)
SELECT * FROM OrgChart
OPTION (MAXRECURSION 200);

By default recursion stops with an error after 100 levels; MAXRECURSION changes the limit, and 0 removes it (use carefully).

How to decide

SituationUse
Result used once, simple queryCTE or subquery
Readability for multi-step logicCTE
Expensive result used more than onceTemp table
Very complex query with bad estimatesTemp table steps
Hierarchy or treeRecursive CTE
Inside a view or inline functionCTE or subquery

When in doubt, try both and compare actual plans and logical reads with realistic data.

Related reading:

Comments

Popular posts from this blog

How to Read a SQL Server Execution Plan (Beginner's Guide)

SARGable Queries in SQL Server: Why Functions in WHERE Slow You Down

SQL Server Statistics Explained: Why Your Query Plans Go Wrong