递归CTE必须向上展开祖先路径,不能向下查子孙;本质是为每条业务记录打上所有上级标签后按ancestor_id分组聚合,CTE内禁止SUM或GROUP BY,否则报错“列数不匹配”,正确做法是CTE只生成映射关系,外层再聚合。

递归CTE必须向上展开祖先路径,不能向下查子孙
自底向上聚合的本质是:给每个叶子节点(比如某个门店、某个成本中心)打上它所有上级节点的标签,然后按这些上级ID分组求和。错误做法是让CTE从根往下递归——那样得到的是“某部门的所有下属”,不是“某门店所属的所有上级”。
关键在JOIN方向和字段继承:
- 锚点选业务表中的实际记录(如
sales里的region_id),不是组织表里的parent_id IS NULL - 递归部分用
ON r.id = rt.parent_id(子找父),不是ON c.parent_id = t.id(父找子) - 必须保留原始业务记录的
amount等值字段,且不在递归体里改动它 -
root_id字段此时应叫ancestor_id更准确,表示“这条销售记录能向上归属到哪个祖级区域”
外层GROUP BY必须作用于递归结果的ancestor_id,不能在CTE里聚合
递归CTE只负责生成“一条销售记录 → 多个上级ID”的映射关系。一旦你在递归支里写SUM(amount)或GROUP BY,数据库立刻报错“列数不匹配”或返回空结果。
正确结构是两层嵌套:
- 内层:
WITH RECURSIVE region_tree AS ( ... )输出每条销售对应的ancestor_id和amount - 外层:
SELECT ancestor_id, SUM(amount) FROM region_tree GROUP BY ancestor_id
如果还想要区域名称,再JOIN一次regions表——别试图在CTE里直接JOIN regions并取name,那会让递归结构变复杂且易出错。
容易漏掉的索引和去重陷阱
没建索引时,INNER JOIN region_tree rt ON s.region_id = rt.id可能触发全表扫描,尤其当销售表有百万行、区域树有上千层时,查询直接卡死。
重复计算比想象中更常见:
- 一个门店属于“华东→上海→浦东”,但数据里同时存在“华东→上海→张江”和“华东→江苏→苏州”,若区域表本身有环(比如浦东父ID误设为张江),递归会把同一笔销售算多次
- 解决方法不是加
DISTINCT,而是先用ROW_NUMBER() OVER (PARTITION BY s.id, rt.ancestor_id ORDER BY rt.level)标记首次归属,再WHERE rn = 1 - MySQL不支持
SEARCH DEPTH FIRST,得靠WHERE rt.level 硬限制;SQL Server必须加<code>OPTION (MAXRECURSION 20)
path字段拼接要统一分隔符,否则LIKE匹配失效
用path做模糊匹配(如cost_center_code LIKE t.path || '%')时,分隔符混用会导致父子关系断裂。例如锚点写CAST(code AS VARCHAR) AS path,递归部分却用ct.path + '/' + c.code(SQL Server)而业务数据里存的是'01.02.03',结果永远不匹配。
实操建议:
- 所有数据库统一用点号
.作分隔符,避免斜杠/(URL转义风险)、下划线_(LIKE通配符冲突) - MySQL用
CONCAT(ct.path, '.', c.code),PostgreSQL用ct.path || '.' || c.code,SQL Server用ct.path + '.' + c.code - path字段声明长度至少500,防止深层级截断(如省→市→区→街道→社区→网格,10级×50字符=500)
真正难的不是写出递归语句,而是确认你的区域/组织表里没有id = parent_id这种自环,也没有A→B→C→A这种隐蔽闭环——它们不会让SQL报错,只会让SUM()结果翻倍且难以定位。

















