MySQL 8.0+ 才支持 Recursive CTE,PHP 和 Eloquent 仅转发 SQL,不实现递归逻辑;必须用 DB::select() 配合命名占位符安全执行,禁用字符串拼接,注意锚点与递归成员语法限制及深度防护。

MySQL 8.0+ 才支持 Recursive CTE,PHP 本身不实现它
PHP(包括 Eloquent)不会“实现” Recursive CTE —— 它只是把 SQL 语句发给数据库执行。真正支持递归查询的是 MySQL 8.0+、PostgreSQL 14+ 或 SQL Server。Laravel 9+ 开始通过 DB::select() 或原生查询才能用上 WITH RECURSIVE,Eloquent 的 whereHas() 或 with() 都不生成递归 SQL。
常见误判是以为加个 ->with('children') 就能查出整棵树 —— 实际只查一层,深度无限时会 N+1;或者用 withDepth()(仅限 Laravel 10+ 的 Tree trait),但它底层仍是多次查询或闭包表,不是 CTE。
- 必须确认数据库版本:
SELECT VERSION();,低于 8.0 直接放弃 Recursive CTE 方案 - Laravel 迁移中不能用
DB::statement('WITH RECURSIVE ...')建表或设约束,CTE 是查询级语法,非 DDL -
DB::select()返回数组,不是 Eloquent 模型实例,关联、访问器、强制类型转换全部失效
Laravel 中安全使用 WITH RECURSIVE 的写法
不要拼接用户输入进 CTE 查询,避免 SQL 注入。用 DB::select() + 命名占位符,配合 DB::raw() 构建结构化递归逻辑:
$results = DB::select("
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories
WHERE id = ?
UNION ALL
SELECT c.id, c.name, c.parent_id, t.depth + 1
FROM categories c
INNER JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree
ORDER BY depth, id
", [$rootId]);-
UNION ALL必须用,UNION会去重并拖慢性能,树结构天然无重复 ID - 起始查询(anchor member)必须在
UNION ALL之前,且不能含ORDER BY/LIMIT/GROUP BY - 递归成员(recursive member)里禁止出现聚合函数、
GROUP BY、HAVING、窗口函数 —— 否则 MySQL 报错Recursive reference to CTE 'tree' is not allowed - 深度限制建议手动加:在
SELECT里加t.depth < 10条件,防止无限循环(比如数据有环)
替代方案对比:什么时候不该硬上 Recursive CTE
如果只是展示三级分类导航、后台菜单折叠展开,用 Eloquent + 闭包表(Closure Table)或嵌套集(Nested Set)更稳;Recursive CTE 在大表(>10 万节点)上没索引支持时容易超时或爆内存。
立即学习“PHP免费学习笔记(深入)”;
- 闭包表:额外一张
category_ancestors表存所有祖先路径,WHERE descendant_id = ?一次查完,适合读多写少 - 父ID + 递归 PHP 处理:用
Category::with('children')查全表再用 PHP 递归组装,内存占用高但兼容所有数据库 -
withDepth()(Laravel 10+):依赖parent_id字段,自动计算层级,但仍是 N+1,需配合withCount('children')控制展开 - MySQL 5.7 或 SQLite 用户:Recursive CTE 不可用,别折腾适配,换方案
调试 Recursive CTE 查询的三个关键点
报错信息往往模糊,重点看三处:
- 错误
ERROR 3636 (HY000): Recursive query aborted after 1001 iterations→ 检查数据是否有环,加depth < 20限制 - 结果为空但语法没错 → 锚点查询没命中(比如
WHERE id = ?的$rootId不存在),先单独跑锚点 SQL 确认 - 查出重复行 → 用了
UNION而非UNION ALL,或递归条件写成c.id = t.parent_id(方向反了)
递归深度、数据闭环、方向逻辑这三点漏掉任一个,CTE 就会静默失败或返回脏数据,比 ORM 关联难 debug 得多。



















