直接用SELECT COUNT(*)自关联会漏层级,因自关联仅支持固定深度(如两次JOIN最多查三层),无法处理任意深度的链式嵌套;必须用递归CTE(MySQL 8.0+/PostgreSQL)或手动多层LEFT JOIN+IF判空模拟,且需防范循环引用与NULL父节点等脏数据问题。

为什么直接用 SELECT COUNT(*) 自关联会漏层级?
树形菜单的层级不是静态可数的,比如根节点下有子节点,子节点还有子节点——这种“链式嵌套”必须递归展开。但多数 SQL 方言(如 MySQL 5.7、SQL Server 2016 以前)不支持 WITH RECURSIVE,只能靠自关联模拟递归。常见错误是只连一次:JOIN menu m2 ON m2.parent_id = m1.id,这最多算出两层,第三层就断了。
实际要覆盖任意深度,得手动展开多层 JOIN,每层对应一个可能的深度位置:
- 第 1 层:自身(
id = parent_id或parent_id IS NULL) - 第 2 层:直接子节点(1 次 JOIN)
- 第 3 层:孙子节点(2 次 JOIN)
- 以此类推——你预设的最大深度决定了 JOIN 次数
MySQL 5.7 下用 LEFT JOIN 展开 4 层并取最大深度
假设菜单表叫 menu,字段为 id、name、parent_id,要统计每个节点的子树最大深度(含自身):
SELECT
m1.id,
m1.name,
1 + IF(m2.id IS NOT NULL, 1, 0)
+ IF(m3.id IS NOT NULL, 1, 0)
+ IF(m4.id IS NOT NULL, 1, 0) AS depth
FROM menu m1
LEFT JOIN menu m2 ON m2.parent_id = m1.id
LEFT JOIN menu m3 ON m3.parent_id = m2.id
LEFT JOIN menu m4 ON m4.parent_id = m3.id
GROUP BY m1.id, m1.name;
注意这里没用 COUNT(),因为 COUNT(m2.id) 会把 NULL 当 0 计,但 LEFT JOIN 后一行变多(笛卡尔效应),直接 COUNT 会重复计数。改用 IF(... IS NOT NULL, 1, 0) 逐层判空更可靠。
- 每多一层,就得加一个
LEFT JOIN和一个IF判断 - 深度上限硬编码在 SQL 里,改深度就得改语句结构
- 如果某节点深度超 4,结果就截断——必须按业务预期设足够大的层数
PostgreSQL 或 MySQL 8.0+ 推荐用 WITH RECURSIVE
上面的手动 JOIN 在深度 > 5 时极易出错且难维护。WITH RECURSIVE 是更干净的解法,以 PostgreSQL 为例:
WITH RECURSIVE menu_tree AS ( -- 锚点:根节点(parent_id IS NULL) SELECT id, name, parent_id, 1 AS depth FROM menu WHERE parent_id IS NULL <p>UNION ALL</p><p>-- 递归:找所有子节点,depth + 1 SELECT m.id, m.name, m.parent_id, mt.depth + 1 FROM menu m INNER JOIN menu_tree mt ON m.parent_id = mt.id ) SELECT id, name, MAX(depth) AS max_depth FROM menu_tree GROUP BY id, name;</p>
关键点:
- 锚点必须明确终止条件(通常是
parent_id IS NULL),否则无限循环 -
MAX(depth)是因为一个节点可能被多个路径访问到(比如有环时),但正常树形结构下每节点只出现一次,SELECT *也能看到完整路径 - MySQL 8.0+ 支持同语法;SQL Server 用
WITH menu_tree AS ( ... )不加RECURSIVE关键字
容易忽略的边界:循环引用和 NULL 父节点处理
真实业务数据常有脏数据:某个节点的 parent_id 指向自己,或指向一个不存在的 id,导致递归死循环或结果缺失。
- 在
WITH RECURSIVE中,PostgreSQL 默认有 100 层递归限制,超限报错ERROR: infinite recursion detected;MySQL 8.0 可设cte_max_recursion_depth系统变量 - 为防循环,可在递归 CTE 中加路径数组记录已访节点(PostgreSQL 示例):
ARRAY[id] AS path,再用WHERE NOT m.id = ANY(path) -
parent_id为NULL的不一定是根节点——有些系统用 0 或 -1 表示根,查前先确认业务约定
深度统计不是纯算法题,它卡在数据质量、SQL 版本、性能容忍度三者的交界上。写之前先 SELECT COUNT(*) FROM menu WHERE parent_id = id 扫一遍自环,比调通语句更重要。










