SQL Tutorial › Subqueries & CTEs › CTE (WITH Clause)

SQL CTE (WITH Clause)

AdvancedLesson 43 of 6911 min read5 queries run on MySQL 8.4

A CTE (Common Table Expression) gives a subquery a name at the top of your query with WITH name AS (...). You then use it like a table. CTEs turn a nested, inside-out query into readable steps from top to bottom, which is why they are now the standard way to write analytical SQL, and a favourite interview topic.

Advertisement
In this lesson
  • Write a CTE with WITH ... AS (...)
  • Chain several CTEs, each building on the last
  • Reuse one CTE several times
  • Choose between a CTE, a subquery and a view

CTE syntax

MySQL
WITH cte_name AS (  SELECT ...)SELECT ...FROM cte_name;

A CTE exists only for the one statement that follows it. MySQL supports CTEs from version 8.0.

Sample data

orders · 10 rows
order_idcustomer_idorder_datestatus
100112025-01-05Delivered
100222025-01-17Delivered
100312025-02-02Delivered
100432025-02-14Cancelled
100542025-03-08Delivered
100652025-03-22Shipped
100722025-04-10Delivered
100812025-04-28Pending
100932025-05-15Delivered
101052025-06-01Delivered
order_items · 16 rows
order_idproduct_idquantity
100121
100165
100241
100311
100321
100451
1005610
100574
100632
100681
100711
100842
100882
100921
101063
101072
products · 9 rows (some columns)
product_idproduct_namecategoryprice
1Wireless MouseElectronics799
2Mechanical KeyboardElectronics3499
3USB-C HubElectronics1999
4Office ChairFurniture8999
5Standing DeskFurniture18999
6Notebook PackStationery249
7Gel PensStationery199
8Desk LampFurniture1499
9Monitor ArmFurniture2799

A first CTE

Order totals, then the orders above 5,000:

SQL
WITH order_totals AS (  SELECT oi.order_id, SUM(oi.quantity * p.price) AS total  FROM order_items oi  JOIN products p ON p.product_id = oi.product_id  GROUP BY oi.order_id)SELECT order_id, totalFROM order_totalsWHERE total > 5000ORDER BY total DESC;
ResultMySQL 8.4
order_idtotal
100820996
100418999
10028999
10065497
4 rows in set
How this query works, step by step
  1. WITH order_totals AS (...) defines a named result: one row per order with its total.
  2. The main query reads order_totals like a table.
  3. WHERE total > 5000 works directly on the computed column, no repeated expression needed.

Compare with the same query as a derived table: the logic is identical, but you read it from the inside out.

SQL
SELECT order_id, totalFROM (  SELECT oi.order_id, SUM(oi.quantity * p.price) AS total  FROM order_items oi JOIN products p ON p.product_id = oi.product_id  GROUP BY oi.order_id) AS order_totalsWHERE total > 5000ORDER BY total DESC;
ResultMySQL 8.4
order_idtotal
100820996
100418999
10028999
10065497
4 rows in set

Several CTEs in steps

Separate CTEs with commas. Each one may use the ones defined before it. Customer spend, the average spend, and who is above it:

SQL
WITH order_totals AS (  SELECT o.order_id, o.customer_id, SUM(oi.quantity * p.price) AS total  FROM orders o  JOIN order_items oi ON oi.order_id = o.order_id  JOIN products p     ON p.product_id = oi.product_id  WHERE o.status <> 'Cancelled'  GROUP BY o.order_id, o.customer_id),customer_spend AS (  SELECT customer_id, SUM(total) AS spend, COUNT(*) AS orders  FROM order_totals  GROUP BY customer_id),average AS (  SELECT AVG(spend) AS avg_spend FROM customer_spend)SELECT c.customer_name, cs.orders, cs.spend, ROUND(a.avg_spend) AS avg_spendFROM customer_spend csJOIN customers c ON c.customer_id = cs.customer_idCROSS JOIN average aWHERE cs.spend > a.avg_spendORDER BY cs.spend DESC;
ResultMySQL 8.4
customer_nameordersspendavg_spend
Ananya Sharma33003810653
1 row in set
How this query works, step by step
  1. order_totals: the value of each non-cancelled order.
  2. customer_spend: built from order_totals, one row per customer.
  3. average: one row with the average customer spend.
  4. The final query joins the steps and keeps customers above average.
  5. Each step can be tested on its own by temporarily selecting from it, which makes debugging easy.

Reusing a CTE

A CTE can be referenced more than once in the same query. Each department’s payroll compared with the largest department payroll:

MySQL
WITH dept_pay AS (  SELECT dept_id, SUM(salary) AS payroll  FROM employees  WHERE dept_id IS NOT NULL  GROUP BY dept_id)SELECT d.dept_id, d.payroll,       ROUND(100 * d.payroll / (SELECT MAX(payroll) FROM dept_pay), 1) AS pct_of_largestFROM dept_pay dORDER BY d.payroll DESC;
ResultMySQL 8.4
dept_idpayrollpct_of_largest
1392000100.0
220700052.8
313300033.9
47800019.9
4 rows in set

CTE with UPDATE and DELETE

In MySQL a CTE can also feed an UPDATE or DELETE. Restock every product that sold 3 or more units:

MySQL
WITH best_sellers AS (  SELECT product_id FROM order_items GROUP BY product_id HAVING SUM(quantity) >= 3)UPDATE productsSET stock = stock + 100WHERE product_id IN (SELECT product_id FROM best_sellers); SELECT product_id, product_name, stock FROM products ORDER BY product_id;
ResultMySQL 8.4
mysql> WITH best_sellers AS (
Query OK, 5 rows affected
mysql> SELECT product_id, product_name, stock FROM products ORDER BY product_id
product_idproduct_namestock
1Wireless Mouse120
2Mechanical Keyboard145
3USB-C Hub0
4Office Chair112
5Standing Desk5
6Notebook Pack400
7Gel Pens350
8Desk Lamp130
9Monitor Arm18
9 rows in set

CTE vs subquery vs view

CTEDerived table (subquery)View
Namedyesalias onlyyes
Readable top to bottomyesno (inside out)yes
Reusable in the same queryyesnoyes
Lives after the querynonoyes, stored in the database
Recursiveyes (Recursive CTE)nono

Exercise: Category revenue share

Write a CTE cat_rev with revenue per product category, then show each category’s revenue and its percentage of total revenue.

Expected resultMySQL 8.4
categoryrevenuepct
Furniture5049369.9
Electronics1609322.3
Stationery56767.9
3 rows in set
Hint 1

In the main query, divide by (SELECT SUM(revenue) FROM cat_rev).

Quick quiz

1. How long does a CTE exist?

2. How do you define two CTEs?

3. Can a later CTE use an earlier one?

CTE FAQ

What is a CTE in SQL?

A Common Table Expression is a named temporary result defined with WITH name AS (SELECT ...) and used by the statement that follows.

What is the difference between a CTE and a subquery?

They often produce the same plan. A CTE is named, defined up front, can be referenced several times and can be recursive; a subquery is written inline where it is used.

Does MySQL support CTEs?

Yes, from MySQL 8.0, including recursive CTEs.

Are CTEs faster than subqueries?

Not inherently. MySQL may merge a CTE into the outer query or materialize it once; the main benefit is readability.