最直接有效的办法是用Include显式预加载关联数据,把1+N次查询压成1次JOIN查询;禁用延迟加载,避免N+1;多级关系用ThenInclude链式加载;笛卡尔爆炸时用AsSplitQuery拆分查询;只读场景优先Select投影DTO并AsNoTracking。

最直接有效的办法是用 Include 显式预加载关联数据,把原本 1+N 次查询压成 1 次 JOIN 查询。别依赖延迟加载,尤其在循环里访问导航属性时,N+1 就悄悄发生了。
用 Include + ThenInclude 一次性加载多级关系
适用于需要完整对象图的场景,比如查博客、文章、作者、评论四级数据:
- 一级包含:`.Include(b => b.Posts)` 加载所有文章
- 二级延伸:`.ThenInclude(p => p.Author)` 加载每篇文章的作者
- 三级延伸:`.ThenInclude(a => a.Profile)` 再加载作者档案(支持链式调用)
EF Core 会生成一条含多个 JOIN 的 SQL,避免逐条查询。注意:导航路径必须连续,不能跳级(如不能从 Blog 直接 ThenInclude 到 Comment)。
拆分查询 + 内存关联(Split Queries)
当多级 Include 导致笛卡尔爆炸(比如 1 个订单 × 10 商品 × 5 日志 = 50 行重复数据),可改用独立查询:
- EF Core 5+ 支持 `.AsSplitQuery()`,让每个 Include 变成单独 SQL,再由 EF 在内存中按主键/外键自动关联
- 比单条 JOIN 更省内存和网络带宽,适合子集合数据量大的情况
- 写法示例:
.AsSplitQuery().Include(o => o.Items).ThenInclude(i => i.Logs)
用 Select 投影最小化数据(推荐高频只读接口)
根本不需要整个实体?那就别加载实体,直接取字段:
- 用
.Select()构造匿名类型或 DTO,只查真正要展示的字段 - 搭配
.AsNoTracking()关闭变更跟踪,查询速度更快、内存更轻 - 例如:
context.Blogs.Select(b => new { b.Name, PostCount = b.Posts.Count() })
这种方式既避开 N+1,又避免了 Include 带来的冗余数据和笛卡尔膨胀。
关掉延迟加载,杜绝隐式触发
延迟加载(Lazy Loading)是 N+1 的温床,尤其在序列化或日志打印时容易意外触发:
- 不引用
Microsoft.EntityFrameworkCore.Proxies包 - 或显式禁用:
options.UseLazyLoadingProxies(false) - 开发阶段配合 EF Core 日志或 MiniProfiler,一眼就能看出重复 SQL 是否出现
基本上就这些。核心就一条:别让导航属性“偷偷查”,要么一次全拿,要么分步手动拿,要么干脆不拿实体。










