递归CTE不能在内部使用GROUP BY或聚合函数,必须先展开树形结构再在外层查询中聚合;常见错误是递归部分误加SUM()或GROUP BY,正确做法是CTE只负责层级展开,聚合逻辑移至外层JOIN后执行。
递归 CTE 不能直接聚合,必须把 GROUP BY 和聚合函数(如 SUM()、COUNT())放到外层查询里——这是最常踩的坑,一写错就报错或结果翻倍。
递归 CTE 的作用是展开树形关系(比如所有子孙节点),它输出的是扁平行集,每行代表一个节点+其某一级祖先/后代。数据库不允许在递归定义块里加 GROUP BY,因为递归过程行数动态变化,聚合语义不成立。
常见错误写法:
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) -- ❌ 错误:递归支里不能用 SUM() FROM orgs p JOIN tree c ON p.id = c.parent_id GROUP BY p.id, p.parent_id -- ❌ 错误:递归支里不能用 GROUP BY)
正确做法是:先让 CTE 完整展开树(含层级、路径等字段),再在外层 JOIN 业务表并 GROUP BY。
关键在 JOIN 条件——不是关联到当前节点,而是关联到 CTE 展开后每一行所代表的“归属关系”。
ON t.id = orders.category_id:这样每个订单会匹配到它所属分类及其所有上级,自然实现“子类销量计入父类”ON t.ancestor_id = orders.category_id,效果相同WHERE t.level = 0 或类似条件,但这已脱离树形汇总目的别漏掉索引:orders.category_id 上必须建索引,否则外层 JOIN 可能全表扫描,深树下性能断崖式下跌。
GROUP BY ... WITH ROLLUP 是按列顺序做逐级归并(如 GROUP BY region, dept WITH ROLLUP 生成 region/dept、region/NULL、NULL/NULL 三档),它不理解树结构,也不依赖 parent_id 字段。它适合固定维度的报表小计,不适合组织架构、商品类目这种动态深度的树。
典型误用:
category_id 放在 GROUP BY 第一位,导致顶层小计变成“全部类目总和”,中间层(如“一级类目总和”)反而没了ROLLUP 自动识别父子关系——它不会,它只认列顺序真要分层展示又带缩进,得靠递归 CTE 里拼 REPEAT(' ', level) 或生成 path 字符串,ROLLUP 做不到。
MySQL 默认 @@cte_max_recursion_depth = 1000,SQL Server 默认 100 层,PostgreSQL 无硬限制但可能 OOM。但真正致命的不是“树太深”,而是“写反 JOIN 条件”或“漏终止条件”:
ON t.id = r.parent_id 写成 ON t.parent_id = r.id → 无限循环WHERE parent_id IS NULL)→ 初始集为空,整个 CTE 返回空WHERE r.parent_id IS NOT NULL → 某些数据 parent_id=0 或 -1,导致死循环上线前务必用 SELECT * FROM your_cte LIMIT 20 快速验证是否收敛,别等查生产库卡住才反应过来。