Quick comparison

CTESubqueryTemp table
ScopeOne statementOne statement (or one nested reference)Session or transaction
Reusable across separate statements?NoNoYes
Can be indexed?No (usually)NoYes
Can self-reference (recursion)?Yes (WITH RECURSIVE)NoNot directly
Can be correlated to an outer row?NoYesNo
Best forReadability, breaking down complex logicSmall, one-off, or row-dependent checksReused or expensive intermediate results

CTE: readable, scoped to one statement

A CTE (WITH name AS (...)) gives an intermediate result a name and lets a query be read top-to-bottom as a sequence of named steps, instead of as deeply nested parentheses:

cte-example.sql
WITH monthly_revenue AS (
  SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS revenue
  FROM orders
  GROUP BY DATE_TRUNC('month', order_date)
)
SELECT month, revenue
FROM monthly_revenue
WHERE revenue > 10000;

It exists only for the duration of this one statement — it can't be queried again afterward, and no other session can see it. For a deeper dive into CTEs versus plain subqueries specifically, see CTEs vs Subqueries – When to Use What.

Subquery: nested, sometimes correlated

A subquery is a query nested directly inside another, with no independent name — it exists only in the exact spot it's written:

subquery-example.sql
-- Non-correlated: computed once
SELECT * FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);

-- Correlated: re-evaluated per outer row
SELECT o.*
FROM orders o
WHERE o.amount > (
  SELECT AVG(o2.amount) FROM orders o2 WHERE o2.customer_id = o.customer_id
);

The correlated version is something neither a CTE nor a temp table does the same way — it genuinely re-runs, conceptually, once per row of the outer query, referencing that specific row's values each time. See Correlated Subqueries Explained Simply for the full mechanics.

Temp table: persisted, indexable, reusable

A temp table is a genuine physical table — it has real storage, can have its own index, and survives across multiple separate statements within the same session or transaction:

temp-table-example.sql
CREATE TEMP TABLE monthly_revenue AS
SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date);

CREATE INDEX idx_month ON monthly_revenue(month);

-- Now reusable across as many separate queries as needed, in this session
SELECT * FROM monthly_revenue WHERE revenue > 10000;
SELECT AVG(revenue) FROM monthly_revenue;

This is the one option of the three that makes sense when a report runs several genuinely separate queries against the same expensive intermediate result — compute it once, index it, then query it repeatedly.

Materialization: the performance detail that trips people up

Whether a CTE's result is actually computed once and stored ("materialized"), or simply inlined into the surrounding query's plan like a subquery, depends on the database and, in some cases, on how the CTE is used:

DatabaseCTE materialization behavior
PostgreSQL 12+Inlined by default (like a subquery), unless referenced more than once or marked MATERIALIZED explicitly
PostgreSQL < 12Always materialized — an "optimization fence" the planner couldn't see through
SQL ServerTypically inlined; the optimizer treats a CTE much like a view or subquery
MySQL 8+May materialize or inline depending on the query, at the optimizer's discretion

The practical implication: a CTE referenced multiple times in the same query is not guaranteed to be computed only once — on some databases, each reference may recompute it from scratch. A temp table never has this ambiguity, since it's computed exactly once and then read from directly.

Decision guide

SituationUse
Breaking a complex query into readable, named stepsCTE
A quick, one-off comparison against an aggregateSubquery
A filter that depends on each outer row's own valuesCorrelated subquery
Walking a hierarchy of unknown depthRecursive CTE
The same intermediate result queried by several separate statementsTemp table
An intermediate result large enough to benefit from its own indexTemp table

Common mistakes

  • Assuming a CTE is always materialized once. On several databases it may be recomputed on every reference — a temp table is the only one of the three that guarantees single computation.
  • Reaching for a temp table for a single, simple statement. The overhead of creating and dropping a physical table isn't worth it if a CTE or subquery would do the same job within one query.
  • Writing a correlated subquery when a plain JOIN would do the same job faster. Correlated subqueries can be expensive if the database doesn't optimize them well; a rewrite as a join is often worth trying.
  • Forgetting temp tables persist for the whole session — leftover temp tables from an earlier step can silently affect a later, unrelated query if names collide.

Key takeaways

  • A CTE is for readability within a single statement — not guaranteed to be materialized only once on every database.
  • A subquery is nested inline and can be correlated to reference the outer query's current row.
  • A temp table is a real, indexable object that persists across multiple statements — the only one of the three genuinely reusable outside a single query.
  • Recursive logic requires a CTE (WITH RECURSIVE) — neither a plain subquery nor a temp table can self-reference the same way.
  • Pick based on scope and reuse needs, not habit — each of the three is the right tool for a different shape of problem.