CTEs & recursive CTEs
Readable queries with WITH, chaining CTEs, and walking hierarchies and series with WITH RECURSIVE.
A Common Table Expression (CTE) gives a name to a subquery so you can use it like a table in the rest of the statement. CTEs make complex queries read top to bottom, like a recipe. Recursive CTEs go further: they can walk trees and graphs, and generate series of numbers or dates. CTEs need MySQL 8.0 or newer.
Your first CTE#
Which customers spend more than the average customer?
WITH customer_totals AS (...) defines a temporary result that exists only for this statement. We used it twice, once in FROM and once in the subquery, without repeating the aggregation. That's something a derived table can't do.
Chaining several CTEs#
Separate CTEs with commas. Each one can use the ones defined before it:
Compare that with the same logic as nested derived tables: you'd have to read it inside-out. CTEs let you build and test a query step by step. Write the first CTE, SELECT * FROM it to check the result, then add the next.
You can name the columns in the header instead of aliasing them inside: WITH monthly (month, revenue) AS (SELECT ...).
CTEs in UPDATE and DELETE#
A WITH clause can come before UPDATE and DELETE statements too (or inside INSERT ... SELECT):
Recursive CTEs#
A recursive CTE refers to itself. It has two parts joined by UNION ALL:
- An anchor member: the starting row(s).
- A recursive member: a
SELECTthat uses the CTE's previous output to produce the next rows.
MySQL repeats step 2 until it produces no new rows. The simplest example counts from 1 to 5:
Generating a date series
Reports often need a row for every day, even days with no sales. Generate the calendar, then LEFT JOIN the data onto it:
Walking a hierarchy
This is the killer feature. A self join goes one level at a time, but a recursive CTE walks a tree of any depth:
Two details matter:
CAST(name AS CHAR(200))in the anchor. The anchor fixes each column's type and length, so without the cast,pathwould be limited to the length of the longest root name and longer paths would fail with a "Data too long" error.- The recursive member joins
categoriestotree, which holds the rows found in the previous iteration.
To find everything under one category, change the anchor to WHERE id = 2. To find the ancestors of a category (its breadcrumb trail), reverse the join: start at the leaf and join c.id = t.parent_id.
Safety limits#
A recursive CTE without a working stop condition would loop forever. MySQL protects you:
Raise the limit for big series with SET SESSION cte_max_recursion_depth = 10000;. You can also add LIMIT inside the recursive CTE (MySQL 8.0.19+). For hierarchies with cycles (A → B → A), track the visited ids in a path string and stop when an id repeats.
CTE, derived table, view or temporary table?#
What's next#
CTEs organise queries. Window functions let you compute running totals, rankings and comparisons with the previous row, all without collapsing rows the way GROUP BY does.
Check your understanding
Quick quiz
1.Which keyword starts a common table expression?
2.What are the two parts of a recursive CTE?
3.What protects you from a recursive CTE that never stops?
Finished reading?
Mark this lesson complete to track your progress.