递归CTE可替代多层自连接查询树形结构,通过锚成员和递归成员自动迭代,避免手动写N层JOIN及重复修改;需注意终止条件、语法限制与数据质量。

为什么自连接查树形结构容易写成“括号套娃”
自连接本身不难,难的是层级不确定时硬写多层JOIN。比如查某个分类下的全部子类(含子子类),用传统自连接就得手动写 l1 JOIN l2 ON l2.parent_id = l1.id,再 JOIN l3 ON l3.parent_id = l2.id……一旦层级超过3级,SQL就变成“猜缩进游戏”,而且改需求(比如加个过滤条件)得在每层都补一遍,极易漏改或错放位置。
递归CTE怎么替代多层自连接
递归CTE把“找下一级”这个动作拆成两步:先找起点(锚成员),再反复用上一轮结果去查下一级(递归成员)。数据库自动迭代,不用你手写N层JOIN。
- 必须用
WITH RECURSIVE开头,不能只写WITH - 锚成员(初始查询)和递归成员之间用
UNION ALL连接,不能用UNION(会去重,破坏递归逻辑) - 递归成员里必须引用自身CTE名,且只能出现在
FROM或JOIN右侧,不能出现在子查询里 - MySQL 8.0+、PostgreSQL、SQL Server 都支持,但 Oracle 要用
CONNECT BY替代
示例:查 category 表中 ID=1 的所有后代节点
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM category WHERE id = 1 UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree;
递归CTE的三个关键避坑点
递归不是万能的,写错会直接卡死或返回空——因为数据库默认有递归深度限制,且逻辑环路会导致无限循环。
-
MAX_RECURSION_DEPTH在 MySQL 中默认是 1000,超了就报错ERROR 3636 (HY000);可通过SET SESSION cte_max_recursion_depth = 2000临时调高,但治标不治本 - 必须确保递归条件最终能终止,比如
c.parent_id = t.id中,如果某条记录的parent_id指向自己,就会死循环 - 递归成员里不能用聚合、窗口函数、
GROUP BY,否则报错Recursive reference to CTE is not allowed in aggregated query
什么时候该用递归CTE,而不是物化路径或闭包表
递归CTE适合读多写少、层级动态、且深度可控的场景。它不改变表结构,上线快,但每次执行都重新计算路径。
- 用递归CTE:组织架构变动频繁、临时分析需求、权限树动态生成
- 别硬扛:商品类目深达20级+、日均百万次查询、要求毫秒响应——这时候该建
path字段或闭包表(closure table) - 注意:PostgreSQL 的递归CTE默认物化中间结果,MySQL 8.0 则倾向内联展开(即每次递归都重跑子查询),性能差异可能达数倍
真正麻烦的从来不是语法会不会写,而是没想清楚“这棵树到底稳不稳定”——ID循环、父ID为空却非根节点、软删除未过滤,这些数据问题会让最标准的 WITH RECURSIVE 也返回错误结果。

















