递归CTE中禁止在递归成员内使用SUM()或COUNT()等聚合函数,因锚点与递归部分列结构必须严格一致;正确做法是先用递归展开全树(含root_id、level等字段),再在外层按root_id或level分组聚合。

不能直接在递归 CTE 内部用 GROUP BY 或聚合函数,否则会报错“递归锚点和递归成员列数不匹配”——这是最常卡住人的地方。
为什么递归CTE里不能写SUM()或COUNT()
递归 CTE 的锚点(初始查询)和递归成员(UNION ALL 后面那部分)必须严格保持字段数量、名称、类型、顺序一致。一旦你在锚点里写 SELECT id, name, 1 as level,递归部分就不能突然加个 SUM(sales),否则两段输出结构对不上,数据库直接拒绝执行。
- 错误示例:
SELECT id, name, SUM(value) FROM ... UNION ALL SELECT c.id, c.name, SUM(c.value) FROM ...→ 报错 - 正确路径:先用递归 CTE 把整棵树“摊平”,再在外层套
GROUP BY - MySQL 8.0+、PostgreSQL、SQL Server 都遵循这一规则,不是 MySQL 特有
怎么写出能用于聚合的递归结果集
关键是在递归过程中带上 root_id 字段,把每个节点都标记回它所属的根节点。这样后续按 root_id 分组,就能统计“以某组织为顶层的所有下属汇总值”。
- 锚点只选原始字段 +
id AS root_id+0 AS depth(根节点自身) - 递归部分用
ON c.parent_id = t.id(子找父),并继承t.root_id,不是c.id - 避免用
parent_id IS NULL当根条件——有些业务用parent_id = 0或parent_id = id,得先确认
WITH RECURSIVE tree AS ( SELECT id, org_name, parent_id, id AS root_id, 0 AS depth FROM t_m_org WHERE parent_id IS NULL -- 注意这里是否真代表根 UNION ALL SELECT c.id, c.org_name, c.parent_id, t.root_id, t.depth + 1 FROM t_m_org c INNER JOIN tree t ON c.parent_id = t.id ) SELECT root_id, COUNT(*) AS total_subunits, SUM(org_level) AS level_sum FROM tree GROUP BY root_id;
聚合结果比预期大?大概率是重复计数
递归展开后,一个叶子节点可能被多个路径包含(比如 A→B→C 和 A→D→C,C 被算两次)。如果业务要求“每个节点只算一次”,就得去重。
- PostgreSQL 可用
DISTINCT ON (root_id, id)先取唯一组合 - 通用方案:外层加
ROW_NUMBER() OVER (PARTITION BY root_id, id ORDER BY depth),然后WHERE rn = 1 - 更根本的解法:查之前先确认数据无环,否则递归可能无限展开或重复收口
MySQL 5.7 怎么办?没有递归CTE
只能靠变量模拟,但风险高、不可靠、难调试。典型写法依赖 @ids 变量拼接,配合 FIND_IN_SET() 迭代查找子节点。
- 无法处理深度 > 20 的树(变量长度限制、性能断崖)
- 并发查询时变量会互相污染,必须加
SELECT ... FOR UPDATE或改用应用层递归 - 不支持在子查询中赋值(MySQL 8.0+ 才放开),所以这类 SQL 不能嵌套进视图或复杂 JOIN
真正要长期维护的系统,升级到 MySQL 8.0+ 并用标准递归 CTE 是唯一可扩展的选择。临时救急可以,但别当主力方案。

















