Free tutorials & notes in Hindi & English · Clean code examples · Mobile friendly learning
MySQL + SQL · Lesson 79

Recursive CTE in MySQL

Anchor + Recursive Member + Stop Rule

A recursive CTE contains a nonrecursive anchor followed by a recursive member, normally joined with UNION ALL. WITH RECURSIVE is required when a CTE refers to itself.

WITH RECURSIVE sequence_cte (n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1
  FROM sequence_cte
  WHERE n < 5
)
SELECT n FROM sequence_cte;
Three questions: What starts recursion? What changes on each iteration? What condition guarantees termination?

Verified Number Series 1 to 5

1 2 3 4 5

The anchor emits 1. Each iteration adds one. When the previous n reaches 5, the condition n < 5 is false, so no next row is produced and recursion ends.

Generate five dates

WITH RECURSIVE dates (day_value) AS (
  SELECT DATE('2026-08-14')
  UNION ALL
  SELECT day_value + INTERVAL 1 DAY
  FROM dates
  WHERE day_value < DATE('2026-08-18')
)
SELECT day_value FROM dates;
2026-08-14 2026-08-15 2026-08-16 2026-08-17 2026-08-18

Verified School Hierarchy Dataset

CREATE TABLE org_employees (
  employee_id INT PRIMARY KEY,
  employee_name VARCHAR(60) NOT NULL,
  manager_id INT NULL,
  INDEX (manager_id),
  FOREIGN KEY (manager_id)
    REFERENCES org_employees(employee_id)
);

INSERT INTO org_employees VALUES
(1, 'Principal', NULL),
(2, 'Coordinator', 1),
(3, 'Teacher A', 2),
(4, 'Teacher B', 2),
(5, 'Lab Assistant', 3);

The root has manager_id NULL. Coordinator reports to Principal; two teachers report to Coordinator; Lab Assistant reports to Teacher A.

Traverse the Hierarchy with Depth and Path

WITH RECURSIVE org AS (
  SELECT employee_id, employee_name, manager_id,
         0 AS depth,
         CAST(employee_name AS CHAR(300)) AS path
  FROM org_employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.employee_id, e.employee_name, e.manager_id,
         o.depth + 1,
         CONCAT(o.path, ' > ', e.employee_name)
  FROM org AS o
  JOIN org_employees AS e
    ON e.manager_id = o.employee_id
  WHERE o.depth < 10
)
SELECT employee_id, employee_name, depth, path
FROM org
ORDER BY path;
1 | Principal | 0 | Principal 2 | Coordinator | 1 | Principal > Coordinator 3 | Teacher A | 2 | Principal > Coordinator > Teacher A 5 | Lab Assistant | 3 | Principal > Coordinator > Teacher A > Lab Assistant 4 | Teacher B | 2 | Principal > Coordinator > Teacher B

CAST in the anchor defines enough path width. The depth guard is defensive; the natural recursion also stops at leaves with no children.

Termination, Depth Limits and Cycle Detection

MySQL's cte_max_recursion_depth protects the server; its documented default is 1000. It is not a replacement for a correct stop predicate. During development, also use a reasonable depth condition and execution timeout.

WITH RECURSIVE org AS (
  SELECT employee_id, manager_id,
         CAST(employee_id AS CHAR(300)) AS id_path,
         0 AS depth
  FROM org_employees
  WHERE manager_id IS NULL
  UNION ALL
  SELECT e.employee_id, e.manager_id,
         CONCAT(o.id_path, ',', e.employee_id),
         o.depth + 1
  FROM org AS o
  JOIN org_employees AS e
    ON e.manager_id = o.employee_id
  WHERE o.depth < 20
    AND FIND_IN_SET(e.employee_id, o.id_path) = 0
)
SELECT * FROM org;

The id_path guard rejects a node already visited on that path. Prevent cycles at data-validation time too; defensive queries should not normalize corrupt hierarchy data.

Type Inference and Recursive-Member Restrictions

  • Result column types are inferred from the nonrecursive anchor, not the recursive member.
  • Widen growing strings with CAST in the anchor.
  • The recursive member must reference the CTE once and in its FROM clause.
  • MySQL restricts aggregate/window functions, GROUP BY, ORDER BY and DISTINCT inside the recursive SELECT member.
  • UNION DISTINCT between anchor and recursive members can remove duplicate rows, but it is not a substitute for a designed cycle rule.

Performance, EXPLAIN and Practice

  • Index hierarchy links such as manager_id.
  • Keep projected recursive rows narrow.
  • Cap depth according to the domain.
  • Use EXPLAIN; recursive cost estimates are per iteration and total iterations are data-dependent.
  • Test roots, leaves, multiple roots, orphaned rows and cycles.

Practice: generate 1–10; generate a seven-day calendar; return only descendants of Coordinator; add a cycle in a disposable copy and confirm the guard; calculate maximum observed depth outside the recursive member.

Official References

Syntax and behavior were checked against the official MySQL 8.4 manual. Verify plans and limits on your own server before production use.

Frequently Asked Questions

What is a recursive CTE?
It is a CTE whose recursive query member refers to its own name, repeatedly producing rows until the recursive member returns no new rows.
What are anchor and recursive members?
The anchor produces initial rows without referencing the CTE. The recursive member uses rows from the previous iteration to produce the next rows.
Why is a termination condition essential?
Without it, recursion can run until the server depth or time guard stops the statement, wasting resources and possibly failing.
What is the default cte_max_recursion_depth?
MySQL documents a default value of 1000 recursion levels, but applications should still include a correct logical stop condition.
Why should text be CAST wider in the anchor?
Recursive CTE result types come from the nonrecursive anchor. A path that grows during recursion can truncate or error unless the anchor defines enough width.
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

💻 Live Code Editor

This page's programs are ready here — run them, edit them, and learn. No installation needed.
Powered by OneCompiler. The code loads into the editor automatically — press Run to see the output. If the editor does not open, open it in a new tab.