cte递归查询必须先定义锚点(查根节点)再写递归成员(用union all连接并正确join),否则引发最大递归超限;string_agg需配合within group(order by level)确保路径顺序,且须防环、建索引、慎用函数。

CTE递归查询必须定义锚点和递归成员,顺序错不得
SQL Server里用CTE做树形路径拼接,最常踩的坑是把锚点查询(anchor member)和递归成员(recursive member)写反,或者漏掉 UNION ALL 连接。CTE递归要求:锚点先查根节点(比如 ParentID IS NULL),递归成员再用 JOIN 或 ON 关联上一层的 ID 和下一层的 ParentID,且必须放在 UNION ALL 后面。
常见错误现象:The statement terminated. The maximum recursion 100 has been exhausted before statement completion. —— 这往往不是真死循环,而是递归逻辑没收敛,比如 ON t.ParentID = r.ID 写成 ON t.ID = r.ParentID,导致每次都在查同一层。
- 锚点查询必须返回完整初始集(如所有一级节点),不能带
WHERE过滤掉本该参与递归的节点 - 递归成员中引用 CTE 自身时,别名必须一致(如
AS r,后面就得用r.ID) - 记得加
MAXRECURSION 0提示(尤其不确定层级深度时),否则默认 100 层就中断
STRING_AGG 需配合 ORDER BY 子句才能保证路径顺序
STRING_AGG 在递归 CTE 中不能直接对原始表字段聚合,必须基于已按层级排序的中间结果。因为 CTE 递归本身不保证输出顺序,STRING_AGG 默认按物理顺序拼接,而物理顺序 ≠ 树路径顺序。
正确做法是在递归 CTE 中增加一个层级计数列(如 Level INT)和路径构建列(如 Path NVARCHAR(MAX)),每层用 CONCAT 或 + 拼上当前节点名;最后再用 STRING_AGG 聚合时,必须显式指定 WITHIN GROUP (ORDER BY Level),否则父子顺序可能颠倒。
- 不要在
STRING_AGG外层再套ORDER BY—— 它只响应WITHIN GROUP子句 - 若节点名含特殊字符(如斜杠、反斜杠),建议提前用
QUOTENAME或REPLACE处理,避免路径语义混淆 -
STRING_AGG在 SQL Server 2017+ 才支持,旧版本得用FOR XML PATH('')替代
性能瓶颈常出在字符串拼接方式与索引缺失上
递归 + 字符串拼接是双重开销操作。实测发现:当树深 > 20 层或单节点子节点 > 500 个时,用 + 拼接 NVARCHAR(MAX) 比用 CONCAT 慢 30% 以上,尤其在未建合适索引时。
- 确保
ParentID列有索引(非聚集即可),否则递归 JOIN 会全表扫描 - 避免在递归 CTE 内部调用函数(如
ISNULL包裹字段再拼接),会阻止参数化和计划重用 - 如果只需最深层路径(如“查某个叶子节点的完整路径”),用
TOP 1+ORDER BY Level DESC提前终止,比全量递归后过滤更高效
兼容性问题:STRING_AGG 不支持 DISTINCT,去重得靠外层处理
想拼路径时自动去重(比如某节点在多条路径中重复出现),不能指望 STRING_AGG(DISTINCT ...) —— SQL Server 目前不支持该语法,会报错 Incorrect syntax near the keyword 'DISTINCT'。
真实场景中,路径重复往往源于数据模型缺陷(如环状引用)或递归条件宽松(如未排除已访问节点)。解决思路是:在递归 CTE 中加一个已访问路径标识列(如 VARCHAR(4000) 存 ID 序列),每次递归前用 CHARINDEX 检查当前 ID 是否已在路径中,是则跳过。虽然略重,但比事后去重可靠。
- 别用
SELECT DISTINCT套整个 CTE 结果再聚合——路径本身是唯一键,去重会破坏结构 - 如果业务允许近似去重,可用
GROUP BY先按最终路径分组,再取任意一条,但会丢失层级信息
STRING_AGG 的排序上下文是否真的绑定到层级维度——这两处一松动,结果就不可信。










