EFCore性能优化方案-优化查询

数据库性能是一个宏大而复杂的主题,涉及整个组件堆栈:数据库、网络、数据库驱动程序和数据访问层(如 EF Core)。 尽管高级层和 O/RM(如 EF Core)大大简化了应用程序开发并改善了可维护性,但它们有时可能是不透明的,隐藏了性能关键的内部详细信息,例如正在执行的 SQL。

一些让人诟病的性能问题一直是热议话题,实际情况可能并不是性能差,而是需要掌握如何规避陷阱和避开影响性能的坑。

1.正确使用索引

查询能否快速运行的主要决定因素是它是否在恰当的位置使用索引:数据库通常用于保存大量数据,而遍历整个表的查询往往是严重性能问题的根源。

// 在开始时匹配,使用索引 (SQL Server)
var posts1 = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
// 在末尾匹配,不使用索引
var posts2 = context.Posts.Where(p => p.Title.EndsWith("A")).ToList();
2.只查询需要的属性

EF Core 能非常轻松地查询出实体实例,然后将它们用于代码中。 但是,查询实体实例可能会频繁从数据库中拉取回超出所需的数据。 考虑以下情况:

foreach (var blog in context.Blogs)
{
 Console.WriteLine("Blog: " + blog.Url);
}

SQL

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]...
FROM [Blogs]

对于这一点的优化方法是,使用 Select 告诉 EF 要查询出的列:

foreach (var blogName in context.Blogs.Select(b => b.Url))
{
  Console.WriteLine("Blog: " + blogName);
}

SQL

SELECT [b].[Url]
FROM [Blogs] AS [b]

这种方法对只读查询非常有用,但如果你需要更新提取的Blog,事情会变得复杂,因为 EF 的更改跟踪仅适用于实体实例。

3.限制结果集大小

返回的行数取决于数据库中的实际数据,因此不可能知道将从数据库中加载的数据量,结果占用的内存量以及处理这些结果时(例如,通过网络将它们发送到用户浏览器)将额外生成的负载量。

var blogs25 = context.Posts
 .Where(p => p.Title.StartsWith("A"))
 .Take(5)
 .ToList();
4.优先使用Async 异步方法

EF Core 中提供了很多形如xxxAsync 的异步方法,推荐使用这些方法提高吞吐量和性能,减少不必要的延时等待。

5.关闭状态追踪

跟踪行为决定了 EF Core 是否将有关实体实例的快照信息保留在其更改跟踪器中。 如果已跟踪某个实体,则该实体中检测到的任何更改都会在 SaveChanges() 期间永久保存到数据库。当决定只查询数据,不更改数据时,非跟踪查询十分有用,非跟踪查询的执行会更快,因为无需为查询实体设置快照跟踪信息。 如果不需要更新从数据库中检索到的实体,则应优先使用非跟踪查询。

//单体查询关闭:
AsNoTracking()
//整个上下文关闭:
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
6.关闭DetectChanges 状态同步

当从数据库进行查询数据时,上下文便捕获了每个实体属性的快照(数据库值,原始值,当前值),当调用SaveChanges 时,在内部会自动调用 DetectChanges 方法,此方法将扫描上下文中所有实体,并比较当前属性值和存储在快照中的原始属性值,如果被找到的属性值发生了改变,此时EF将会与数据库进行交互,进行数据更 新。

会导致自动调用 DetectChanges 方法: Find、Local、Remove、Add、Update、Attach、SaveChanges 和

Entry 等。

但是,自动同步状态会频繁调用,可手动关闭以上方法的自动同步,当数据都修改好后,一次性手动同步。

context.ChangeTracker.AutoDetectChangesEnabled = false;
//执行操作后手动同步状态
context.ChangeTracker.DetectChanges();
7.使用 EF.Functions.xxx 进行模糊查询

EF Core 支持使用 StartsWith、Contains 和 EndsWith 方法进行模糊查询,这些方法被翻译成 LIKE 语句,但为了提高性能也提供了一个EF.Functions.Like 方式,这种方式生成的 SQL 语句性能更优。

var result= context.Blogs.Where(b => EF.Functions.Like(b.BlogName, "%xcode%"))
8.高效分页

分页是指在页面中检索结果,而不是一次性检索结果;这通常针对大型结果集完成,其中显示用户界面,允许用户导航到结果的下一页或上一页。

9.缓冲和流式处理

缓冲指将所有查询结果加载到内存中,而流式处理意味着 EF 每次向应用程序提供一个结果,绝不让内存中包含整个结果集。 原则上,流式处理查询的内存要求是固定的:无论查询返回 1 行还是 1000 行,内存要求都相同。另一方面,返回的行数越多,缓冲查询需要的内存越多。 对于产生大型结果集的查询,这可能是一个重要的性能因素。

// oList和ToArray会导致整个结果集被缓冲:
var blogsList = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
var blogsArray = context.Posts.Where(p => p.Title.StartsWith("A")).ToArray();
​
// foreach流式,每次处理一行:
foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")))
{
 // ...
}

如果查询只返回几个结果,可能无需担心这一点。 但是,如果查询可能返回的行数非常多,则有必要考虑使用流式处理而不是缓冲。

10.预先加载相关实体

在ORM中有一个经典的问题,那就是N+1问题,一般出现在一对多查询中。N+1问题是这样产生的:查询主表一次,得到N条记录,根据N条记录,查询关联副表,更需要N次。在数据量较大的情况下,是非常消耗数据路性能的。

EFCore中预先加载表示从数据库中加载关联数据,作为初始查询的一部分,这样就可以在一次往返中提取所有必需的数据。

使用 Include 方法来指定要包含在查询结果中的关联数据:

var blogs = context.Blogs
 .Include(blog => blog.Posts)
 .ToList();

任何技术都有两面性,使用N+1 模式的优点是可以单独缓存部分数据。

11.使用原始 SQL

在某些情况下,你的查询存在更优化的 SQL,而 EF 不能生成这种 SQL。 在这些情况下,手动编写 SQL 可以显著提高性能,例如通过 FromSqlRaw.

猜你喜欢

转载自blog.csdn.net/gcf10080353/article/details/131716268