WITH RECURSIVE 是处理层级数据的正解,需正确编写锚点与递归体、控制深度、建立 parent_id 索引,并清理环状引用等脏数据。

用 WITH RECURSIVE 实现层级遍历,别写自连接
MySQL 8.0+、PostgreSQL、SQL Server 都支持 WITH RECURSIVE,这是处理组织架构、分类树、BOM 清单等层级数据的正解。手写多层 LEFT JOIN 不仅难维护,还容易漏层级、爆笛卡尔积。
常见错误是把递归 CTE 当成普通子查询:在递归部分引用外部表、或在 UNION ALL 后加 WHERE 过滤(会截断递归链)。递归体只能依赖上一层结果,过滤必须放在最外层或递归体内部用条件限制深度。
- 递归锚点(anchor)必须能单独执行,且不依赖递归部分
- 递归体(recursive term)必须通过
JOIN或条件关联到上一层的输出列,不能出现新表 - 加
LEVEL或path字段辅助调试,比如CONCAT(t.path, '→', t.id)
控制递归深度,避免无限循环
数据库不会自动终止死循环,一旦父子关系异常(如 A → B → A),查询可能卡住或超时。PostgreSQL 默认限 100 层,MySQL 8.0 要手动设 cte_max_recursion_depth 系统变量(默认 1000)。
更可靠的做法是在 CTE 内部加计数器和阈值判断:
WITH RECURSIVE tree AS ( SELECT id, parent_id, 1 AS depth FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.parent_id, t.depth + 1 FROM categories c INNER JOIN tree t ON c.parent_id = t.id WHERE t.depth <p>注意:<code>WHERE</code> 必须写在递归 <code>SELECT</code> 内,写在外面无效;深度阈值按业务真实层级设(比如菜单通常 ≤ 6,部门树 ≤ 8),别硬写 100。</p><h3>索引没建对,递归查询照样慢</h3><p>递归本身不慢,慢在每次 JOIN 都要查 <code>parent_id</code>。如果 <code>categories(parent_id)</code> 没索引,第一层快,第二层开始每层都全表扫。</p><p>正确索引不止一个字段:</p>
- 必须有
INDEX (parent_id)—— 递归 JOIN 的驱动列 - 如果常按
name排序或搜索,加联合索引INDEX (parent_id, name) - 主键
id自动有索引,不用额外建
别信“反正有主键就行”——parent_id 是递归里的查找键,不是主键的前缀,单独建索引才生效。
MySQL 8.0 以下或 SQL Server 2012+ 的兼容写法
老版本 MySQL 只能靠应用层迭代或存储过程模拟递归,但性能差、逻辑散。SQL Server 支持 WITH RECURSIVE,但语法略有不同:需显式声明列名,且递归列类型必须严格一致(比如 id 是 INT,锚点和递归体都不能转成 VARCHAR)。
跨数据库可移植性低,别强求一套 SQL 跑所有环境。如果必须兼容旧版 MySQL,优先考虑在应用里做两层缓存:首查全树存 Redis,后续只查变更节点。
真正麻烦的不是语法,是业务数据里隐含的环状引用、空 parent_id 值、或 parent_id 指向不存在的 id —— 这些问题不清理,再好的递归写法也会查出错或漏数据。










