递归CTE不能在递归成员中使用SUM()或GROUP BY,因其仅用于展开层级关系;聚合必须移至外层,通过JOIN业务表后GROUP BY实现。

递归CTE不能直接写SUM()或GROUP BY
这是最常被踩的坑:一在递归支里加SUM()或GROUP BY,立刻报错或结果翻倍。递归CTE的作用只是展开层级关系,输出的是扁平的“节点+祖先/后代”行集,不是聚合结果。数据库不允许在递归定义块里做聚合,因为每轮迭代行数不固定,聚合语义不成立。
常见错误写法:WITH RECURSIVE tree AS ( SELECT id, parent_id, sales FROM orgs WHERE parent_id IS NULL UNION ALL SELECT p.id, p.parent_id, SUM(c.sales) -- ❌ FROM orgs p JOIN tree c ON p.id = c.parent_id GROUP BY p.id, p.parent_id -- ❌)
- 锚点和递归成员都只能做JOIN、WHERE、字段计算(如
level + 1),不能出现任何聚合函数 - 想算“某节点及其所有子节点的总销量”,必须把聚合逻辑移到CTE外层
- CTE里该加的只有层级标识(
level)、路径(path)或祖先ID(ancestor_id)这类辅助字段
外层聚合时JOIN条件决定汇总范围
CTE展开后,怎么让订单销量“自动计入父类”?关键不在CTE里,而在外层JOIN的关联方式。
- 如果CTE是向下展开(每个节点列出其所有后代),用
ON t.descendant_id = orders.category_id:每个订单匹配到它自己及所有上级分类 - 如果CTE是向上展开(每个节点列出其所有祖先),用
ON t.ancestor_id = orders.category_id:效果一样,订单仍能命中整条路径上的所有祖先 - 要排除父级、只算本级,得在外层加
WHERE t.level = 0(或t.id = t.ancestor_id),但这已脱离树形汇总目的 - 漏掉
orders.category_id上的索引,JOIN会全表扫描,树越深性能越差
HAVING必须放在外层GROUP BY之后
想过滤“子树总预算超过100万的部门”,不能在CTE里写HAVING——CTE根本不支持HAVING。正确路径是:CTE展开 → 外层JOIN业务数据 → GROUP BY节点ID → 再用HAVING过滤。
-
HAVING只能跟在GROUP BY后面,且作用于聚合后的分组结果 - 例如查“下属总开销超50万的部门”,外层SQL类似:
SELECT t.dept_id, SUM(o.cost) AS total_cost FROM dept_tree t JOIN orders o ON t.descendant_id = o.dept_id GROUP BY t.dept_id HAVING SUM(o.cost) > 500000 - 别试图在CTE里用
WHERE模拟过滤——那只会提前剪枝,漏掉本应参与汇总的子节点
别拿ROLLUP当递归CTE用
GROUP BY ... WITH ROLLUP和递归CTE完全不是一回事。ROLLUP只按列顺序做静态小计,不理解parent_id,也不处理动态深度树。
- 把
category_id放GROUP BY第一位,ROLLUP生成的是“所有类目总和”,不是“每个一级类目的子类总和” - 它不会识别父子关系,更不会生成缩进或路径字符串
- 真要分层展示带缩进,必须靠CTE里的
REPEAT(' ', level)或拼path,ROLLUP做不到 - 混用两者会导致逻辑混乱:一个按维度顺序归并,一个按树结构展开,结果对不上
实际执行时,最容易被忽略的是CTE展开后的数据量——一棵10层深、每层100个节点的树,展开后可能有近10亿行中间结果。这时候外层JOIN和GROUP BY的性能压力远超预期,索引和深度限制(如MySQL的cte_max_recursion_depth)必须提前设好。










