递归JOIN并非SQL标准语法,实际指WITH RECURSIVE中通过自引用CTE实现的递归查询;其递归逻辑在CTE递归成员内完成,限制条件(如level < N或路径去环)必须置于递归部分WHERE子句中,否则无法阻止无效递归展开。

什么是递归JOIN?它和WITH RECURSIVE不是一回事
很多人看到“递归JOIN”第一反应是写个WITH RECURSIVE,但严格来说,SQL里没有“递归JOIN”语法——JOIN本身不递归,递归靠的是CTE(公用表表达式)驱动的自引用查询。所谓“带限制条件的递归查询”,实际是指在WITH RECURSIVE中控制递归深度、避免循环、提前终止等行为。
关键点在于:递归逻辑写在CTE的递归成员里,限制条件必须放在递归部分的WHERE子句中,而不是外部查询里;否则只会过滤最终结果,无法阻止无意义的递归展开。
如何防止无限递归?必须加层级计数和路径检测
最常见错误是只依赖外键关系做递归,却没防住环形引用(比如A→B→C→A)。PostgreSQL/SQL Server/MySQL 8.0+ 都支持在递归CTE中维护一个路径数组或字符串,配合NOT IN或@path @> new_id类判断来剪枝。
实操建议:
- 用一个
level整数列记录递归深度,配合level 硬性截断(避免OOM) - 用
ARRAY[base_id] || new_id(PostgreSQL)或CONCAT(path, ',', id)(MySQL)累积访问路径 - 递归分支的
WHERE里必须包含new_id != ALL(path)或LOCATE(CONCAT(',', new_id, ','), CONCAT(',', path, ',')) = 0 - 别把路径检测逻辑放在最外层
SELECT的WHERE——那已经晚了,递归早就跑飞了
WHERE条件放错位置会导致性能雪崩
比如想查“部门ID=5下所有职级≤3的员工”,如果写成:
WITH RECURSIVE org AS ( SELECT id, name, manager_id, 1 as level FROM dept WHERE id = 5 UNION ALL SELECT d.id, d.name, d.manager_id, o.level + 1 FROM dept d JOIN org o ON d.manager_id = o.id ) SELECT * FROM org WHERE level <= 3;
这会先递归出全部子孙,再过滤——数据量大时可能生成百万行中间结果。正确做法是把level (注意是<code>不是<code>)放进递归分支的<code>ON或WHERE:
UNION ALL SELECT d.id, d.name, d.manager_id, o.level + 1 FROM dept d JOIN org o ON d.manager_id = o.id WHERE o.level < 3 -- ✅ 在这里剪枝
同理,若要限定“只查状态为active的员工”,条件d.status = 'active'也必须放在递归分支内部,不能等到最后才过滤。
不同数据库对递归深度的默认限制差异很大
MySQL 8.0 默认cte_max_recursion_depth = 1000,超出直接报错ERROR 3636;PostgreSQL不限制但受work_mem制约,容易OOM;SQL Server默认100层,可通过OPTION (MAXRECURSION 32767)调整。这些不是可选配置,而是你写查询前必须确认的底线。
更隐蔽的问题是:某些数据库(如旧版MariaDB)根本不支持WITH RECURSIVE,强行用会报ERROR 1064。上线前务必查文档确认版本兼容性,别只测本地环境。
递归查询真正难的不是语法,是想清楚“哪些条件必须在递归过程中生效”——漏掉一个路径去重或深度限制,轻则慢十倍,重则锁死连接。

















