因为自连接需手动限定层级,无法动态适配无限嵌套结构,导致深层节点遗漏;真正解决需用WITH RECURSIVE递归查询,其通过锚点、递归和终止条件实现逐层展开,匹配树形数据本质。

为什么自连接查不出完整的树形层级?
因为普通 JOIN 只能拉平两层关系(比如父→子),而真实组织架构或分类目录往往有无限嵌套。用 LEFT JOIN 手动连 3 次表,既难维护又漏深层节点——这不是数据问题,是查询模型没匹配层级本质。
真正要的是“从根往下逐层展开”,必须让 SQL 具备递推能力。主流方案只有两个:WITH RECURSIVE(标准 CTE 递归)和多层 JOIN 别名拼接(不推荐但偶有遗留系统在用)。
CTE 递归必须写的三部分:锚点、递归、终止条件
漏掉任意一部分,查询会报错或死循环。PostgreSQL/SQL Server/MySQL 8.0+ 都支持,但语法细节有差异:
-
锚点(anchor):必须用
WHERE parent_id IS NULL或明确指定根节点(如id = 1),不能写成parent_id = 0(除非业务约定 0 是根) -
递归部分:
JOIN必须关联上一层的id和下一层的parent_id,方向反了就断层 -
终止隐含在 JOIN 结果为空时:不需要显式
MAXRECURSION(SQL Server 除外),但建议加LEVEL 防意外环
示例(以 PostgreSQL 为例):
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 1 AS level FROM categories WHERE parent_id IS NULL -- 锚点:根节点 UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM categories c INNER JOIN tree t ON c.parent_id = t.id -- 关键:子的 parent_id = 父的 id ) SELECT * FROM tree ORDER BY level, id;
用别名自连接硬写 3 层,什么情况下还能凑合用?
仅限层级深度**绝对固定且 ≤ 3** 的场景(比如省-市-区三级行政区),且你无法改数据库版本(如 MySQL 5.7 不支持 CTE)。此时用别名拼接虽丑但有效:
- 每层必须用不同别名(
t1,t2,t3),否则字段混淆 -
LEFT JOIN要用对:t2.parent_id = t1.id,不是t2.id = t1.parent_id - 查 3 层就得写 3 个
JOIN,4 层就要重写 SQL —— 这是硬伤,不是技巧
片段示意:
SELECT t1.name AS province,
t2.name AS city,
t3.name AS district
FROM areas t1
LEFT JOIN areas t2 ON t2.parent_id = t1.id
LEFT JOIN areas t3 ON t3.parent_id = t2.id
WHERE t1.parent_id IS NULL;最容易被忽略的坑:NULL 值处理和索引失效
递归 CTE 中如果 parent_id 允许为 NULL(合理),但没在锚点里过滤,会导致根节点被跳过;反过来,如果业务要求把 parent_id = 0 当根,却写了 IS NULL,结果全空。
- 确保
parent_id字段有索引,否则递归每层都要全表扫描 - MySQL 8.0 的 CTE 默认不物化,大数据量时性能差,可加
/*+ MATERIALIZE */提示(需确认版本支持) - SQL Server 要显式加
OPTION (MAXRECURSION 32767),否则默认只跑 100 层
层级深、节点多、更新频繁的表,递归查询本身就会变慢——这时候该考虑把路径存成 lft/rgt 或 path 字符串,而不是硬扛 CTE。

















