普通JOIN无法查出完整树形结构,因为其只能固定连接层数,而树形数据深度不确定;递归CTE是唯一标准解法,必须用WITH RECURSIVE定义,严格包含锚点(查顶层节点)和递归成员(通过JOIN自身实现“从父找子”),并手动维护level字段以防无限循环。

为什么直接用 JOIN 无法查出完整树形结构
因为普通 JOIN 只能固定层数,而组织架构、评论回复链、分类目录这类数据深度不确定——你不知道某条记录底下嵌套了 3 层还是 12 层。硬写多层 LEFT JOIN 不仅难维护,还会让查询计划爆炸式膨胀,甚至触发数据库的连接数限制。
递归 CTE 是唯一标准解法,但必须满足两个前提:WITH RECURSIVE 关键字(PostgreSQL/MySQL 8.0+/SQL Server/Oracle 都支持),以及明确的锚点(anchor)和递归成员(recursive member)划分。
WITH RECURSIVE 的基本结构怎么写才不出错
错误写法常见于把锚点和递归部分混在一起,或漏掉终止条件。正确结构必须严格分两段,用 UNION ALL 连接:
WITH RECURSIVE tree AS ( -- 锚点:起始节点(比如 root_id = 1) SELECT id, name, parent_id, 0 AS level FROM categories WHERE id = 1 <p>UNION ALL</p><p>-- 递归成员:基于上一轮结果继续找子节点 SELECT c.id, c.name, c.parent_id, t.level + 1 FROM categories c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree;
- 锚点查询不能依赖递归表
tree,否则报错recursive reference must be in UNION ALL - 递归成员里只能用
INNER JOIN或LEFT JOIN,但右表必须是tree本身,且关联条件要指向“父→子”方向(即c.parent_id = t.id) -
level字段不是必须的,但强烈建议加上,既能控制深度(加WHERE level ),也能避免无限循环
如何防止无限递归导致查询卡死
真实数据中常有脏数据:A 的父节点是 B,B 的父节点又误设为 A。数据库不会自动检测环,只会一直跑直到超时或达到最大递归深度。
- PostgreSQL 默认限制
max_recursion_depth = 100,可临时调高:SET search_path TO DEFAULT; SET statement_timeout = '30s'; - MySQL 8.0+ 用
cte_max_recursion_depth系统变量,会话级设置:SET SESSION cte_max_recursion_depth = 500; - 更稳妥的做法是在递归成员中加入路径追踪,用数组或字符串记录已访问 ID:
ARRAY[t.id] AS path(PG),然后在WHERE中加NOT c.id = ANY(t.path)
连接父子节点时,怎么把父名、层级、路径都查出来
单纯查 ID 关系没意义,业务通常需要展示“一级分类 > 二级分类 > 三级分类”这样的路径,或渲染缩进菜单。关键是在递归过程中累积字段:
WITH RECURSIVE tree AS (
SELECT
id, name, parent_id,
name AS full_path,
0 AS level
FROM categories
WHERE parent_id IS NULL -- 根节点
<p>UNION ALL</p><p>SELECT
c.id, c.name, c.parent_id,
t.full_path || ' > ' || c.name, -- 拼接路径
t.level + 1
FROM categories c
INNER JOIN tree t ON c.parent_id = t.id
)
SELECT id, name, full_path, level FROM tree;-
full_path必须在锚点和递归成员中都定义,类型要一致(PG 用text,MySQL 用CONCAT()) - 如果要按树形顺序排序(根→子→孙),不能只靠
ORDER BY level,得用ORDER BY full_path或额外维护一个sort_path字段(如'001.002.005') - MySQL 对字符串拼接长度敏感,默认
group_concat_max_len = 1024,路径长了会被截断,需提前调大
递归 CTE 的难点不在语法,而在对数据闭环和路径累积的理解。一旦锚点选错、连接方向反了、或者忘了加深度防护,轻则结果为空,重则拖垮整个库。动手前先用 EXPLAIN 看执行计划里有没有 “Recursive Union” 节点,比盲目调参管用得多。

















