Self Join是同一张表与自身关联,适合查父子关系因父/子节点共存于一表;需用别名区分,建parent_id索引,深度查询依赖数据库递归CTE。
什么是Self Join,为什么它适合查父子关系
Self Join 是指一张表和自己做 JOIN,本质是把同一张表当作两个逻辑上不同的表(比如
和
)来关联。在父子结构中,父节点和子节点都存在同一张表里(如
和
),必须用 Self Join 才能分别拿到“父行”和“子行”。
不推荐用子查询或应用层递归拼接,因为:
子查询嵌套深了性能断崖式下降
应用层遍历无法利用数据库索引,IO 成倍增加
缺少层级控制(比如只查 3 层)时逻辑易出错
怎么写基础的两层父子查询(Parent → Child)
核心是让子表的
关联父表的
:
注意点:
别名
和
必须明确,否则字段歧义(如
到底是父还是子?)
如果要包含根节点(
),改用
,但
字段会为 NULL
索引一定要建在
上,否则 JOIN 全表扫描
如何查任意深度的层级(比如祖→父→子→孙)
标准 SQL 不支持递归,但主流数据库提供了扩展:
PostgreSQL:用
MySQL 8.0+:同样支持
SQL Server:用
+
SQLite:支持
(3.8.3+)
以 PostgreSQL 为例,查从根到第 3 层的所有路径:
容易踩的坑:
忘记加终止条件(如
),导致无限循环报错
递归分支没限制,数据有环(A→B→A)时直接崩溃,上线前务必检查
是否合法
MySQL 8.0 以下不支持递归 CTE,只能靠应用层或临时表模拟
查某节点的所有祖先或所有后代,哪个更高效
查所有
后代
(向下遍历)通常更快:
可以用单次递归 CTE 完成,索引走
即可
大多数业务场景(如展示分类树、权限继承)都需向下展开
查所有
祖先
(向上遍历)难点在于:
每次 JOIN 都要回溯一层,深度越大 JOIN 越多
若没有对
建主键索引,性能恶化明显
更稳妥的做法是:在写入时冗余存储路径(如
),查祖先就用
实际选型要看更新频次——路径冗余适合读多写少,递归 CTE 适合写频繁但层级浅(≤5)的场景。
递归不是银弹,深度超过 10 层时,数据库执行计划可能退化,这时候该考虑把树结构缓存到 Redis 或改用闭包表(Closure Table)模型。
t1t2idparent_idparent_ididSELECT p.name AS parent_name, c.name AS child_name
FROM categories p
JOIN categories c ON c.parent_id = p.id;pcidparent_id IS NULLLEFT JOINc.*parent_idWITH RECURSIVEWITH RECURSIVECTEUNION ALLWITH RECURSIVEWITH 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
JOIN tree t ON c.parent_id = t.id
WHERE t.level < 3 -- 控制最大深度
)
SELECT * FROM tree;t.level < 3parent_idparent_ididpath = '/1/5/12/'LIKE '/1/%'