with recursive可一次性查出整棵树,前提是表含parent_id等父子外键;锚点须精准定位起点(如where id = ?),不可笼统用parent_id is null;递归join方向必须正确(向下查用e.parent_id = s.id),且仅限inner join;终止依赖join失败或where过滤,层级与路径需在cte内用level和concat等计算生成。

能直接用 WITH RECURSIVE 一次性查出整棵树,不需要应用层循环,前提是表结构满足父子同表、有明确的外键指向(如 manager_id 或 parent_id)。
锚点怎么写:必须精准定位起点
锚点不是随便选一行,而是要明确你要展开的“根节点”。比如查某个员工的所有下属,锚点就得锁定该员工;查某个部门的所有子部门,锚点就得用 WHERE id = ? 或 WHERE name = ? 精确匹配。
- 常见错误:用
WHERE parent_id IS NULL当锚点——这会拉出所有顶层节点,不是“从某节点开始遍历”,而是“查整棵树” - 如果起点不存在,整个 CTE 返回空结果,不会报错,容易被忽略
- 锚点查询必须能走索引,否则第一轮就慢;确保
id、name等条件字段有单列或联合索引
递归部分怎么 JOIN:方向别搞反
向下查子节点(如查下属、子部门)时,JOIN 条件是 e.manager_id = s.id,即“子表的上级 ID 等于上一轮结果的 ID”;向上查父节点(如面包屑路径)时,则是 c.id = p.parent_id,即“父表的 ID 等于上一轮结果的父 ID”。
- 方向写反会导致结果为空,且无提示——数据库只是没匹配到数据,不是语法错误
- 必须用
INNER JOIN,不能用LEFT JOIN,否则可能引入 NULL 行,破坏递归逻辑 - 递归成员里不能出现聚合函数(如
COUNT)、窗口函数(如ROW_NUMBER()),MySQL 8.0 不支持
终止条件藏在哪:靠 JOIN 和 WHERE 共同控制
递归没有显式的 WHILE 或 STOP WHEN,它的终止完全依赖两件事:一是 JOIN 没有匹配行,二是递归成员里的 WHERE 子句过滤掉后续轮次的数据。
- 比如向下查子节点时,最后一层员工的
manager_id不再出现在上层id列中,JOIN 失败,递归自然停止 - 但如果你在递归部分加了
WHERE e.status = 'active',那中途遇到一个 inactive 员工,它和它的全部子树都会被跳过 - 最大递归深度受
max_recursion_depth限制(默认 1000),超深树要提前设大:SET SESSION max_recursion_depth = 3000;
层级和路径怎么带出来:用变量式字段累加
单纯查 ID 和名字不够用,业务通常需要知道深度、完整路径、编号等。这些都得在 CTE 内部用计算字段生成,不能留到外层 SELECT 再处理。
- 层级
depth:锚点设为1,递归部分写s.depth + 1 - 路径拼接:锚点用
CAST(name AS CHAR(500)) AS path初始化,递归部分用CONCAT(p.path, ' / ', e.name) - 注意
CONCAT遇到任一参数为NULL就返回NULL,向上查路径时,顶层节点的parent_id是NULL,所以锚点的path必须显式赋值,不能依赖字段拼接
最易被忽略的是递归成员中隐含的性能假设:每轮 JOIN 都要扫描上一轮结果集大小 × 子表大小。如果某中间层节点有几百个子节点,而子表没对 manager_id 建索引,第二轮就会全表扫——这时候慢的不是语法,是索引缺失。











