include仅在查询执行前对根iqueryable生效,一旦被select等操作中断即失效;theninclude只支持导航属性;assplitquery需配合include且不适用于所有场景;select投影更轻量但不可追踪。

Include 不是万能开关,它只在查询执行前生效,且一旦被 LINQ 操作打断就会失效——N+1 问题绝大多数时候不是没写 Include,而是写了但没起作用。
Include 为什么查着查着就失效了
常见错误现象:ToList() 前写了 Include,结果循环里访问 order.Customer.Name 还是触发额外查询;或者用了 Select 投影后导航属性全为 null。
- 根本原因:EF Core 的预加载逻辑绑定在「当前查询的根 IQueryable 实例」上。一旦调用
Select、Where(非过滤主表字段)、OrderBy等操作,尤其是返回匿名类型或 DTO 时,EF Core 会丢弃所有未显式投影的导航属性,Include自动作废 - 最容易被忽略的坑:在
Include后接.AsNoTracking()是安全的,但接.Select(x => new { x.Id, x.Customer.Name })就危险——EF Core 6+ 会尝试自动 JOIN,EF Core 5 及更早版本直接退化为 N+1,且不报错 - 验证方法:打开 EF Core 日志(
DbContextOptionsBuilder.LogTo(Console.WriteLine)),看生成的 SQL 是否含JOIN;别信返回结果“看起来有数据”,要盯住实际发了几条 SQL
ThenInclude 多级嵌套总报错或漏数据
典型错误信息:InvalidOperationException: The expression 'x => x.Address.City' is not a valid member access expression,或查出来 Address 有值但 City 为空。
- 必须确认每一级都是导航属性:比如
Customer.Address是Address实体没问题,但若Address类里定义的是public string CityName { get; set; }(普通字段),ThenInclude(x => x.CityName)就非法——ThenInclude只接受导航属性,不能点到标量字段 - 别混用 Lambda 和字符串路径:
Include("Customer").ThenInclude("Address")在 EF Core 6+ 已废弃,编译可能过,运行时报错或静默失败;统一用Include(x => x.Customer).ThenInclude(x => x.Address) - 三层以上嵌套(如
Order → Customer → Address → Province)建议拆开写:Include(o => o.Customer).ThenInclude(c => c.Address).Include(o => o.Customer).ThenInclude(c => c.Province),比强行链式ThenInclude更稳定,也方便调试哪一级断了
AsSplitQuery 能不能当 Include 的替代品
能缓解,但不是通用解药。它解决的是「一对多 + 多对一混合导致的笛卡尔积爆炸」,不是所有 N+1 场景都适用。
- 适用条件:必须配合
Include使用,且只对存在多个一对多关系的查询有效(例如查Order同时Include了Customer和OrderItems)。单独用AsSplitQuery()不会加载任何关联数据 - 硬限制:不支持
GroupBy、Distinct、聚合函数(Count()、Avg()等),也不支持AsNoTrackingWithIdentityResolution() - 性能权衡:虽然避免了笛卡尔积,但会发多条 SQL,网络往返次数增加;如果数据库和应用服务器不在同一机房,延迟可能反超单条 JOIN 查询
什么时候该放弃 Include,改用 Select 投影
当你只需要几个字段,且后续不打算修改实体时,Select 往往比 Include 更轻、更快、更可控。
- 关键区别:
Include返回可追踪实体,适合读+写场景;Select返回不可追踪对象,内存占用低、序列化快,但无法直接SaveChanges() - 注意空引用风险:若
Customer为 null,o.Customer.Name在投影中会抛NullReferenceException,需改用o.Customer?.Name或提前Where过滤 - 复杂集合投影要小心:例如
ProductNames = o.OrderItems.Select(oi => oi.Product.Name),EF Core 会转成子查询或 APPLY,大数据量时性能可能不如分两步查(先查订单,再用 IN 批量查商品)
最常被忽略的一点:N+1 的根因往往不在查询写法,而在业务逻辑里——比如接口返回 JSON 时,序列化器自动触碰了未加载的导航属性;或者前端反复请求同一订单详情却没缓存,后端每次重建 DbContext 查一遍。优化得先定位真实触发点,而不是无脑堆 Include。











