递归CTE必须先完成层级展开,再在外层查询中分组汇总;递归成员内禁止GROUP BY、SUM()等聚合操作,仅锚点与递归部分通过UNION ALL合并,终止条件为递归结果为空。

递归CTE必须在GROUP BY之前展开完整层级
直接在递归查询里写 GROUP BY 或 SUM() 会报错,因为递归成员中禁止聚合函数、DISTINCT、GROUP BY。真正能汇总的时机只有一个:递归结束后的外层查询。
典型错误是把 JOIN employees 放进递归体内部,比如:
WITH RECURSIVE dept_tree AS ( SELECT id, name, parent_id FROM departments WHERE parent_id IS NULL UNION ALL SELECT d.id, d.name, d.parent_id FROM departments d JOIN dept_tree t ON d.parent_id = t.id JOIN employees e ON e.dept_id = d.id -- ❌ 这里语法非法,且逻辑错位 )
正确路径是两步分离:
- 先用递归生成“每个部门 → 它的所有直接/间接下属部门”映射(每行含
root_id和descendant_id) - 再用这个结果去
JOIN员工表,最后按root_id分组求和
MySQL 8.0+ 才支持 WITH RECURSIVE,5.7 及更早版本无法运行
如果你执行 WITH RECURSIVE 报错 “You have an error in your SQL syntax”,大概率是 MySQL 版本太低。5.7 不识别 RECURSIVE 关键字,强行用只会让整个 CTE 被当成普通子查询,导致只查出直属下级,漏掉所有间接下属。
验证方式很简单:
SELECT VERSION();
若返回值低于 8.0.0,就别硬套递归写法。替代方案包括:
- 应用层递归(查一层、再查下一层,拼结果)
- 预计算路径字段(如
path = '/1/5/12/'),用LIKE匹配 - 使用闭包表(closure table)预先存好所有祖先-后代关系
按 root_id 分组才是关键,不是按原始部门ID
很多人写完递归后仍按 departments.id 分组,结果只是把每个部门自己的员工加起来,完全没体现“上级汇总下级”的业务含义。
核心在于递归时必须携带根节点标识。例如:
WITH RECURSIVE dept_tree AS ( -- 锚点:把每个部门都当作潜在根节点开始扩展 SELECT id AS root_id, id AS dept_id FROM departments UNION ALL -- 递归:从 root_id 出发,往下找所有子孙 dept_id SELECT t.root_id, d.id FROM departments d INNER JOIN dept_tree t ON d.parent_id = t.dept_id ) SELECT d.name, SUM(e.salary) AS total_salary FROM dept_tree t JOIN departments d ON t.root_id = d.id JOIN employees e ON e.dept_id = t.dept_id GROUP BY t.root_id, d.name;
这里 t.root_id 是分组依据,不是 t.dept_id —— 否则就退化成普通分组。
SQL Server 和 PostgreSQL 的递归语法基本一致,但终止机制有差异
三者都支持 WITH RECURSIVE(PostgreSQL 和 SQL Server 可省略 RECURSIVE 关键字),但对无限递归的防护策略不同:
- PostgreSQL 默认无深度限制,需手动加
WHERE level <= 10防死循环 - SQL Server 默认最大递归 100 层,超限报错
Msg 530,可用OPTION (MAXRECURSION n)调整 - MySQL 8.0 用
cte_max_recursion_depth系统变量控制,默认 1000
实际写的时候,建议始终显式加深度限制,尤其当组织树可能含环(比如 A→B→C→A)时,不加限制会导致查询卡死或超时。

















