MySQL Recursive CTE with Examples
Learn MySQL recursive CTE syntax with number sequences and hierarchy traversal, including stop conditions and recursion limits.
A recursive common table expression (CTE) is a WITH query that refers to its own name. In MySQL, recursive CTEs can generate a sequence or traverse hierarchical data such as an employee reporting tree. They are supported in MySQL 8.0 and later.
Every recursive CTE needs an initial result, a recursive query that reads rows from the CTE, and a condition that eventually stops producing rows. For the general WITH syntax and nonrecursive examples, see MySQL CTEs.
MySQL recursive CTE syntax
Use WITH RECURSIVE before the CTE definitions:
WITH RECURSIVE cte_name (column_name, ...) AS (
-- Anchor member: create the initial row or rows
SELECT ...
UNION ALL
-- Recursive member: produce the next rows
SELECT ...
FROM cte_name
WHERE stop_condition
)
SELECT ...
FROM cte_name;
The first query is the anchor member. It does not read from the CTE and creates the starting rows. The recursive member reads the previous iteration’s rows and produces the next ones. Recursion ends when the recursive member returns no rows.
The column names and types are based on the anchor member. Use a column list to make the names clear, and make sure the anchor expressions have suitable types for all values produced later.
Generate a number sequence
This query returns the integers from 1 through 10:
WITH RECURSIVE numbers (n) AS (
SELECT 1
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < 10
)
SELECT n
FROM numbers
ORDER BY n;
The anchor starts with 1. Each recursive iteration adds one to the prior value. Once n reaches 10, WHERE n < 10 is false, so the next iteration returns no rows and the recursion stops. The outer ORDER BY controls the displayed order.
Traverse an employee hierarchy
A common hierarchy design stores each employee’s manager in the same table. Here is a small example table:
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(50) NOT NULL,
manager_id INT NULL
);
INSERT INTO employees (employee_id, employee_name, manager_id) VALUES
(1, 'Avery Chen', NULL),
(2, 'Morgan Lee', 1),
(3, 'Jordan Kim', 1),
(4, 'Taylor Ray', 2),
(5, 'Casey Park', 3);
To return employee 1 and everyone who reports to them, directly or indirectly, use a recursive CTE:
WITH RECURSIVE org_chart (employee_id, employee_name, manager_id, depth) AS (
SELECT employee_id, employee_name, manager_id, 0
FROM employees
WHERE employee_id = 1
UNION ALL
SELECT e.employee_id, e.employee_name, e.manager_id, org_chart.depth + 1
FROM employees AS e
JOIN org_chart
ON e.manager_id = org_chart.employee_id
)
SELECT employee_id, employee_name, manager_id, depth
FROM org_chart
ORDER BY depth, employee_id;
The anchor selects the starting employee. Each recursive pass finds employees whose manager_id matches an employee from the previous pass. depth is 0 for the starting employee and increases for each level below them. This example assumes the employee hierarchy has no reporting cycles.
Stop conditions and recursion limits
Always design the recursive member to stop. In the number example, WHERE n < 10 provides a clear stopping condition. In the hierarchy example, the query stops when no additional employees report to the current level. A cycle in hierarchical data can keep producing rows, so check that the relationships are valid before running a traversal.
MySQL also limits recursive depth as a safety measure. The default value of cte_max_recursion_depth is 1000. Reaching that limit is not a substitute for a correct stop condition; increasing it can allow a runaway query to run longer.
Common recursive CTE errors
- Missing
RECURSIVE: If the CTE refers to itself, start the clause withWITH RECURSIVE. - No termination condition: Make the recursive member eventually return no rows. For hierarchies, avoid cycles or explicitly guard against them.
- Unexpected duplicate rows:
UNION ALLretains rows from every iteration.UNION DISTINCTremoves duplicate complete rows, but rows that differ in a column such asdepthare still distinct. - Sorting or aggregation in the recursive member: MySQL restricts constructs such as
ORDER BY,GROUP BY, and aggregate functions in the recursive query. Apply ordering or aggregation in the outer query when possible.
For the full list of rules, see the MySQL 8.0 Reference Manual: Recursive Common Table Expressions.