SELF JOIN适合查询固定层数的父子关系,如员工→经理或员工→经理→总监,写法简洁、兼容性好,但无法动态适应深度变化;递归CTE才是查任意深度层级的唯一可靠方式。

SELF JOIN适合查固定层数的父子关系
当业务明确只要员工→经理、或员工→经理→总监这种最多两到三层的关系时,SELF JOIN写起来快、读得懂、数据库兼容性好(MySQL 5.7、PostgreSQL 9.6 都支持)。它本质是把表“展开”成多份,每加一层就多一个 JOIN,但不能动态适应深度变化。
-
LEFT JOIN保留无上级的记录(如 CEO),INNER JOIN只返回有完整上级链的员工 - 别名必须不同,比如
e1、e2、e3,否则报错ERROR 1066: Not unique table/alias - ON 条件里要写清楚方向:查下属用
e2.manager_id = e1.id,查上级用e1.manager_id = e2.id,反了结果就空 - 如果某层 manager_id 为 NULL,后续 JOIN 会断掉——
e2.manager_id = e3.id这一环失效,e3.name全是 NULL
递归 CTE 是查任意深度层级的唯一可靠方式
组织架构经常变动,有人汇报给总监,有人跨级汇报,还有临时兼任、代理等场景。WITH RECURSIVE 不依赖预设层数,自动向下遍历直到叶子节点,还能带 level 字段标记深度、用 path 或 NOT id = ANY(path) 防环。
- MySQL 8.0+、PostgreSQL、SQL Server、SQLite 3.8.3+ 支持;老版本 MySQL(如 5.7)不支持,硬写多层 SELF JOIN 也撑不住四层以上
- 锚点(anchor)必须写清起点,比如
WHERE manager_id IS NULL找顶层,漏掉这句整个递归就跑不起来 - 递归部分必须用
UNION ALL,不能用UNION(去重开销大,且可能误删合法重复 ID) - MySQL 默认递归深度限 100 层,可通过
SET SESSION cte_max_recursion_depth = 200调整;SQL Server 用OPTION (MAXRECURSION 200)
别用 SELF JOIN 做循环检测
SELF JOIN 最多查出“祖父 = 孙子”这类固定跳数闭环,比如 a.id = b.parent_id AND b.id = c.parent_id AND c.id = a.parent_id,但对“A→B→C→A”三跳环、或更长的环完全无感。它不维护路径状态,也没终止机制,本质上是静态展开。
- 真正防环得靠递归 CTE 中的
path数组(PostgreSQL)或字符串拼接 +LIKE(MySQL),再加WHERE NOT id = ANY(path)判断是否已访问过 - 自引用(
parent_id = id)不等于逻辑错误,可能是合法业务规则(如部门负责人管自己),单靠 JOIN 无法区分 - 想快速筛明显异常?可以先用
SELECT * FROM org_unit WHERE parent_id = id,但这只是初筛,不能替代递归检测
性能和可维护性差异比想象中大
三层 SELF JOIN 看着简单,但字段别名易冲突(比如都叫 name 必须手动 AS)、NULL 处理逻辑分散、改个字段就得同步改三处。递归 CTE 表结构扁平,字段只定义一次,扩展新字段或加过滤条件都在同一层写。
- 索引关键:无论哪种方式,
manager_id字段必须有索引,否则递归或 JOIN 都慢得没法用 - 数据量大时,递归 CTE 在 PostgreSQL 和 SQL Server 上优化较好;MySQL 8.0 的递归性能偏弱,若层级深、节点多,建议加
level <= 5提前截断 - 如果业务要求“查某人所有下属(不限深度)”,硬写五层 JOIN 不仅难 debug,还可能因中间某层 NULL 导致结果少一半——这时候递归不是“更高级”,而是“唯一能跑通的”
实际选型时,别纠结“哪个更酷”,先问清楚:这个查询未来会不会变深?有没有环?要不要导出完整路径?答案一旦涉及“可能”“不确定”“需要追溯到底”,就该直接上递归 CTE。

















