跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

怎么在SQL中通过子查询实现父子层级数据的汇总_利用递归CTE或多层嵌套

普通子查询无法处理无限层级树形结构,因其仅支持单层嵌套;递归CTE是唯一标准解法,由锚点和递归成员组成,需正确传递root_id以实现子树级汇总。 为什么不能直接用普通子查询做无限层级汇总 普通子查询(比如
SELECT ... FROM (SELECT ...) AS t
)只能展开一层嵌套,无法表达“当前节点的父节点还有父节点”这种动态路径。一旦层级超过2层(如部门→子部门→孙部门→曾孙部门),硬写多层
JOIN
或嵌套子查询会迅速失控:SQL变长、可读性归零、修改成本极高,且根本无法适配深度不确定的树形结构。 常见错误现象包括:
ERROR: missing FROM-clause entry for table "t1"
(别名作用域混乱)、
column "id" does not exist
(子查询字段未显式命名)、或查出重复/遗漏的中间节点。 父子关系必须有明确的自关联字段,如
parent_id
指向同表的
id
根节点通常用
parent_id IS NULL
或
parent_id = 0
标识,需提前确认业务规则 MySQL 5.7 及更早版本不支持递归 CTE,强行写会报错
Recursive common table expression is not supported
PostgreSQL / SQL Server / MySQL 8.0+ 怎么写递归CTE 递归 CTE 是唯一能自然表达任意深度树形遍历的标准方案。它由两部分组成:锚点(anchor,即根节点)和递归成员(recursive member,即“找当前节点的所有子节点”),用
UNION ALL
连接。 关键点在于:递归成员的
FROM
必须引用 CTE 自身别名,且连接条件必须让子节点的
parent_id
匹配上一层的
id
—— 这个方向反了就会查成空集。
WITH 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;

level
字段不是必须的,但强烈建议加上,便于后续按层级聚合(如
SUM(amount) GROUP BY level
) 递归深度默认可能受限(PostgreSQL 默认100,SQL Server 默认100,MySQL 8.0 默认1000),超深树要加
MAXRECURSION 0
(SQL Server)或调整配置 如果需要汇总每个节点下的全部子孙数据(比如统计部门总人数),得在递归后用窗口函数或再次
JOIN
,不能在递归体内部
SUM()
MySQL 5.7 或 SQLite 等不支持递归CTE时怎么妥协 没有递归 CTE 就没法真正“动态”处理未知深度,只能接受一个现实:预设最大层级数(比如最多4级),然后用固定层数的
LEFT JOIN
展开。这不是优雅解法,但能跑通、易理解、兼容性好。 核心思路是把树“拍平”成宽表:每一级都作为独立字段,例如
lvl1_id
,
lvl1_name
,
lvl2_id
,
lvl2_name
…再用
CASE WHEN
或
COALESCE
做汇总逻辑。
SELECT 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 JOIN
,6层就得写5次
JOIN
,维护成本随深度指数上升 不能用
INNER JOIN
,否则会丢掉叶子节点(它们没有子节点,JOIN 后变 NULL) 如果要算“某节点下所有子孙的 amount 总和”,得先用这个宽表生成中间结果,再
GROUP BY root.id
并
SUM()
所有层级的 amount 字段 递归CTE里做汇总时最容易漏掉的一步 很多人写完递归 CTE 就直接
GROUP BY id
,结果发现总数对不上——因为递归结果里每个节点只出现一次,但它的子孙数据还没被拉进来参与计算。真正要做“以某节点为根的子树汇总”,必须让每个子孙行都携带其最顶层根节点的标识(比如
root_id
),然后按该标识分组。 实现方式是在递归 CTE 的锚点里初始化
root_id
字段,在递归成员里透传它:
WITH 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
传递,就只能汇总到当前节点自身,做不到“子树级”聚合。这个字段看起来不起眼,却是父子汇总逻辑成立的前提。

相关文章