Skip to content
Go back

EF Core 预优化指南:5 个写出高性能查询的技巧

预先优化的 EF Query 技术:5 个成功步骤

背景

我们经常听到关于优化某段代码或查询的讨论,这通常指的是**“重构一开始没有写好的代码”**。

例如,有人在 foreach 循环中放置了数据库调用。过了一段时间,他发现这是一个性能问题,于是“优化”了代码。实际上,这并不叫优化,这只是把代码改成了一开始就该有的样子。

对于 Entity Framework Core,我们可以遵循许多规则来避免这些低级错误。与其等待性能瓶颈出现再去亡羊补牢,不如在编写代码的最初阶段就采用“预优化”的思维。即使是初学者,也应该尽量在第一遍写代码时就符合这些最佳实践。

今天,我将介绍编写 EF Core 查询时应遵循的 5 个基本技巧。

1. 只获取你需要的字段 (Projection)

这个技巧的核心是通过只选择特定操作所需的字段,而不是获取整个实体对象,来优化数据检索。这种方法可以显著提高应用程序的性能。

反例与正例:

假设数据库中的 Employees 表有许多字段,但在展示层我们只需要名字和邮箱。

// 推荐做法:使用 Select 进行投影
var employeeList = context.Employees
    .Select(e => new EmployeeDto
    {
        Name = e.Name,
        Email = e.Email
    })
    .ToList();

通过这样做,我们避免了“提取所有数据”的浪费。

为什么要这样做?

  1. 减少数据传输 (Reduced Data Transfer):获取更少的字段意味着从数据库传输到应用程序的数据量更少,这对网络性能至关重要。
  2. 降低内存使用 (Lower Memory Usage):加载更少的字段意味着在应用程序中占用更少的内存,这对于处理大数据集非常重要。
  3. 提高查询性能 (Improved Query Performance):数据库引擎检索和传输的数据更少,查询执行速度更快。
  4. 减少实体跟踪开销 (Decreased Entity Tracking Overhead):投影出的 DTO 或匿名对象通常不会被 EF Core 更改追踪器跟踪,节省了额外的开销(除非你显式要求)。

2. 避免 N+1 查询

N+1 查询是数据库操作中常见的性能杀手,尤其是在使用 ORM 时。它发生在你先使用 1 个查询获取对象列表,然后循环遍历这些对象,为每个对象再执行 1 个查询来获取相关数据。如果是 100 个对象,就是 1 + 100 次查询。

问题代码:

// 1 次查询
var blogs = context.Blogs.ToList();

foreach (var blog in blogs)
{
    // N 次查询:每次访问 Posts 都会触发数据库调用
    var posts = blog.Posts; 

    foreach (var post in posts)
    {
        Console.WriteLine(post.Title);
    }
}

修复代码 (Eager Loading):

// 在一次查询中获取所有博客及其文章
var blogs = context.Blogs
    .Include(b => b.Posts)
    .ToList();

foreach (var blog in blogs)
{
    // 数据已在内存中,无需额外查询
    foreach (var post in blog.Posts)
    {
        Console.WriteLine(post.Title);
    }
}

关键点:

3. 使用 .AsNoTracking()

.AsNoTracking() 是一个非常有效的性能优化手段,适用于只读场景(即你只打算读取数据,而不打算更新或删除它)。

示例:

var products = context.Products
    .AsNoTracking()
    .ToList();
// 仅用于展示,不进行修改

为什么要这样做?

  1. 性能提升:在大规模应用或大数据集下效果尤为明显。查询执行更快,内存占用更少。
  2. 减少开销:EF Core 不需要为这些实体建立快照或维护状态信息,直接跳过更改追踪逻辑。

注意事项:

4. 避免笛卡尔积爆炸 (Cartesian Explosion)

笛卡尔积爆炸是指查询由于不正确的连接 (Join) 方式,意外产生了不成比例的大量记录,严重影响性能。

反例 (笛卡尔积):

假设我们想列出所有书籍及其作者,但写出了错误的 LINQ 查询:

// 错误查询:导致笛卡尔积
var query = from a in context.Authors
            from b in context.Books
            select new { a.Name, b.Title };

var results = query.ToList(); 

在这个查询中,我们错误地将每个作者与每本书进行了组合(Cross Join),无论这本书是否由该作者编写。如果作者有 100 人,书有 1000 本,结果将是 100,000 条记录。

正例 (正确连接):

// 正确查询:使用 Join
var query = from a in context.Authors
            join b in context.Books on a.AuthorId equals b.AuthorId
            select new { a.Name, b.Title };

var results = query.ToList();

影响:

5. 使用 AsSplitQuery()

在 EF Core 5.0 及更高版本中,.AsSplitQuery() 允许将包含集合关联的单一查询拆分为多个 SQL 查询。这在处理复杂查询或大型数据集时能显著提高性能。

背景:

默认情况下,EF Core 会尝试使用 JOIN 在单个 SQL 查询中获取所有相关数据。对于简单关系这很有效,但如果你加载一个包含大量子实体的集合(例如一名作者有 1000 本书),单个 JOIN 查询会导致主实体的数据(作者信息)在每一行子实体(书)中重复,造成数据冗余和传输浪费(这也称为数据爆炸)。

未使用 AsSplitQuery (默认行为):

var authors = context.Authors
    .Include(a => a.Books)
    .ToList();
// 生成 1 条 SQL,使用 LEFT JOIN,可能导致数据冗余传输

使用 AsSplitQuery (优化后):

var authors = context.Authors
    .Include(a => a.Books)
    .AsSplitQuery()
    .ToList();
// 生成 2 条 SQL:一条查 Authors,一条查 Books,然后在内存中组装

收益与权衡:

总结

不要等到由于粗心和代码写得不好而遇到性能问题时可以“优化”。从一开始就尽力遵循这些基本原则。考虑到 EF Core 如今的强大功能,只要遵循这些基础,通常就能满足绝大多数性能需求。

除了这些,还有很多其他的最佳实践值得学习,但这 5 点是迈向高性能 EF Core 之旅的坚实第一步。


参考资料:


标签


Previous Post
.NET 10 和 C# 14 新特性:API 请求/响应管道增强
Next Post
从第一性原理解决消息顺序问题