Skip to content
Go back

提升EF Core性能的4个实用技巧

Published:  at  12:00 AM

提升EF Core性能的4个实用技巧

Entity Framework Core(EF Core)提供了极高的开发效率,但ORM的抽象也可能隐藏昂贵的数据库操作。下面我把原来的四个技巧扩展为更专业的说明:原理、适用场景、折衷与进阶实践(包含代码示例与测量建议),便于在工程化场景中落地。

1) 避免在循环中对数据库发起查询(N+1)——原理与应对

问题:在循环体内对数据库逐条查询会导致大量往返(RTT)与多次查询计划解析,表现为“N+1 查询”问题(主查询1次 + 每条记录一个额外查询)。在高并发或数据量大的场景,这会迅速成为瓶颈。

对策:一次性按集合或批量查询,或采用关联加载(Include/ThenInclude)与投影(Select)来减少往返次数。

示例(坏):

for (int i = 0; i < ids.Count; i++)
{
    var e = context.MyEntities.FirstOrDefault(x => x.Id == ids[i]);
    // 每次循环发一条 SQL
}

改进(好):一次性查询并在内存中处理:

var entities = context.MyEntities
    .Where(e => ids.Contains(e.Id))
    .ToList(); // 单次 SQL

// 或者按键构建字典以 O(1) 查找
var map = entities.ToDictionary(e => e.Id);

进阶建议:

性能测量:开启EF Core日志(LogLevel.Information/Debug)观察生成的 SQL;使用数据库的慢查询日志或执行计划(EXPLAIN/SET STATISTICS)验证总SQL次数与时间。

2) 只选择需要的字段(Projection)——减少传输与内存

原理:默认实体查询会把表的所有列映射到实体属性,数据库发送与反序列化成本高。通过投影到匿名类型或DTO,仅检索必要列可显著减少网络带宽、IO与GC压力。

示例:

// 好:只查询必要列到 DTO
var results = context.Orders
    .Where(o => o.Status == OrderStatus.Paid)
    .Select(o => new OrderDto { Id = o.Id, Total = o.TotalAmount, CustomerName = o.Customer.Name })
    .ToList();

// 不好:加载完整 Order 实体和大量导航
var all = context.Orders.ToList();

进阶:

注意事项:

3) AsNoTracking 与 AsNoTrackingWithIdentityResolution 的使用场景

原理:默认情况下,EF Core 会将查询出的实体放进 ChangeTracker,以便后续变更检测与保存,但这会带来内存与 CPU 成本。对于只读场景禁用跟踪可以显著提高吞吐。

API 对比:

示例:

var list = context.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .ToList();

// 包含多个 Include 且仍希望保持实体引用一致
var complex = context.Orders
    .Include(o => o.Items)
    .ThenInclude(i => i.Product)
    .AsNoTrackingWithIdentityResolution()
    .ToList();

进阶建议:

测量方法:对比同一查询在 AsNoTracking 和 默认下的内存占用(dotnet-counters / Performance Profiler)与执行时间。

4) Include 的代价与 AsSplitQuery(避免笛卡尔爆炸)

问题:当使用 Include 加载多个一对多或多对多导航时,EF Core 默认会生成一个包含多个 JOIN 的单一 SQL(Single Query)。这会导致行重复(父行被子行重复展开),在父行与子行数量都较大时出现“笛卡尔爆炸”。

解决方案:

示例:

// 可能导致笛卡尔爆炸的单条查询
var q1 = context.Authors
    .Include(a => a.Books)
    .Include(a => a.Addresses)
    .ToList();

// 拆分查询以避免重复传输
var q2 = context.Authors
    .Include(a => a.Books)
    .Include(a => a.Addresses)
    .AsSplitQuery()
    .ToList();

注意:AsSplitQuery 会产生多条 SQL,所以对数据库连接/网络延迟敏感的场景需谨慎;另外,EF Core 从某些版本开始对 Include 的行为与默认策略进行了改进(请参考你使用的 EF Core 版本文档)。

进阶优化(超出本文四招,但非常有用):

示例:编译查询

private static readonly Func<MyDbContext, int, MyEntity?> _compiledById =
    EF.CompileQuery((MyDbContext ctx, int id) =>
        ctx.MyEntities.FirstOrDefault(e => e.Id == id));

// 使用
var entity = _compiledById(context, 123);

更多测量建议:

实用清单(工程化建议)

小结

EF Core 的性能调优不是单次行为,而是一个持续的工程实践:先测量、再优化、再验证。本文把常见的四个技巧扩展为可操作的工程建议,并补充了进阶实践(编译查询、批量操作、索引与执行计划分析)。按需逐项实施并以数据驱动决策,通常能在不牺牲可维护性的前提下把性能提升数倍。



Previous Post
从 N+1 到批量化:LINQ 查询性能优化的现代视角
Next Post
ASP.NET Core 中的四种限流策略详解与代码实现