【问题标题】:LINQ Include slowing down performance when searchingLINQ 包括在搜索时降低性能
【发布时间】:2018-11-09 04:13:21
【问题描述】:

我们有以下方法允许我们在项目表中搜索 DataGrid:

public async Task<IEnumerable<Project>> GetFilteredProjects(string searchString)
{
    var projects = _context.Projects.Where(p => p.Current);

    projects.Include(p => p.Client);
    projects.Include(p => p.Architect);
    projects.Include(p => p.ProjectManager);

    if (!string.IsNullOrEmpty(searchString))
    {
        projects = projects
            .Where(p => p.NormalizedFullProjectName.Contains(searchString)
                    || p.Client.NormalizedName.Contains(searchString)
                    || p.Architect.NormalizedFullName.Contains(searchString)
                    || p.ProjectManager.NormalizedFullName.Contains(searchString));
    }

    projects = projects.OrderBy(p => p.Name).Take(10);

    return await projects.ToListAsync();
}

如果我们不在项目上使用Include,那么搜索是即时的。但是,将它们添加到搜索中后可能需要 3 秒以上。

我们需要包含其他实体,以便用户可以根据需要搜索它们。

我们如何能够提高性能但仍保留Include 以允许对其进行搜索?

如果没有Incldue,该方法如下所示:

public async Task<IEnumerable<Project>> GetFilteredProjects(string searchString)
{
    var projects = _context.Projects.Where(p => p.Current);

    if (!string.IsNullOrEmpty(searchString))
    {
        projects = projects
            .Where(p => p.Name.Contains(searchString));
    }

    projects = projects.OrderBy(p => p.Name).Take(10);

    return await projects.ToListAsync();
}

没有Include,性能如下:

Include:

【问题讨论】:

  • 查看任务管理器并检查内存使用情况。我怀疑 Include 会添加到您的内存中,并且发生交换会减慢运行时间。
  • 您是否在 SQL 数据库上运行过跟踪?可能是您缺少索引或其他内容。
  • “我们需要包含其他实体以允许用户在他们想要的时候搜索它们”——我认为这不是真的。搜索(Where 子句)与包含无关。
  • NormalizedFullName 字段,这是否也作为字段存储在数据库中?我只是在这里猜测,但该字段可能没有被索引。当我处理人名搜索时,我会使用姓氏,然后可能是名字。
  • 附注 - 在您的示例中 Includes 无效,因为您没有将返回的对象分配回 projects 变量。

标签: c# sql linq asp.net-core entity-framework-core


【解决方案1】:

简短的回答是,包含所有额外的实体需要时间和精力,因此会增加加载时间。

但是,您的假设存在缺陷:

我们需要包含其他实体,以便用户可以根据需要搜索它们。

这不是(必然)正确的。过滤发生在数据库级别。 Include 告诉实体框架加载来自数据库的记录。这是两个不同的东西。

看下面的例子:

_context.Projects
        .Include(p => p.Architect)
        .Where(p => p.Architect.Name == "Bob")
        .ToList()

这将为您提供一个名为 Bob 的项目(及其架构师)的列表。

_context.Projects
        .Where(p => p.Architect.Name == "Bob")
        .ToList()

这将为您提供一个名为 Bob 的建筑师的项目列表(不含建筑师);但它实际上并没有将Architect 对象加载到内存中。

_context.Projects
        .Include(p => p.Architect)
        .ToList()

这将为您提供项目列表(及其架构师)。它将包含每个项目,列表没有被过滤。


您只需要在想要进行内存中过滤时使用Include,即在已经从数据库加载的集合上。

你是否是这种情况取决于这部分:

    projects = projects
        .Where(p => p.NormalizedFullProjectName.Contains(searchString)
                || p.Client.NormalizedName.Contains(searchString)
                || p.Architect.NormalizedFullName.Contains(searchString)
                || p.ProjectManager.NormalizedFullName.Contains(searchString));

如果NormalizedFullProjectName(和其他属性)是数据库列,则 EF 能够在数据库级别执行过滤。您不需要 Include 来过滤项目。

如果NormalizedFullProjectName(和其他属性)不是数据库列,则 EF 必须首先将项目加载到内存中,然后才能应用过滤器。在这种情况下,您确实需要Include,因为架构师(和其他人)需要加载到内存中。


如果您只是出于过滤目的(而不是显示目的)加载相关实体,并且您正在数据库级别进行过滤;那么您可以简单地删除包含语句。

如果您需要加载这些相关实体(用于内存过滤或显示目的),那么您无法轻松删除 Include 语句,除非您编写一个 Select 指定您需要的字段需要。

例如:

_context.Projects
        .Select(p => new { Project = p, ArchitectName = p.Architect.Name })
        .ToList()

这将加载项目实体(全部),但只加载建筑师的姓名,不加载其他属性。如果您的相关实体有许多您当前不需要的属性,这可能会显着提升性能。

注意:当前示例使用匿名类型。我通常提倡为此创建一个自定义类型;但这与我们在这里解决的性能问题无关。


更新

根据您的更新,您似乎暗示预期的过滤发生在从数据库加载对象之后。

这是您的性能问题的根源。您正在获取大量数据,但只显示其中的一部分。没有显示出来的数据还需要加载,白费力气。

这里有不同的性能参数:

  • 全部加载一次 - 一次加载所有数据(可能需要很长时间),然后允许用户过滤加载的数据(非常快)
  • 加载块 - 仅加载与应用的过滤器匹配的数据。如果用户更改过滤器,您将再次加载数据。第一次加载会快得多,但与内存中过滤相比,后续过滤操作会花费更长的时间。

你应该在这里做什么不是我的决定。这是一个优先事项。一些客户更喜欢其中一种。我想说,在大多数情况下,第二个选项(加载块)在这里是更好的选择,因为如果用户从不查看 90% 的数据集,它可以防止不必要地加载大量数据集。这是对性能和网络负载的浪费。

我给出的答案适用于“加载块”方法。

如果您决定采用“一次加载所有内容”的方法,那么您将不得不接受初始加载对性能的影响。您可以做的最好的事情是严格限制返回的数据列(就像我在 Select 中展示的那样),以最大限度地降低性能/网络成本。

我认为没有合理的论据来混合这两种方法。你最终会遇到这两个缺点。

【讨论】:

  • 哇。多么好的答案。首先感谢。你说我们在过滤之前加载所有数据,我们认为这行:projects = projects.OrderBy(p =&gt; p.Name).Take(10); 意味着当在数据库上执行命令时,我们只取前 10 个,因此不加载整个集合。这是正确的假设吗?
  • @CBreeze:Queryable.Take returns a Queryable object,所以在我看来它还没有加载数据。据我所知,数据是通过调用枚举列表或返回不可枚举值(单个、第一个、总和、计数、最大值、...)的方法加载的
  • @CBreeze:另外,我认为Take 在这里不是问题。 Take 发生在 过滤相关实体之后;因此,即使Take 确实枚举了集合,您仍然可以先进行数据库内过滤。
  • @CBreeze 请注意,您正在使用 EF Core。与 EF6 不同,如果 Core 出于某种原因无法将您的查询转换为 SQL - 它会将所有数据拉入内存并在那里执行操作(除非您明确告诉它在这种情况下抛出异常)。这可能会或可能不会发生在您的情况下,但值得牢记。
猜你喜欢
  • 2015-03-04
  • 1970-01-01
  • 2014-02-15
  • 1970-01-01
  • 2020-06-02
  • 1970-01-01
  • 2014-08-22
  • 1970-01-01
  • 2016-02-25
相关资源
最近更新 更多