VillageSQL is a drop-in replacement for MySQL with extensions.
All examples in this guide work on VillageSQL. Install Now →
WITH keyword. CTEs were introduced in MySQL 8.0 — on MySQL 5.7, use subqueries instead.
Basic CTE Syntax
WITH clause, and each can reference the ones defined before it:
CTEs vs Subqueries
| CTE | Subquery | |
|---|---|---|
| Readability | Named, defined once at the top | Inline, can nest deeply |
| Reuse | Reference the same CTE multiple times | Must repeat the subquery |
| Debugging | Easy to run the CTE definition in isolation | Have to extract and run the subquery manually |
| Performance | Generally the same; optimizer often inlines CTEs | Same |
| MySQL version | 8.0+ | All versions |
Recursive CTEs
A recursive CTE references itself. This is the standard SQL pattern for querying hierarchical data — org charts, category trees, threaded comments.RECURSIVE keyword is required — without it, MySQL doesn’t recognize the self-reference and returns ERROR 1146: Table 'org_chart' doesn't exist. A recursive CTE has two parts joined by UNION ALL:
- Anchor member — the base case (employees with no manager)
- Recursive member — the step case (reports of employees already in the result)
cte_max_recursion_depth (default: 1000) and errors. Guard against this with a depth column and a WHERE depth < N condition.
Frequently Asked Questions
Are CTEs faster than subqueries?
Usually the same. The MySQL optimizer often inlines a CTE the same way it would a subquery. CTEs referenced multiple times may be materialized into a temporary table, which can be faster (avoids re-executing the subquery) or slower (extra I/O). If you suspect unwanted materialization, checkEXPLAIN for “MATERIALIZED” in the extra column.
Can I use CTEs in INSERT, UPDATE, or DELETE?
Yes. CTEs work with DML statements:What’s the recursion depth limit?
The default is 1000 iterations (cte_max_recursion_depth). For deeper hierarchies, increase the session variable: SET SESSION cte_max_recursion_depth = 5000. If you hit the limit on normal data, check that your recursive member has a proper termination condition and that the data doesn’t contain cycles.
Troubleshooting
| Problem | Solution |
|---|---|
ERROR 1064 on WITH syntax | CTEs require MySQL 8.0 — check SELECT VERSION() |
ERROR 1146: Table 'cte_name' doesn't exist | Missing RECURSIVE keyword — add WITH RECURSIVE when the CTE references itself |
ERROR 3577: Recursive query aborted after N iterations | Add a depth counter and WHERE depth < N guard condition |
| CTE referenced in WHERE clause loses rows | Window functions and CTEs are evaluated after WHERE — filter in a subquery around the CTE |
| CTE performing worse than subquery | Check EXPLAIN for “MATERIALIZED” — try inlining the CTE as a subquery to compare |

