SQLQuokka
Start practicing
QuestionsLearnSandboxFeaturesRoadmapFAQ
SQL Concept

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 RECURSIVE walks 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_idnamedept_idsalarymanager_idhire_date
1Aarav1095000NULL2021-03-01
2Diya108800012022-06-15
3Kabir107200012023-01-10
4Meera2064000NULL2020-11-20
5Rohan205800042024-02-05
6Isha3061000NULL2022-09-12
7Arjun206400042023-07-30
8ZoyaNULL52000NULL2024-05-01

orders

order_idcustomer_idorder_dateamount
10112025-01-05120
10212025-01-2080
10322025-01-07200
10412025-02-02150
10532025-02-1090
10622025-02-2560
10732025-03-01130
10812025-03-0340

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;
Result
namesalarydept_avg
Aarav9500085000
Arjun6400062000
Diya8800085000
Meera6400062000

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;
Result
monthrevenuechange_from_prev
1400NULL
2300-100
3170-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 (…) = 1 is 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.

  1. Monthly Listing Ratings

    Average star rating for each listing in each month.

    Easy✓ Solved
  2. Largest Unique Number

    Find the largest number that appears exactly once.

    Easy✓ Solved
  3. Most Watched Title per Genre

    Find the title with the highest total watch time in each genre, keeping ties.

    Medium✓ Solved
  4. Org Chart Downstream Reports

    List everyone who reports, directly or indirectly, to a given manager along with their depth.

    Medium✓ Solved
  5. Ordered Purchase Funnel

    Measure a three-step conversion funnel where the steps must happen in order.

    Medium✓ Solved
  6. Each Rider's Third Completed Trip

    Return the third completed trip for every rider who has at least three.

    Medium✓ Solved

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.