self join能查树形结构是因为它用两个别名将同一张含id和parent_id的表分别视为父节点和子节点,通过on child.parent_id = parent.id显式关联单层父子关系;它不递归,仅支持固定两级查询,多级需叠加join或改用递归cte。

SELF JOIN 为什么能查树形结构
因为树的每个节点都可能同时是父节点和子节点,SELF JOIN 让一张表能以两个别名(比如 t1 和 t2)同时参与连接,从而把「当前节点」和「它的直接子节点」关联起来。不是靠递归,而是靠显式连接层级关系字段(通常是 parent_id 和 id)。
常见错误是写成 ON t1.id = t2.parent_id 却忘了方向——这实际查的是「t1 是父,t2 是子」;若想查「t1 是子,t2 是父」,就得反过来写 ON t1.parent_id = t2.id。方向错,结果就全乱。
- 必须确保有明确的父子字段,如
id和parent_id,且parent_id允许为NULL(根节点) - 别名命名要有语义,比如用
child和parent比a/b更不容易出错 - 单层
SELF JOIN只能查两级(如部门 + 直属下级),要查多层得叠加多次 JOIN 或改用递归 CTE(MySQL 8.0+/PostgreSQL/SQL Server 支持)
查二级子节点:一次 SELF JOIN 的典型写法
假设表 org 有字段 id、name、parent_id,要列出每个部门及其直属下级部门名称:
SELECT parent.name AS parent_name, child.name AS child_name FROM org AS parent JOIN org AS child ON child.parent_id = parent.id;
注意这里 JOIN 默认是 INNER JOIN,所以根节点(parent_id IS NULL)不会出现在 parent 别名侧,也不会有“无下属”的部门显示在结果里。如果需要保留所有父部门(包括没子部门的),得改用 LEFT JOIN:
-
LEFT JOIN org AS child ON child.parent_id = parent.id→ 父部门全出来,子部门列可能为NULL - 避免
WHERE child.id IS NOT NULL后又用LEFT JOIN,这会退化成INNER JOIN - 如果表数据量大,记得在
parent_id字段上建索引,否则 JOIN 性能会断崖式下跌
查三级结构(祖-父-子):两次 SELF JOIN 的写法与陷阱
要展示「上级部门 → 部门 → 下级部门」三层,就得连三次:grandparent → parent → child:
SELECT g.name AS grandparent_name, p.name AS parent_name, c.name AS child_name FROM org AS g JOIN org AS p ON p.parent_id = g.id JOIN org AS c ON c.parent_id = p.id;
这种写法容易忽略两点:
- 没有过滤根节点路径:如果某条链中间有
parent_id为空(比如 p.parent_id IS NULL),那整行就消失——JOIN天然跳过 NULL 匹配 - 结果集爆炸:一个根节点下有 10 个子节点、每个子节点又有 10 个孙节点,就会产生 100 行,不是 20 行。业务上是否真需要扁平展开?还是该用应用层递归组装?
- 别名嵌套易混淆:建议按层级命名(
lvl0,lvl1,lvl2),而不是t1/t2/t3
什么时候不该硬扛 SELF JOIN 查多层
超过 3 层、或层级深度不固定时,SELF JOIN 就不再是合理选择。它会导致 SQL 难读、难维护、难优化,且无法表达“查到叶子节点为止”这类逻辑。
此时应切换方案:
- MySQL 8.0+ / PostgreSQL / SQL Server:改用
WITH RECURSIVECTE,写一次就能查任意深度 - 旧版 MySQL(5.7):只能靠应用代码循环查询,或预计算路径字段(如
path = '/1/5/12/')再用LIKE查询 - 如果只是做前端树形渲染,更推荐查出全部节点后,在代码里构建成树(用哈希表按
parent_id分组),比拼 SQL 更稳
最常被忽略的一点:SELF JOIN 本身不解决「排序按树形顺序」的问题。即使连出了父子关系,结果默认仍是无序的;要实现缩进式展示,还得靠额外字段(如 lft/rgt 的闭包表,或递归查询里的 level 列)来辅助排序。










