递归查询在C#中需手动实现,无标准写法;推荐用C#控制递归、SQL单层查询,注意索引、防循环引用、栈溢出及IO优化。

递归查询在 C# 中不是内置语法特性,而是靠你自己写逻辑实现的;它本身没有“标准写法”,关键看你要查什么——数据库树形结构?文件系统目录?内存中的嵌套对象?不同场景下递归的终止条件、数据加载方式、性能风险都完全不同。
递归查询数据库父子关系(如部门、菜单)
最常见的需求是查出某个节点的所有子节点(含子孙),比如 SELECT * FROM Dept WHERE ParentId = @id 这种单层查法不够用,必须反复查。
常见错误是直接在 SQL 里硬写多层 JOIN 或拼接 UNION ALL,结果层级一变就失效,或者把全部数据拉到内存再筛,数据量大时内存爆掉。
推荐做法是:用 C# 控制递归,SQL 只负责单层查询,每次只查当前层子节点:
public static List<dept> GetSubDepts(int rootId)
{
var result = new List<dept>();
void Traverse(int parentId)
{
var children = db.Query<dept>("SELECT * FROM Dept WHERE ParentId = @id", new { id = parentId });
foreach (var dept in children)
{
result.Add(dept);
Traverse(dept.Id); // 递归进下一层
}
}
Traverse(rootId);
return result;
}</dept></dept></dept>
- 必须确保数据库表有合理的索引,至少
ParentId列要建索引,否则每次Traverse都全表扫 - 没做深度限制,如果数据存在循环引用(A→B→C→A),会栈溢出;上线前务必加
maxDepth参数或用HashSet记录已访问 ID - EF Core 用户注意:
AsNoTracking()要加上,避免上下文跟踪大量实体拖慢速度
用 Stack 替代真实递归(防 StackOverflowException)
当树很深(比如超过 1000 层)或不确定数据质量时,纯递归函数容易触发 StackOverflowException,且无法捕获处理。这时候该用迭代 + 显式栈模拟。
核心思路是把“下一步要处理的 ID”压入 Stack,每次弹一个,查它的子节点,再把子节点 ID 全部压进去:
public static List<dept> GetSubDeptsIterative(int rootId)
{
var result = new List<dept>();
var stack = new Stack<int>();
stack.Push(rootId);
<pre class="brush:php;toolbar:false;">while (stack.Count > 0)
{
int currentId = stack.Pop();
var children = db.Query<dept>("SELECT * FROM Dept WHERE ParentId = @id", new { id = currentId });
foreach (var dept in children)
{
result.Add(dept);
stack.Push(dept.Id); // 注意:后序遍历用 Push,前序可改用 Queue<t>
}
}
return result;</t></dept>
}
- 这个版本完全规避了调用栈深度问题,适合导出、后台任务等长耗时场景
- 顺序和纯递归不同(这里是“深度优先但子节点逆序”),如果业务强依赖遍历顺序,得换
Queue做广度优先,或在压栈前反转children - 别忘了给
stack加最大容量检查,防止恶意构造的超深环路吃光内存
递归查询本地对象集合(List)
比如你从 API 拿回一个扁平的 List,每个项有 Id 和 ParentId,需要构建成树形结构再绑定到 TreeView。
错误做法是每找一个子节点都遍历整个列表 —— 时间复杂度 O(n²),1000 条目就要百万次比较。
正确做法是先建字典索引,再一次性递归组装:
public static MenuItem BuildTree(List<menuitem> items, int? rootId = null)
{
var dict = items.ToDictionary(x => x.Id);
MenuItem Build(int? id)
{
if (!dict.TryGetValue(id ?? 0, out var node)) return null;
node.Children = items.Where(x => x.ParentId == id).Select(x => Build(x.Id)).ToList();
return node;
}
return Build(rootId);
}</menuitem>
- 上面代码有个坑:
items.Where(...)每次都重新遍历,应该提前用Lookup建好父子映射,查子节点才是 O(1) - 如果原始数据不含根节点(
ParentId为 null),记得先过滤出所有顶层项,再分别调用Build - JSON 序列化时小心循环引用,默认 JSON.NET 会报错,得配
ReferenceHandler = ReferenceHandler.Preserve
递归本身很简单,难的是想清楚“谁来加载数据”“在哪截断”“怎么防环”“要不要缓存”。实际项目里,90% 的递归性能问题,根源不在递归写法,而在没控制好 IO 次数或没建对索引。










