Level 16: Structuring with WITH (CTE)
Updated on
Pro · level 16 of 20. Goal: Split a complex query into named, readable steps.
Topics: WITH CTE several CTEs reusing a CTE
Lesson
When a query chains several calculations, subqueries nest inside each other and reading becomes hard: you have to read from the inside out. WITH lets you write the same steps from top to bottom, like a recipe.
WITH name AS (SELECT …) creates a named temporary table, which the query that follows can use like a real table. This is called a CTE (Common Table Expression). It only exists for the duration of the query: nothing is saved in the database.
Example: WITH totals AS (SELECT seller, SUM(amount) AS total FROM sales GROUP BY seller) SELECT seller, total FROM totals WHERE total > 1200. The first step computes the total of each seller; the second filters that result with a plain WHERE, since total is now an ordinary column of totals.
You can chain several CTEs, separated by commas, without repeating WITH: WITH a AS (…), b AS (… FROM a …) SELECT … FROM b. Each CTE can use the ones before it. The final query comes after the last one, without a comma.
A CTE can be read several times in the same query. A monthly CTE that computes the total of each month can be used both in the FROM and in a subquery that computes the average of those totals, without rewriting the calculation.
Method tip: build one CTE at a time. Write the first step, run it on its own and check its result, then add the next step. If the final result is wrong, you know which step to look at.
Mind the punctuation: no semicolon between a CTE and the rest of the query, it would cut it in two.
Syntax
WITH step1 AS (
SELECT …
),
step2 AS (
SELECT …
FROM step1
)
SELECT …
FROM step2;Worked example
WITH totals AS (
SELECT seller, SUM(amount) AS total
FROM sales
GROUP BY seller
)
SELECT seller, total
FROM totals
WHERE total > 1200;Step 1: the total per seller. Step 2: we filter that result like an ordinary table.
| id | seller | region | month | amount |
|---|---|---|---|---|
| 1 | Ana | North | 2025-01 | 300 |
| 2 | Ana | North | 2025-02 | 450 |
| 3 | Ana | North | 2025-03 | 400 |
| 4 | Ben | North | 2025-01 | 500 |
| 5 | Ben | North | 2025-02 | 350 |
| 6 | Ben | North | 2025-03 | 600 |
| 7 | Cleo | South | 2025-01 | 200 |
| 8 | Cleo | South | 2025-02 | 700 |
| 9 | Cleo | South | 2025-03 | 650 |
| 10 | Dan | South | 2025-01 | 400 |
| 11 | Dan | South | 2025-02 | 400 |
| 12 | Dan | South | 2025-03 | 100 |
Example result
| seller | total |
|---|---|
| Ben | 1450 |
| Cleo | 1550 |
Key points
- WITH name AS (query) names a step.
- Several CTEs are separated by commas, without repeating WITH.
- The final query comes after the last CTE.
Common pitfalls
- Putting a semicolon after the CTE's parenthesis cuts the query in two.
- A CTE only exists for the duration of the query.
The level’s 5 exercises
- With a CTE it that contains the employees of the 'IT' department, show the name and hiring date of those hired after 2018 (from January 1st, 2019).
- With a CTE, compute the average of each student (student_id), then show the students whose average is at least 13, rounded to 2 decimal places (avg_grade).
- With a CTE done that computes the hours of the finished tasks (status = 'done') per project, show the name of each project and those hours.
- With a CTE monthly that computes the total sales of each month, show the months whose total is above the average of the monthly totals.
- Compute the balance of each account (sum of its transactions), then the total wealth of each owner (owner), from richest to poorest. Accounts without transactions are ignored.
In SpeedQL, every query is checked straight away: its result is compared with the expected one, then the query is run again on a hidden control database. Each exercise has written hints, to show only if you get stuck.
See also: the cheat sheet card · the SQL exercises with solutions on this topic
Do the level 16 exercises
Free, no sign-up: the lesson and the 5 exercises open right in your browser.
← Previous level: EXISTS and correlated subqueries · Next level: Window functions: ranking →