固定深度用SELF JOIN,任意深度或需路径追踪时必须用递归CTE;查两层上级用SELF JOIN更直观可控,查所有祖先或防环必须用递归CTE。

递归 CTE 和 SELF JOIN 解决的是不同层级的问题,不是“选哪个更好”,而是“哪个能解决当前问题”——固定深度用 SELF JOIN,任意深度或需路径追踪时必须用递归 CTE。
查两层上级就用 SELF JOIN,别硬套递归 CTE
比如展示“员工 → 直属经理 → 经理的经理”,三层结构清晰、固定,SELF JOIN 更直观、性能更可控:
- 写法简单:
SELECT e.name, m1.name, m2.name FROM employees e LEFT JOIN employees m1 ON e.manager_id = m1.id LEFT JOIN employees m2 ON m1.manager_id = m2.id - 索引友好:只要
manager_id有索引,每层 JOIN 都能走索引查找 - 结果稳定:不会因中间节点缺失(如某经理没上级)导致整行消失 ——
LEFT JOIN可保主表行数不变 - 递归 CTE 在这种场景反而多此一举:要设
LEVEL限制、写终止条件、还可能被优化器误判为复杂查询而降级执行
检测循环引用或查完整汇报链,SELF JOIN 根本做不到
SELF JOIN 是静态展开,最多覆盖你写的层数。它对「A→B→C→A」这类三跳闭环完全无感,也查不出「Alice 的上级链到底有多长」:
-
SELECT * FROM org_unit a JOIN org_unit b ON a.id = b.parent_id JOIN org_unit c ON b.id = c.parent_id WHERE a.id = c.parent_id只能捕获“祖父=曾孙”,漏掉所有其他长度的环 - 想查某员工的所有祖先(无论几层),必须用递归 CTE:
WITH RECURSIVE ancestors AS (SELECT id, manager_id FROM employees WHERE id = ? UNION ALL SELECT e.id, e.manager_id FROM employees e JOIN ancestors a ON e.id = a.manager_id) - 防环关键在递归部分加判断:
WHERE NOT e.id = ANY(a.path)(PostgreSQL)或用LEVEL+ 配置项cte_max_recursion_depth=200(MySQL)兜底 - 不加防环机制,遇到真实循环(如 A→B→A)会无限递归直到超限报错:
ERROR 3636 (HY000): Recursive query aborted after 1000 iterations
日期连续性校验这类“隐式链”,SELF JOIN 比窗口函数更可靠
窗口函数如 LAG() 按物理行序取值,股票休市日缺失会导致“隔了5天的上涨”被算作“连续6天”。SELF JOIN 能强制要求“前一天必须真实存在”:
- 连接条件必须写成:
t1.trade_date = t2.trade_date + INTERVAL 1 DAY(MySQL)或t1.trade_date = t2.trade_date + 1(PostgreSQL) - 上涨判断必须放
ON子句,不是WHERE:ON t1.stock_code = t2.stock_code AND t1.close_price > t2.close_price,否则首日会被过滤 - 索引必须是复合索引:
(stock_code, trade_date),单列trade_date索引在自连接中基本无效 - 这种写法本质是“显式建链”,虽不能自动延伸 N 天,但每一步都可验证,比依赖行序的窗口函数更贴近业务真实约束
真正容易被忽略的点是:递归 CTE 的终止条件和层级限制必须与业务深度匹配。组织架构平均 5 层,cte_max_recursion_depth 设成 10 是安全的;但若实际存在 12 层汇报链,就会被截断——这不是语法错误,而是静默丢失数据。SELF JOIN 没这问题,但它也绝不会帮你发现第 6 层是谁。

















