sql server递归cte必须严格分为锚点和递归成员两部分,用union all连接,列名数量类型须一致;查子树用o.parentguid=cte.id,查祖先用o.id=cte.parentguid;务必加option(maxrecursion n)防无限循环。

SQL Server 中用 WITH AS 做递归查询,不是“能不能”,而是“必须严格按两段结构写 + 控制递归深度”,否则极易卡死或返回不全。
递归CTE必须拆成锚点 + 递归成员两部分
很多人直接套用 SELECT * FROM table 加 UNION ALL 就报错,根本原因是没理解 SQL Server 对递归 CTE 的语法硬约束:它要求第一部分(锚点)和第二部分(递归成员)必须明确分离,且只能用 UNION ALL 连接——UNION 会报错,JOIN 单独写在外面也不行。
- 锚点部分:必须返回初始行,通常是根节点(如
WHERE ParentGUID = '00000000-0000-0000-0000-000000000000')或指定起点(如WHERE ID = '5b82d74d-c5c7-4cbb-af28-0518bdc257d9') - 递归成员:必须显式
JOIN到自身 CTE 名,连接条件要体现父子关系,例如ON o.ParentGUID = cte.ID(查子节点)或ON o.ID = cte.ParentGUID(查父节点) - 列名和数量必须完全一致:两部分
SELECT的字段数、类型、顺序要对齐,否则报错Types don't match between the anchor and the recursive part
查子树 vs 查祖先链,连接方向不能反
同一个表,查“某员工的所有下属”和查“某员工的全部上级”逻辑相反,但新手常把 JOIN 条件写反,导致结果为空或只有一层。
- 查子树(向下递归):锚点选目标节点,递归时让子表的
ParentGUID匹配 CTE 的ID,即o.ParentGUID = cte.ID - 查祖先(向上递归):锚点仍选目标节点,但递归时让子表的
ID匹配 CTE 的ParentGUID,即o.ID = cte.ParentGUID - 典型错误:用
o.ParentGUID = cte.ParentGUID或漏写WHERE导致全表扫描,性能骤降
不加 OPTION (MAXRECURSION n) 很可能触发无限循环
SQL Server 默认递归上限是 100 层,但实际业务中层级常超限,或因数据异常(比如 A→B→C→A 形成环)直接卡住连接。不显式限制,轻则超时,重则阻塞其他查询。
- 安全做法:所有递归查询末尾都加
OPTION (MAXRECURSION 32)(32 是常见组织/菜单深度上限) - 调试时可设为 0(无限制),但上线前必须改回具体数值,否则生产环境风险极高
- 错误提示
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.就是没加或设太小
需要层级序号或路径字符串?别硬拼,用内置计算
单纯返回 ID 和名称不够,业务常要“第几级”或“张志军 → 杜高扬 → 屠玉韵”这种路径。这些必须在 CTE 内部用表达式算,不能留到外面 ROW_NUMBER() 或字符串拼接。
- 层级计数:锚点写
0 AS Level,递归部分写cte.Level + 1 - 路径拼接:锚点用
CAST(EName AS NVARCHAR(MAX)) AS Path,递归部分用cte.Path + ' → ' + o.EName;注意必须用CAST显式转NVARCHAR(MAX),否则默认截断为 30 字符 - 避免在外部用
STUFF或多次FOR XML拼路径——性能差且难调试
最易被忽略的一点:递归 CTE 的执行计划里,CTE 部分不会显示真实迭代次数,只能靠 MAXRECURSION 和结果行数反推;如果发现层级数对不上,优先检查锚点是否过滤了空父节点、递归条件是否用了可空字段(如 ParentGUID IS NULL 要小心处理)。











