Self Join是同一张表与自身关联,适合查父子关系因父/子节点共存于一表;需用别名区分,建parent_id索引,深度查询依赖数据库递归CTE。

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










