应优先使用递归CTE:SQL Server和MySQL 8.0+用WITH/ WITH RECURSIVE,MySQL 5.7才考虑临时表+循环;CTE必须为批处理首句,锚查询动态限定根节点,需加OPTION(MAXRECURSION n),排序依赖path字段而非level。

直接说结论:SQL Server 和 MySQL 8.0+ 应该用递归 CTE,别写存储过程;MySQL 5.7 或必须用存储过程的场景,才考虑临时表 + 循环或自定义函数——但后者有线程安全和维护成本问题。
SQL Server 里 WITH 必须是批处理第一条语句
在存储过程中写递归 CTE,常见错误是把 WITH 套在 IF 或 BEGIN 后面,比如:
IF @rootId IS NOT NULL BEGIN WITH Tree AS (...) SELECT * FROM Tree; END
这会报错 Incorrect syntax near 'WITH'。因为 SQL Server 要求 WITH 必须是批处理中第一个可执行语句。
- 正确做法:把整个 CTE + SELECT 块放在
IF分支内,且确保它是该分支的第一条语句 - 锚查询要用
WHERE id = @rootId动态限定根节点,别拼字符串 - 必须加
OPTION (MAXRECURSION n)在最终SELECT末尾,不能省;MAXRECURSION 0生产禁用 - 如果树深不确定,建议把
@maxRecursion设为输入参数,由调用方控制
MySQL 8.0+ 优先用 WITH RECURSIVE,不是存储过程
MySQL 8.0+ 原生支持 WITH RECURSIVE,语法干净、可读性强,比写存储过程靠谱得多。关键不是“查出来”,而是“排序时能反映树形深度和同级顺序”。
-
UNION ALL必须写全,写成UNION会去重,可能漏掉同名合法节点 - 层级字段(如
level)必须显式累加:t.level + 1,不能依赖ROW_NUMBER()—— 因为递归结果本身无序 - 真正控制“父排前、子紧随、同级按 name 排”的,是最终
ORDER BY path,其中path推荐用CONCAT(t.path, '.', c.id)或更健壮的LPAD(c.id, 10, '0')拼接 - 避免在 CTE 外再套一层
SELECT *包装,除非真要分页或加ORDER BY
MySQL 5.7 只能靠临时表 + 循环,慎用自定义函数
MySQL 5.7 不支持递归 CTE,硬要纯 SQL 实现树查,主流方案是创建临时表 + 存储过程循环插入后代。自定义函数(如 GET_CHILD_NODE)看着简洁,但实际有硬伤:
- 函数内用
WHILE+GROUP_CONCAT+FIND_IN_SET,字符串长度上限(默认 1024)容易截断,深层树直接失效 - 函数返回的是 ID 字符串,
FIND_IN_SET(id, ...)无法走索引,大数据量时性能崩盘 - 多个并发调用可能因共享会话变量或临时表名冲突导致结果错乱(线程不安全)
- 若必须用存储过程,推荐用
tmpLst临时表 +WHILE row_count() > 0迭代,比游标更稳
排序和缩进显示不能只靠 level 字段
很多人以为 ORDER BY level, name 就能实现树形展示,其实不行。它只能保证“所有 level=1 的排前面,然后是 level=2”,但父子之间不连续——比如父节点 A(level=1)和它的两个子节点(level=2)会被分散在结果集不同位置。
- 真正有效的排序依据是路径字符串
path,例如'1.2.5'和'1.2.6',字典序天然保证拓扑顺序 - 前端缩进显示(如
├─、│├─)应基于level字段用REPLICATE(' ', level)拼空格,比 LEFT JOIN 自关联稳定 - SQL Server 构造
sort_path时,务必对 ID 做定长填充(如RIGHT('00000' + CAST(id AS VARCHAR), 5)),否则1, 10, 2会排成1, 10, 2而非1, 2, 10
最易被忽略的一点:递归 CTE 的排序逻辑只在最终 SELECT 里生效,CTE 内部加 ORDER BY 是语法错误,且不影响递归遍历顺序。想控制兄弟节点顺序,必须把排序信息编码进 path 或 sort_path 字段里。










