Entity Framework Core:DBCommand 执行速度很快,但总时间慢了 10 倍

问题描述 投票:0回答:1

我正在使用 EF Core 3.1 + PostrgeSql。执行后:

var stopWatch = Stopwatch.StartNew();

var entity1 = await _context.Entity1
    .Include(e => e.Entity2)
    .Include(e => e.Entity3)
        .ThenInclude(e => e.Entity4)
    .FirstOrDefaultAsync(e => e.Id == someId);

stopWatch.Stop();
_logger.LogInformation($"Elapsed: {stopWatch.ElapsedMilliseconds} milliseconds.");

可以看到下一个日志:

[16:49:36 INF] Executed DbCommand (43ms) [Parameters=[@__Id_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT t."Id", ....
...
ORDER BY ....
[16:49:36 INF] Elapsed: 400 milliseconds.

我可以看到

DBCommand
被执行了
43ms
。如果我获取记录的 SELECT 查询并针对数据库执行它,则大约需要相同的时间。查询返回 45 列和 2000 行数据集。

但是整体时间几乎多了十倍:

400ms
.

问题是:

  • 额外的时间大约是多少?是 EF 创建 C# 对象所花费的时间吗?
  • 可以减少吗?

我尝试过的:

  • 我尝试使用
    AsNoTracking()
    。这里好像没有任何效果。
asp.net entity-framework
1个回答
0
投票

是的,EF 需要一些时间来构建和关联实体。为了进行公平比较,需要检查 DbContext 在测试之前是否已“预热”。针对 DbContext 执行的第一个查询将始终产生一次性配置成本。您的领域模型越大,此成本就越大。因此,如果这个测试有可能第一次针对 DbContext 执行:

var temp = _context.Entity1.Any(); // Run a simple initial query which will ensure the one-off config cost is not being counted.

var stopWatch = Stopwatch.StartNew();

var entity1 = await _context.Entity1
    .Include(e => e.Entity2)
    .Include(e => e.Entity3)
        .ThenInclude(e => e.Entity4)
    .FirstOrDefaultAsync(e => e.Id == someId);

stopWatch.Stop();
_logger.LogInformation($"Elapsed: {stopWatch.ElapsedMilliseconds} milliseconds.");

影响查询性能的其他因素是解析跟踪的实例,但如果您发现

AsNoTracking()
的性能没有什么不同,那么这不是您的情况的一个因素。 Hoewver,供将来参考,当您有一个已加载的 DbContext,并且正在跟踪相当数量的实体时,这些实体可能与特定查询中加载的实体相关,也可能不相关;如果您通过跟踪运行该查询,那么 DbContext 不仅会继续跟踪加载的任何实体的引用,而且还会遍历all相关实体来查找与您请求的实体相关的跟踪实例并将它们关联起来,无论您是否告诉它是否急切加载。

例如:

var children = _context.Children.Where(x => x.ParentId < 5).ToList();

var parent1 = _context.Parents.Single(x => x.ParentId == 1);
var parent6 = _context.Parents.Single(x => x.ParentId == 6);

当我们加载两个父级时,即使我们没有使用

Include
显式地急切加载子级,因为针对 DbContext 的早期跟踪查询加载了父级 1 到 5 的子级,当您检查父级 #1 时,它会显示它的子集合已加载,父集合 #6 不会。这可能会导致情境行为,并显示适用于两个父查询的性能成本,因为 DbContext 将扫描所有跟踪的引用以查看加载时是否有任何内容与任一父查询关联。即使子级已加载并被跟踪,使用
AsNoTracking()
加载父级也会导致仅返回父级,并且无需 DbContext 扫描跟踪实体以获取要填充的引用。

当您确实需要急切加载实体图时,提高性能的一个技巧是使用

AsSplitQuery()
减少笛卡尔积。它不是使用 JOIN 生成单个查询,而是为相关实体构建单独的查询,从而导致提取的记录少得多。在 1 条记录有 10 个 A、10 个 B、20 个 C 的情况下,笛卡尔会回拉 2000 行。使用
AsSplitQuery()
它将拉回 41 行。使用 AsSplitQuery() 并不是灵丹妙药,因为如果排序和分页等是查询的一个因素,则可能会出现问题。

最小化笛卡尔积和跟踪参考的影响的最佳方法是尽可能使用投影。不要获取实体及其亲属,而是使用

Select
ProjectTo
(自动映射器)从您需要的实体图中仅获取列。这会自动避免添加或搜索跟踪缓存的问题,并且使用较少的列,可以减小生成的笛卡尔坐标的大小,填充的结果通常比实体对象图更简单/更平坦。

希望这能为您提供一些参考途径。与原始 SQL 查询相比,EF 总是会产生一点额外费用,但通常不应该是速度慢很多倍的情况,并且为其增加的成本提供了很多功能。

© www.soinside.com 2019 - 2024. All rights reserved.