使用EF Core(.NET 6)Include()加载大量数据时如何解决性能问题?
问题场景
我正在开发基于.NET 6的Entity Framework Core应用,调用GetQuestionByTest()方法检索数据时遇到性能问题。需要加载与TblQuestionTest实体关联的TblMultipleChoices数据,但数据量较大导致应用响应延迟。
问题代码:
public IEnumerable<TblQuestionTest> GetQuestionByTest(int tesId) { return context.TblQuestionTests .Include(e => e.Test) .Include(e => e.TblMultipleChoices) .Where(e => e.TestId == tesId); }
选择题模型(scaffold生成):
public partial class TblMultipleChoice { public int MultipleChoiceId { get; set; } public string? MultipleChoiceDesc { get; set; } public int QuestionId { get; set; } public string Score { get; set; } = null!; public virtual TblQuestionTest Question { get; set; } = null!; }
优化策略与实现
1. 投影查询:只加载需要的字段
默认Include会加载关联实体的所有字段,很多属性可能是业务不需要的。用Select手动指定所需字段,能大幅减少数据库返回的数据量,降低传输和序列化开销。
示例代码:
// 可以定义DTO类替代匿名类,实现强类型返回 public class QuestionTestDto { public int QuestionTestId { get; set; } public string QuestionContent { get; set; } = null!; public TestDto Test { get; set; } = null!; public List<MultipleChoiceDto> Choices { get; set; } = new(); } public class TestDto { public int TestId { get; set; } public string TestName { get; set; } = null!; } public class MultipleChoiceDto { public int Id { get; set; } public string? Desc { get; set; } public string Score { get; set; } = null!; } public IEnumerable<QuestionTestDto> GetQuestionByTest(int tesId) { return context.TblQuestionTests .Where(e => e.TestId == tesId) .Select(e => new QuestionTestDto { QuestionTestId = e.QuestionTestId, QuestionContent = e.QuestionContent, Test = new TestDto { TestId = e.Test.TestId, TestName = e.Test.TestName }, Choices = e.TblMultipleChoices.Select(mc => new MultipleChoiceDto { Id = mc.MultipleChoiceId, Desc = mc.MultipleChoiceDesc, Score = mc.Score }).ToList() }); }
2. 分页加载:避免一次性返回全部数据
如果前端不需要一次性展示所有题目,分页是最直接的优化手段。通过Skip和Take控制每次返回的数据量,减轻数据库和应用的压力。
示例代码:
public IEnumerable<TblQuestionTest> GetQuestionByTest(int tesId, int pageIndex, int pageSize) { return context.TblQuestionTests .Include(e => e.Test) .Include(e => e.TblMultipleChoices) .Where(e => e.TestId == tesId) .Skip((pageIndex - 1) * pageSize) .Take(pageSize); }
3. 使用拆分查询(Split Query)
EF Core默认用笛卡尔积方式查询关联数据,当TblMultipleChoices数量较多时,TblQuestionTest的数据会被重复返回,导致数据量暴增。AsSplitQuery()可以把查询拆分成多个独立SQL语句,分别查询主实体和关联实体,避免数据重复。
示例代码:
public IEnumerable<TblQuestionTest> GetQuestionByTest(int tesId) { return context.TblQuestionTests .Include(e => e.Test) .Include(e => e.TblMultipleChoices) .Where(e => e.TestId == tesId) .AsSplitQuery(); // 添加拆分查询 }
4. 数据库索引优化
检查数据库中TblQuestionTests表的TestId字段、TblMultipleChoices表的QuestionId字段是否创建索引。无索引时数据库会做全表扫描,数据量大时速度极慢。
通过EF Core迁移添加索引:
// 在TblQuestionTests的配置类中添加 builder.HasIndex(e => e.TestId); // 在TblMultipleChoices的配置类中添加 builder.HasIndex(e => e.QuestionId);
执行迁移命令:
Add-Migration AddIndexesForPerformance Update-Database
5. 延迟加载(谨慎使用)
如果不是每次调用都需要加载TblMultipleChoices,可以开启延迟加载。它会在第一次访问TblMultipleChoices属性时才查询数据库,但要注意N+1问题(每个主实体都会触发一次单独查询)。
开启方式:在DbContext构造函数中设置:
public YourDbContext(DbContextOptions<YourDbContext> options) : base(options) { ChangeTracker.LazyLoadingEnabled = true; }
注:你的实体导航属性已经用
virtual修饰,满足延迟加载的前提条件。此方案仅适合偶尔需要关联数据的场景。
内容的提问来源于stack exchange,提问作者Juju Lian

