是的,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()
的性能没有差异,那么这不是您的情况下的一个因素.如果您有一个已经加载的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);
当我们加载两个父对象时,即使我们没有显式地Eager 地加载子对象Include
,因为前面跟踪的针对DbContext的查询加载了父对象1到5的子对象,当您判断父对象#1时,它将加载其子对象集合,而父对象#6不会.这可能导致情景行为,并显示适用于两个父查询的性能成本,因为DbContext将扫描所有跟踪的引用,以查看加载时是否有任何内容与父查询相关联.加载带有AsNoTracking()
的父项,即使加载和跟踪子项也会导致只返回父项,而不会产生DbContext扫描被跟踪实体以查找要填充的引用的开销.
当您确实需要紧急加载实体图时,提高性能的一个提示是使用AsSplitQuery()
来减少笛卡尔乘积.它不是生成带有联接的单个查询,而是为相关实体构建单独的查询,从而拉取的记录要少得多.在1个记录有10个A和10个B、20个C的情况下,笛卡尔回调2000行.如果有AsSplitQuery()
行,它将回调41行.使用AsSplitQuery()不是灵丹妙药,因为如果排序和分页是查询的一个因素,则可能会出现问题.
最大限度地减少Cartestian产品和跟踪引用的影响的最佳方法是尽可能多地使用投影.不要获取实体及其相关项,而是使用Select
或ProjectTo
(Automapper)从实体图中获取所需的列.这自动避免了添加或搜索跟踪缓存的问题,并且在列较少的情况下,可以减小所得到的笛卡尔坐标的大小,其中填充的结果通常比实体对象图更简单/更平坦.
希望这能给你提供一些可供参考的途径.与原始SQL相比,EF总是会带来一些额外的查询,但它通常不应该慢很多倍,并且提供了大量的功能来弥补它确实增加的成本.