datarekha

Common Table Expressions (CTEs)

WITH clauses turn nested SQL into readable, named steps — the single biggest readability win in modern SQL.

7 min read Intermediate SQL Lesson 11 of 27

What you'll learn

  • The WITH name AS (…) syntax and what it gives you
  • Chaining CTEs into named analytical steps
  • Why CTEs beat subqueries for anything multi-step
  • The 'CTEs aren't always inlined' performance gotcha

Before you start

A Common Table Expression is a named subquery written at the top of your statement with one keyword, WITH. Once defined, the rest of the query treats it like a table.

Why it matters: nested subqueries force you to write inside-out — the reader has to find the deepest parentheses, then walk outward. A CTE lets you write top-down, in the order you would describe the problem aloud. We will use these two tables (each order carries its revenue in amount):

users idnamecountry
1AshaIN
2BoCN
3CarlosUS
4DiyaIN
ordersuser_idamount
101130
1021900
103320
104450

The same query, read top-down

WITH user_spend AS (
  SELECT u.id, u.country, COALESCE(SUM(o.amount), 0) AS spend
  FROM   users u
  LEFT JOIN orders o ON o.user_id = u.id
  GROUP BY u.id, u.country
)
SELECT country, AVG(spend) AS avg_spend
FROM   user_spend
GROUP BY country
ORDER BY avg_spend DESC;
countryavg_spend
IN490
US20
CN0

The CTE says, in plain order, “first compute each user’s spend, then average it per country.” Asha’s 930 and Diya’s 50 average to 490 for IN; Bo, who never ordered, is kept at 0 by the LEFT JOIN. That reads exactly how you would explain it to a colleague.

Chaining steps

Several CTEs can sit in one WITH, comma-separated, each referencing the ones before it. Here is a three-step pipeline — spend per user, then the top spender in each country:

WITH user_totals AS (
  SELECT u.id, u.name, u.country, COALESCE(SUM(o.amount), 0) AS total_spend
  FROM   users u
  LEFT JOIN orders o ON o.user_id = u.id
  GROUP BY u.id, u.name, u.country
),
ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY country ORDER BY total_spend DESC) AS rn
  FROM   user_totals
)
SELECT country, name, total_spend
FROM   ranked
WHERE  rn = 1;
countrynametotal_spend
INAsha930
USCarlos20
CNBo0

Each named step stands alone — you can swap the final SELECT for SELECT * FROM user_totals to inspect that stage. That debuggability is something nested subqueries simply cannot give you. The same shape builds the classic funnel: a signed_up CTE, a purchased CTE, a repeat_buyers CTE, each a stage, collapsed into one row of counts at the end (here: 4 signed up, 3 purchased, 1 repeat buyer).

CTESubquery
Reusable by name in the same queryYesNo (must repeat)
Reads top-downYesNo (inside-out)
Can be recursiveYes (WITH RECURSIVE)No
Inspectable in isolationYesHard

The reuse point is the real payoff: define user_spend once and reference it three times (country average, the global average, and their difference) — with subqueries you would paste the same five lines three times, and the next reader (probably you, in three months) would not thank you.

Practice

Quick check

0/3
Q1What does a CTE give you that a subquery doesn't?
Q2Why do CTEs make multi-step analysis more debuggable?
Q3On MySQL 8 or older Postgres, what's a CTE performance gotcha?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

FAQCommon questions

Questions about this lesson

What is a CTE in SQL?

A CTE (Common Table Expression), written with the `WITH` keyword, is a named temporary result set you can reference within a single query. It makes complex queries readable by breaking them into named, sequential steps.

What's the difference between a CTE and a subquery?

Both are temporary, but a CTE is named up front with `WITH`, which makes it reusable within the query and easier to read, while a subquery is inline. A recursive CTE can also reference itself, which a subquery cannot.

Are CTEs faster than subqueries?

Usually not by themselves — most databases optimise them similarly, and a CTE's main benefit is readability. Some engines materialise a CTE, which can help or hurt, so check the query plan when performance matters.

Practice this in an interview

All questions
What does CTE materialization mean, when does it help versus hurt, and how do you control it in PostgreSQL?

Materialization means the database executes the CTE once, stores its result in a temporary structure, and re-reads that structure for each reference. It helps when the CTE is expensive and referenced many times, but hurts when the optimizer needs to push predicates through the CTE — which it cannot do across a materialization boundary.

When should you choose a CTE over a subquery, and does a CTE always offer a performance advantage?

CTEs improve readability and allow a named result to be referenced multiple times in one query, but most databases inline them during optimization, so they carry no inherent performance advantage over equivalent subqueries. Only databases that materialize CTEs by default — like older PostgreSQL versions — show a measurable difference.

How do you chain multiple CTEs in a single query, and what are the scoping and execution rules you need to know?

Multiple CTEs are defined in a single WITH clause separated by commas; each CTE can reference any CTE defined before it in the list but not one defined after. The entire WITH block is logically evaluated before the final SELECT, and each CTE is visible throughout the rest of the query including inside other CTEs.

How does a recursive CTE work, and how would you use one to walk an employee-manager hierarchy?

A recursive CTE has an anchor member that seeds the recursion and a recursive member that joins back to the CTE itself; the engine iterates until no new rows are produced. It is the standard SQL approach for querying trees and graphs such as org charts, bill-of-materials, and threaded comments.

Related lessons

Explore further

Skip to content