必须先用WITH RECURSIVE摊平树形结构再GROUP BY,因递归CTE内禁止聚合操作且逻辑上无法向上累加;否则下级数据无法纳入父级统计。

直接说结论:分组后的递归层级汇总,不能先 GROUP BY 再递归,必须先用 WITH RECURSIVE 把整棵树摊平(带 root_id、level、path),再对外层结果按 root_id 或 level 分组聚合。否则下级数据根本进不了父级统计。
为什么不能在递归CTE里写GROUP BY
递归 CTE 本身禁止聚合操作:GROUP BY、SUM()、DISTINCT 全都不允许出现在递归查询体内。数据库会直接报错,比如 PostgreSQL 提示 “recursive query must not contain aggregates”,MySQL 8.0 则抛出类似 ERROR 1221 (HY000): Incorrect usage of UNION and ORDER BY 的语法错误。
更关键的是逻辑错误:即使绕过语法限制(比如用子查询包装),递归过程中的每一轮只看到“当前层”数据,无法把子节点的聚合值回传给父节点——这违背了递归向下钻取后向上累加的本质需求。
- 递归体只能
JOIN上一层输出的列,不能引入新表或聚合结果 - 别名必须严格一致,例如锚点选了
id, parent_id, level,递归体也得返回同名同类型三列 -
UNION ALL是必须的,UNION会去重并拖慢性能,而树形结构天然无重复行
正确写法:先摊平,再分组
核心是两步分离:第一步用递归 CTE 构建“节点→所有下属”的宽表映射;第二步对这个中间结果做普通聚合。例如统计每个部门及其全部下级员工的总薪资:
root_id
- MySQL 5.7 不支持
WITH RECURSIVE,硬套会报错ERROR 1349 (HY000): View's SELECT contains a reference to a temporary table - PostgreSQL 和 SQL Server 要求递归列类型完全一致,比如
id是INT,就不能在锚点里写CAST('1' AS TEXT) - 如果部门表有环(A → B → A),不加
WHERE dt.level 可能卡住或超时
容易被忽略的索引和 NULL 处理
递归性能瓶颈几乎全在 parent_id 查找上。即使 id 有主键索引,parent_id 没索引会导致第二层开始全表扫描——10万行的表,第三层可能就慢到不可接受。
另外,摊平后的结果常含大量 NULL:比如根节点的 parent_id 是 NULL,但这是合法数据;而 ROLLUP 产生的 NULL 是占位符。导出到 BI 工具前若不做区分,Excel 里全显示为空白,排序却乱序。
- 必须单独为
parent_id建索引:CREATE INDEX idx_dept_parent_id ON departments(parent_id); - 真实业务中,用
COALESCE(parent_id, -1)统一处理根节点NULL,避免和聚合占位符混淆 - 如果前端需要展示层级缩进,别靠应用层拼空格,直接在 CTE 里加
REPEAT(' ', level - 1) || name AS indented_name
最常踩的坑是把递归当成黑盒,以为加个 WITH RECURSIVE 就自动能汇总。实际上它只负责“展开”,真正的分组逻辑必须放在 CTE 外面,且依赖展开后新增的维度字段(如 root_id)才能成立。没有这个中间层,所谓“层级汇总”只是镜花水月。










