递归cte内不能写group by,因为递归要求每轮输入输出行结构严格一致,而group by会合并行、改变结果集结构,破坏迭代基础;必须先用with recursive展开层级,再在外层聚合。

递归聚合不能在递归 CTE 内部直接做,必须拆成两步:先用 WITH RECURSIVE 展开完整层级路径,再在外层查询中用 GROUP BY 或窗口函数聚合。
为什么递归 CTE 里不能写 GROUP BY
递归 CTE 要求每一层输出的行数、字段名和类型完全一致,而 GROUP BY 会合并行、改变结果集结构,破坏递归引擎的迭代基础。不同数据库表现各异:
- PostgreSQL 直接报错:
recursive query "tree" does not have the form non-recursive-term UNION [ALL] recursive-term - MySQL 8.0 可能静默返回空结果,不提示错误但无数据
- SQL Server 拒绝执行,中断递归过程
本质不是语法限制,而是语义冲突:递归需要“每层输出可作为下层输入”,而聚合结果已不再是原始节点集合。
向上聚合(如统计某员工所有上级的总薪资)的正确写法
以 MySQL 8.0 为例,目标是算出员工 ID = 9(吴涛)的所有直属上级(不含自己)的薪资总和:
WITH RECURSIVE upline AS ( SELECT id, name, manager_id, salary FROM employees WHERE id = 9 UNION ALL SELECT e.id, e.name, e.manager_id, e.salary FROM employees e INNER JOIN upline u ON e.id = u.manager_id ) SELECT (SELECT name FROM employees WHERE id = 9) AS employee, COUNT(*) AS level_count, SUM(salary) AS total_upline_salary FROM upline WHERE id != 9;
关键点:
- 锚点选目标员工自身,递归支向上查其
manager_id对应的人——即“找我的上级,再找我上级的上级” -
WHERE id != 9排除自己,否则会把吴涛的薪资也计入 - 聚合操作全部放在外层
SELECT,CTE 本身只负责生成扁平化的祖先链
向下聚合(如统计某部门及其所有子部门的订单总额)
假设分类表 categories 有 id、name、parent_id,订单表 orders 有 category_id 和 amount:
WITH RECURSIVE subtree AS ( SELECT id, name, parent_id, 0 AS level FROM categories WHERE id = 1 -- 根分类ID UNION ALL SELECT c.id, c.name, c.parent_id, s.level + 1 FROM categories c INNER JOIN subtree s ON c.parent_id = s.id ) SELECT s.id, s.name, SUM(o.amount) AS total_amount FROM subtree s LEFT JOIN orders o ON s.id = o.category_id GROUP BY s.id, s.name;
注意:
- 关联用
LEFT JOIN,确保没订单的分类也能出现(金额为NULL,SUM会转为 0) - 如果只要“本级+所有后代”的汇总,就不要加
WHERE过滤层级;若只要本级,得额外加AND s.level = 0 - MySQL 不支持数组防环,建议业务层保证
parent_id不形成闭环,或加level 防止无限递归
容易被忽略的终止条件与循环风险
递归没有自动环检测机制。一旦出现 A → B → A 这类引用,数据库可能卡死、超时或返回截断结果。必须手动设限:
- 加
level字段并限制最大深度,例如WHERE s.level - PostgreSQL 可用
ARRAY[id]记录路径,配合NOT id = ANY(path)防重入 - MySQL 建议前置校验:先查是否存在自引用或双向引用,或在应用层做拓扑排序
真正难的不是写出能跑的 SQL,而是想清楚你要聚合的是“路径上的每个节点”,还是“每个节点所代表的子树范围”——前者用向上遍历,后者用向下展开,逻辑一错,结果全偏。











