递归cte是轮次迭代追加而非循环:第0轮为锚点结果,每轮仅用上一轮新增行驱动join并追加新行,终止于某轮输出为空;禁止在递归成员中使用group by、order by等因破坏“单轮输入→单轮输出”线性流。

递归CTE不是循环,是轮次迭代追加
MySQL 的 WITH RECURSIVE 不会把递归部分编译成 while 循环或函数调用,而是按“轮次(iteration)”执行:每一轮用上一轮的**全部输出行**作为驱动数据去 JOIN,再把新产出的匹配行追加进全局结果集。锚点结果是第 0 轮;第 1 轮用第 0 轮结果驱动;第 2 轮只用第 1 轮新增的行驱动——不是用所有历史行反复扫一遍。
常见误解是“对锚点每一行单独递归一次”,实际不是。比如锚点返回 3 行,第 1 轮 JOIN 可能生成 15 行;第 2 轮就拿这 15 行去 JOIN,可能产出 60 行。终止条件是某轮递归查询返回空集,而不是某行不满足 WHERE。
递归成员里为什么不能写 GROUP BY 或 ORDER BY
因为 MySQL 在执行模型上强制要求“单轮输入 → 单轮输出”的线性迭代流。一旦在递归分支里加 GROUP BY,就会破坏这个前提——聚合会混洗、压缩多行输入为更少行输出,导致下一轮无法正确以“逐行驱动 JOIN”方式继续。
你如果看到报错 Recursive member cannot contain GROUP BY 或 Recursive reference in a subquery is not allowed,基本就是踩了这条红线。
- ✅ 正确做法:把聚合移到最终
SELECT中,比如统计每层节点数:SELECT level, COUNT(*) FROM dept_tree GROUP BY level - ❌ 错误写法:在递归部分写
SELECT ..., COUNT(*) FROM ... GROUP BY ... - 同理,
ORDER BY、LIMIT、DISTINCT、窗口函数、子查询中引用 CTE 都被禁止
中间结果集存在哪?为什么看不到临时表
MySQL 内部用一个隐式临时表缓存每轮结果,但这个表不暴露给用户,也不支持 EXPLAIN 直接查看结构。它只在 CTE 执行生命周期内存在,且每轮只保留当前轮新增行用于下一轮驱动(不是全量快照)。
这意味着你无法在递归过程中做跨轮聚合、去重或排序——那些操作必须等整个迭代结束、所有轮次结果合并完成后,在最终 SELECT 里做。
如果你需要调试中间状态,唯一办法是把某一轮结果抽出来单独查,比如先跑锚点 + 第 1 轮逻辑(去掉 UNION ALL 和递归部分),再人工比对。
cte_max_recursion_depth 控制的是轮次数,不是行数
默认值是 1000,意思是最多允许执行 1000 轮迭代。当第 1001 轮准备启动时,MySQL 就直接中断并报错:Recursive query aborted after 1000 iterations。这不是“查了 1000 行就停”,哪怕某轮只产出 1 行,也算 1 轮。
调大这个值要小心:SET SESSION cte_max_recursion_depth = 2000 可临时放宽,但真正该做的是在递归部分加硬性 WHERE 过滤,比如 WHERE level ,从逻辑上切断无效路径,避免依赖深度阈值兜底。
容易被忽略的一点:即使你加了 level ,但如果数据存在环(比如 A→B→C→A),MySQL 仍可能因无法收敛而耗尽轮次——所以业务数据本身必须保证 DAG 结构,或额外加路径防重逻辑(如用 JSON_ARRAY_APPEND 记录已遍历 ID)。











