SQL CTE (WITH Clause) Explained with Examples
A common table expression, written with WITH, gives a name to an intermediate result so the main query can use it like a table. CTEs turn a long, nested query into readable steps, which is exactly what interviewers like to see on harder questions.
Examples run in MySQL 8 and PostgreSQL · Updated
In short
WITH name AS (…)defines a named result for one query.- Separate several CTEs with commas; later ones can use earlier ones.
- CTEs make multi-step logic readable and easy to debug.
WITH RECURSIVEwalks hierarchies and sequences.
Naming a step
Instead of repeating a subquery, define it once: WITH dept_avg AS (SELECT dept_id, AVG(salary) …), then join dept_avg in the main query. Each CTE exists only for the statement it belongs to.
A good habit for interview questions: write one CTE per idea ("monthly revenue", "ranked rows", "first order per user"), then select from the last one. You can test each step by selecting from it directly.
The example tables
Every example on this page uses these tables. They are small on purpose, so you can check each result by eye.
employees
| emp_id | name | dept_id | salary | manager_id | hire_date |
|---|---|---|---|---|---|
| 1 | Aarav | 10 | 95000 | NULL | 2021-03-01 |
| 2 | Diya | 10 | 88000 | 1 | 2022-06-15 |
| 3 | Kabir | 10 | 72000 | 1 | 2023-01-10 |
| 4 | Meera | 20 | 64000 | NULL | 2020-11-20 |
| 5 | Rohan | 20 | 58000 | 4 | 2024-02-05 |
| 6 | Isha | 30 | 61000 | NULL | 2022-09-12 |
| 7 | Arjun | 20 | 64000 | 4 | 2023-07-30 |
| 8 | Zoya | NULL | 52000 | NULL | 2024-05-01 |
orders
| order_id | customer_id | order_date | amount |
|---|---|---|---|
| 101 | 1 | 2025-01-05 | 120 |
| 102 | 1 | 2025-01-20 | 80 |
| 103 | 2 | 2025-01-07 | 200 |
| 104 | 1 | 2025-02-02 | 150 |
| 105 | 3 | 2025-02-10 | 90 |
| 106 | 2 | 2025-02-25 | 60 |
| 107 | 3 | 2025-03-01 | 130 |
| 108 | 1 | 2025-03-03 | 40 |
Examples
Join to a computed table
Department averages are computed once in a CTE, then each employee is compared with their own department.
WITH dept_avg AS (
SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
)
SELECT e.name, e.salary, ROUND(a.avg_salary) AS dept_avg
FROM employees e
JOIN dept_avg a ON e.dept_id = a.dept_id
WHERE e.salary > a.avg_salary
ORDER BY e.name;| name | salary | dept_avg |
|---|---|---|
| Aarav | 95000 | 85000 |
| Arjun | 64000 | 62000 |
| Diya | 88000 | 85000 |
| Meera | 64000 | 62000 |
Chaining CTEs
Step one totals revenue per month; step two compares each month with the previous one using LAG.
WITH monthly AS (
SELECT EXTRACT(MONTH FROM order_date) AS month, SUM(amount) AS revenue
FROM orders
GROUP BY EXTRACT(MONTH FROM order_date)
),
ranked AS (
SELECT month, revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS change_from_prev
FROM monthly
)
SELECT month, revenue, change_from_prev
FROM ranked
ORDER BY month;| month | revenue | change_from_prev |
|---|---|---|
| 1 | 400 | NULL |
| 2 | 300 | -100 |
| 3 | 170 | -130 |
Common mistakes
- Using a CTE in a later statement. A CTE only lives for the single statement it starts. Run it again (or create a view) if another query needs it.
- Filtering on a window function directly.
WHERE ROW_NUMBER() OVER (…) = 1is not allowed. Compute the row number in a CTE, then filter in the outer query. - Forgetting the comma. Several CTEs are separated by commas and share one
WITH:WITH a AS (…), b AS (…).
Practise CTEs (WITH clause)
Interview questions that use this concept, easiest first. Each one has the step-by-step visualizer and answer checking.
Monthly Listing Ratings
Average star rating for each listing in each month.
Largest Unique Number
Find the largest number that appears exactly once.
Most Watched Title per Genre
Find the title with the highest total watch time in each genre, keeping ties.
Org Chart Downstream Reports
List everyone who reports, directly or indirectly, to a given manager along with their depth.
Ordered Purchase Funnel
Measure a three-step conversion funnel where the steps must happen in order.
Each Rider's Third Completed Trip
Return the third completed trip for every rider who has at least three.
Want a full set? Work through the Subqueries & CTEs study plan.
Questions people ask
- Do MySQL and PostgreSQL both support CTEs?
- Yes: PostgreSQL for a long time, and MySQL since version 8.0, including WITH RECURSIVE.
- Is a CTE faster than a subquery?
- Usually they perform the same; modern optimisers inline simple CTEs. Use CTEs for readability.