实体框架 + LINQ + “包含” == 超慢

本文关键字:超慢 包含 框架 LINQ 实体 | 更新日期: 2023-09-27 18:30:37

试图重构一些最近变得非常慢的代码,我遇到了一个需要 5+ 秒才能执行的代码块。

该代码由 2 个语句组成:

IEnumerable<int> StudentIds = _entities.Filters
                    .Where(x => x.TeacherId == Profile.TeacherId.Value && x.StudentId != null)
                    .Select(x => x.StudentId)
                    .Distinct<int>();

_entities.StudentClassrooms
                    .Include("ClassroomTerm.Classroom.School.District")
                    .Include("ClassroomTerm.Teacher.Profile")
                    .Include("Student")
                    .Where(x => StudentIds.Contains(x.StudentId)
                    && x.ClassroomTerm.IsActive
                    && x.ClassroomTerm.Classroom.IsActive
                    && x.ClassroomTerm.Classroom.School.IsActive
                    && x.ClassroomTerm.Classroom.School.District.IsActive).AsQueryable<StudentClassroom>();

所以这有点混乱,但首先我从一个表(过滤器)中获取一个 Id 的不同列表,然后我使用它查询另一个表。

这些是相对较小的表,但仍然是 5+ 秒的查询时间。

我把它放在 LINQPad 中,它表明它首先执行底部查询,然后运行 1000 个"不同"查询。

一时兴起,我通过添加 .ToArray() 在最后。 这提高了 1000 倍的速度......现在大约需要 100 毫秒才能完成相同的查询。

怎么回事? 我做错了什么?

实体框架 + LINQ + “包含” == 超慢

这是

Linq 中延迟执行的缺陷之一: 在您的第一种方法中,StudentIds实际上是一个IQueryable,而不是内存中的集合。这意味着在第二个查询中使用它将在数据库上再次运行查询 - 每次。

使用 ToArray() 强制执行第一个查询会使StudentIds成为内存中的集合,第二个查询中的Contains部分将在此包含固定项目序列的集合上运行 - 这将映射到等效于 SQL where StudentId in (1,2,3,4)查询的内容。

当然,此

查询的速度要快得多,因为您预先确定了此序列一次,而不是每次执行Where子句时。不使用ToArray()的第二个查询(我认为)将映射到具有where exists (...)子查询的 SQL 查询,该子查询针对每一行进行评估。

ToArray() 具体化对服务器内存的初始查询。

我的猜测是查询提供程序无法解析表达式StudentIds.Contains(x.StudentId)。因此,它可能认为studentIds是一个已经加载到内存中的数组。因此,它可能在解析阶段一遍又一遍地查询数据库。确定的唯一方法是设置探查器。

如果需要在数据库服务器上执行此操作,请使用连接,而不是"包含"。如果需要使用 包含 来执行看起来像联接问题的操作,则很可能缺少代理项主键或某处的外键。

您也可以将studentIds声明为 IQueryable 而不是 IEnumerable。这可能会为查询提供程序提供将studentIds解释为表达式(又名表达式)所需的提示。数据尚未加载到内存。我不知何故对此表示怀疑,但值得一试。

如果所有其他方法都失败,请使用 ToArray() 。这会将初始studentIds加载到内存中。