Intermediate: combining and changing data · Subqueries, CTEs and set operations · topic 29 of 62 · 35 min
Common table expressions (WITH)
Explanation
Why name a subquery?
Derived tables work, but they nest inside out: to understand the query you start in the middle and read outwards. With two or three steps, the brackets pile up and the logic hides. A common table expression, or CTE, moves each step to the top of the query and gives it a name. You read the query like a recipe: first this, then this, then the answer.
WITH totals AS (
SELECT c.name, SUM(oi.qty * oi.price) AS total
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status <> 'Cancelled'
GROUP BY c.id, c.name
)
SELECT name, total
FROM totals
WHERE total > (SELECT AVG(total) FROM totals)
ORDER BY total DESC;
-- Ravi Kumar | 6497
-- Asha Verma | 4678The shape is WITH name AS (query), followed by the main statement. The CTE exists only while this one statement runs; it is not saved anywhere. Using a CTE twice, as above, is something a derived table cannot do without copying the whole subquery.
Several CTEs, each building on the last
Separate the CTEs with commas, and write WITH only once. A later CTE may read an earlier one.
WITH dept_stats AS (
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees
WHERE dept_id IS NOT NULL
GROUP BY dept_id
),
company AS (
SELECT AVG(salary) AS avg_all FROM employees
)
SELECT d.name, s.headcount,
ROUND(s.avg_salary) AS avg_salary,
ROUND(s.avg_salary - c.avg_all) AS vs_company
FROM dept_stats AS s
JOIN departments AS d ON d.id = s.dept_id
CROSS JOIN company AS c
ORDER BY vs_company DESC;
-- Engineering | 4 | 140000 | 40000
-- Finance | 2 | 86500 | -13500
-- Sales | 3 | 80667 | -19333
-- HR | 1 | 80000 | -20000Monthly revenue, then filter
WITH monthly AS (
SELECT substr(o.order_date, 1, 7) AS month,
SUM(oi.qty * oi.price) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status <> 'Cancelled'
GROUP BY month
)
SELECT month, revenue FROM monthly WHERE revenue >= 3000 ORDER BY month;
-- 2025-01 | 3779
-- 2025-03 | 7167CTE, subquery or view?
| Tool | Lives for | Good for |
|---|---|---|
| Subquery / derived table | One spot in one query | A small step used once |
| CTE (WITH) | One statement | Several named steps, or a step used more than once |
| View (module 10) | Saved in the database | A query many reports reuse, under a name everyone can see |
Does a CTE make queries faster?
Not by itself; it is mainly about readability. How it runs depends on the engine. Materialising means computing the CTE once and storing the result in a temporary table; inlining means pasting it into the main query as if it were a subquery, so the planner can optimise everything together. PostgreSQL before version 12 always materialised CTEs, which sometimes made them slower. Since version 12 it inlines a CTE used once, materialises one used more than once, and lets you choose with AS MATERIALIZED or AS NOT MATERIALIZED. SQLite (3.35+) accepts the same hints. SQL Server treats a CTE like an inline view and does not store it. MySQL supports CTEs from version 8.0. If speed matters, check the query plan (module 10).
Common mistakes
- Writing WITH before every CTE. Write WITH once, then name AS (...), name2 AS (...), separated by commas.
- A comma after the last CTE, before the main SELECT: a syntax error.
- Expecting the CTE to exist in the next statement. It lives for one statement only; use a view or a table to keep it.
- A semicolon between the CTE and its SELECT, which ends the statement early. The main query follows the closing bracket directly.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Common table expressions (WITH) · 1/6
Nested subqueries are read from the inside out, which gets hard. A CTE gives each step a name at the top.
Prefer a video by a teacher? Find free videos on “sql cte with clause common table expression tutorial” on YouTube ↗
In real life: A monthly business review query
Analysts at startups and large companies alike write their monthly reports as a chain of CTEs: clean the orders, total them per month, compare each month with the previous one, and finally pick what goes on the slide. Each step has a name, so a reviewer can run one CTE at a time and check its numbers. Tools such as dbt are built around this step-by-step style.
WITH delivered AS (
SELECT * FROM orders WHERE status = 'Delivered'
),
per_customer AS (
SELECT customer_id, COUNT(*) AS orders FROM delivered GROUP BY customer_id
)
SELECT c.name, p.orders
FROM per_customer AS p
JOIN customers AS c ON c.id = p.customer_id;Practice (0/3 done)
Write a two-step CTE report. Step 1 (a CTE named totals): each customer's name and total spent (sum of qty * price), ignoring Cancelled orders. Step 2: return name and total for customers whose total is above the average of all those totals, highest total first.
Use a CTE named monthly that computes revenue (sum of qty * price, ignoring Cancelled orders) per month, where month is substr(order_date, 1, 7). Then return month and revenue for the months with revenue of at least 3000, in month order.
Use two CTEs. dept_stats: for employees who have a department, dept_id, headcount and avg_salary per department. company: the average salary of all employees. Return the department name, headcount, avg_salary rounded to a whole number (ROUND(x)), and vs_company (department average minus company average, rounded to a whole number), largest vs_company first.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Recursive CTEsSubqueries, CTEs and set operations · same module
- Subqueries, CTEs and window functionsBackend Development · SQL in depth
- Top-N per group and de-duplicationWindow functions
- Views and materialized viewsIndexes, query plans and views
- UPDATE and DELETE safelyChanging data, transactions and ACID
- Joining three or more tablesJOINs
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
How long does a CTE exist?
Answer: Only while the one statement that defines it runs
It is part of one statement. A view is the saved version.
How do you define two CTEs in one query?
Answer: WITH a AS (...), b AS (...) SELECT ...
WITH once, CTEs separated by commas, no comma before the main query.
Which is a real advantage of a CTE over a derived table?
Answer: It can be referenced more than once by name, and steps read from top to bottom
Readability and reuse within the statement; speed depends on the engine.
In PostgreSQL 12 and later, how is a CTE that is referenced only once usually run?
Answer: Inlined into the main query, unless you write AS MATERIALIZED
PostgreSQL 12 changed the default; earlier versions always materialised.
You want a named query that many reports and users can reuse across sessions. What fits best?
Answer: A view
A view is saved in the database (module 10); a CTE lives for one statement.
What is a CTE?Related
Answer: A named temporary result written with WITH, used by the query after it
WITH name AS (...) defines a temporary named result that exists only for that query.
A correlated subquery is slow on a table of 10 lakh rows. What is a good first step?Related
Answer: Check the plan, index the linking column, or rewrite it as a join to a grouped derived table
In principle it runs once per outer row; an index or a rewrite helps.
In PostgreSQL, a subquery used with = returns 3 rows. What happens?Related
Answer: An error: more than one row returned by a subquery
PostgreSQL, MySQL and SQL Server raise an error; SQLite quietly uses the first row.
From Scalar subqueries
In SQL Server, a recursive CTE goes deeper than 100 levels. What happens by default?Related
Answer: It stops with an error; OPTION (MAXRECURSION n) changes the limit
SQL Server's default MAXRECURSION is 100.
From Recursive CTEs
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Subqueries, CTEs and set operations” before its exam: Recursive CTEs, UNION, INTERSECT and EXCEPT.
- Module 6 exam: subqueries, CTEs and set operations · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
- Then the next module, Window functions. 62 topics left in the course before the final exam.