普通子查询无法处理无限层级树形结构,因其仅支持单层嵌套;递归CTE是唯一标准解法,由锚点和递归成员组成,需正确传递root_id以实现子树级汇总。
为什么不能直接用普通子查询做无限层级汇总
普通子查询(比如
)只能展开一层嵌套,无法表达“当前节点的父节点还有父节点”这种动态路径。一旦层级超过2层(如部门→子部门→孙部门→曾孙部门),硬写多层
或嵌套子查询会迅速失控:SQL变长、可读性归零、修改成本极高,且根本无法适配深度不确定的树形结构。
常见错误现象包括:
(别名作用域混乱)、
(子查询字段未显式命名)、或查出重复/遗漏的中间节点。
父子关系必须有明确的自关联字段,如
指向同表的
根节点通常用
或
标识,需提前确认业务规则
MySQL 5.7 及更早版本不支持递归 CTE,强行写会报错
PostgreSQL / SQL Server / MySQL 8.0+ 怎么写递归CTE
递归 CTE 是唯一能自然表达任意深度树形遍历的标准方案。它由两部分组成:锚点(anchor,即根节点)和递归成员(recursive member,即“找当前节点的所有子节点”),用
连接。
关键点在于:递归成员的
必须引用 CTE 自身别名,且连接条件必须让子节点的
匹配上一层的
—— 这个方向反了就会查成空集。
字段不是必须的,但强烈建议加上,便于后续按层级聚合(如
)
递归深度默认可能受限(PostgreSQL 默认100,SQL Server 默认100,MySQL 8.0 默认1000),超深树要加
(SQL Server)或调整配置
如果需要汇总每个节点下的全部子孙数据(比如统计部门总人数),得在递归后用窗口函数或再次
,不能在递归体内部
MySQL 5.7 或 SQLite 等不支持递归CTE时怎么妥协
没有递归 CTE 就没法真正“动态”处理未知深度,只能接受一个现实:预设最大层级数(比如最多4级),然后用固定层数的
展开。这不是优雅解法,但能跑通、易理解、兼容性好。
核心思路是把树“拍平”成宽表:每一级都作为独立字段,例如
,
,
,
…再用
或
做汇总逻辑。
每多一层就要加一个
,6层就得写5次
,维护成本随深度指数上升
不能用
,否则会丢掉叶子节点(它们没有子节点,JOIN 后变 NULL)
如果要算“某节点下所有子孙的 amount 总和”,得先用这个宽表生成中间结果,再
并
所有层级的 amount 字段
递归CTE里做汇总时最容易漏掉的一步
很多人写完递归 CTE 就直接
,结果发现总数对不上——因为递归结果里每个节点只出现一次,但它的子孙数据还没被拉进来参与计算。真正要做“以某节点为根的子树汇总”,必须让每个子孙行都携带其最顶层根节点的标识(比如
),然后按该标识分组。
实现方式是在递归 CTE 的锚点里初始化
字段,在递归成员里透传它:
漏掉
传递,就只能汇总到当前节点自身,做不到“子树级”聚合。这个字段看起来不起眼,却是父子汇总逻辑成立的前提。
SELECT ... FROM (SELECT ...) AS tJOINERROR: missing FROM-clause entry for table "t1"column "id" does not existparent_ididparent_id IS NULLparent_id = 0Recursive common table expression is not supportedUNION ALLFROMparent_ididWITH RECURSIVE tree AS (
-- 锚点:取所有根节点
SELECT id, name, parent_id, 0 AS level
FROM orgs
WHERE parent_id IS NULL
UNION ALL
-- 递归成员:找上一层结果中每个节点的直接子节点
SELECT c.id, c.name, c.parent_id, p.level + 1
FROM orgs c
INNER JOIN tree p ON c.parent_id = p.id
)
SELECT * FROM tree ORDER BY level, id;
levelSUM(amount) GROUP BY levelMAXRECURSION 0JOINSUM()LEFT JOINlvl1_idlvl1_namelvl2_idlvl2_nameCASE WHENCOALESCESELECT
root.id AS root_id,
root.name AS root_name,
lvl2.id AS lvl2_id,
lvl3.id AS lvl3_id,
COALESCE(lvl3.amount, lvl2.amount, root.amount) AS effective_amount
FROM orgs root
LEFT JOIN orgs lvl2 ON lvl2.parent_id = root.id
LEFT JOIN orgs lvl3 ON lvl3.parent_id = lvl2.id
WHERE root.parent_id IS NULL;LEFT JOINJOININNER JOINGROUP BY root.idSUM()GROUP BY idroot_idroot_idWITH RECURSIVE tree AS (
SELECT id, name, parent_id, id AS root_id, 0 AS level
FROM orgs WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, p.root_id, p.level + 1
FROM orgs c
INNER JOIN tree p ON c.parent_id = p.id
)
SELECT
root_id,
COUNT(*) AS descendant_count,
SUM(COALESCE(emp_count, 0)) AS total_employees
FROM tree t
LEFT JOIN orgs o ON t.id = o.id
GROUP BY root_id;
root_id