【问题标题】:LINQ Search nvarchar(MAX) column extremely slow using .Contains()使用 .Contains() 的 LINQ 搜索 nvarchar(MAX) 列非常慢
【发布时间】:2019-12-12 12:05:56
【问题描述】:

我有一个 .net 核心 API,我正在尝试使用 .Contains() 搜索 440 万条记录。这显然非常慢 - 26 秒。我只是查询一列,它是记录的名称。在处理数百万条记录时,这个问题一般如何解决?

我之前从未处理过数百万条记录,因此除了明显更改 .Select 和 .Take 之外,我还没有尝试过任何过于激烈的操作。不过,我已经为此花费了很多时间。

.Where 中包含的其他过滤器仅在用户选择在前端使用它们时使用 - 真正的问题只是按 CompanyName 搜索。

注意;我在返回结果时使用 .ToArray()。

我在数据库中有索引,但无法为 CompanyName 添加索引,因为它是 Nvarchar(MAX)。

我也查看了执行计划,它并没有真正显示出任何异常。

query = _context.Companies.Where(
    c => c.CompanyName.Contains(paging.SearchCriteria.companyNameFilter.ToUpper())
         && c.CompanyNumber.StartsWith(
                string.IsNullOrEmpty(paging.SearchCriteria.companyNumberFilter)
                ? paging.SearchCriteria.companyNumberFilter.ToUpper()
                : ""
            )
         && c.IncorporationDate > paging.SearchCriteria.companyIncorperatedGreaterFilter
         && c.IncorporationDate < paging.SearchCriteria.companyIncorperatedLessThanFilter
    )
    .Select(x => new Company() {
                    CompanyName = x.CompanyName,
                    IncorporationDate = x.IncorporationDate,
                    CompanyNumber = x.CompanyNumber
                }
    )
    .Take(10);

我预计查询大约需要 1 / 2 秒,因为当我在 ssms 中执行类似查询时大约需要 1 / 2 秒。

这是提交给数据库的代码:

Microsoft.EntityFrameworkCore.Database.Command: Information: Executing DbCommand [Parameters=[@__p_4='?' (DbType = Int32), @__ToUpper_0='?' (Size = 4000), @__p_1='?' (Size = 4000), @__paging_SearchCriteria_companyIncorperatedGreaterFilter_2='?' (DbType = DateTime2), @__paging_SearchCriteria_companyIncorperatedLessThanFilter_3='?' (DbType = DateTime2), @__p_5='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [t].[CompanyName], [t].[IncorporationDate], [t].[CompanyNumber]
FROM (
    SELECT TOP(@__p_4) [c].[CompanyName], [c].[IncorporationDate], [c].[CompanyNumber], [c].[ID]
    FROM [Companies] AS [c]
    WHERE (((((@__ToUpper_0 = N'') AND @__ToUpper_0 IS NOT NULL) OR (CHARINDEX(@__ToUpper_0, [c].[CompanyName]) > 0)) AND (((@__p_1 = N'') AND @__p_1 IS NOT NULL) OR ([c].[CompanyNumber] IS NOT NULL AND (@__p_1 IS NOT NULL AND (([c].[CompanyNumber] LIKE [c].[CompanyNumber] + N'%') AND (((LEFT([c].[CompanyNumber], LEN(@__p_1)) = @__p_1) AND (LEFT([c].[CompanyNumber], LEN(@__p_1)) IS NOT NULL AND @__p_1 IS NOT NULL)) OR (LEFT([c].[CompanyNumber], LEN(@__p_1)) IS NULL AND @__p_1 IS NULL))))))) AND ([c].[IncorporationDate] > @__paging_SearchCriteria_companyIncorperatedGreaterFilter_2)) AND ([c].[IncorporationDate] < @__paging_SearchCriteria_companyIncorperatedLessThanFilter_3)
) AS [t]
ORDER BY [t].[IncorporationDate] DESC
OFFSET @__p_5 ROWS FETCH NEXT @__p_4 ROWS ONLY

已解决!在这两个答案的帮助下!

最后,按照建议,我尝试了全文搜索,这种搜索速度很快,但搜索结果的准确性有所降低。为了更准确地过滤这些结果,我在应用全文搜索后在查询中使用了 .Contains。

这是有效的代码。希望这对其他人有所帮助。

//查询 = _context.Companies //.Where(c => c.CompanyName.StartsWith(paging.SearchCriteria.companyNameFilter.ToUpper()) //&& c.CompanyNumber.StartsWith(string.IsNullOrEmpty(paging.SearchCriteria.companyNumberFilter) ? paging.SearchCriteria.companyNumberFilter.ToUpper() : "") //&& c.IncorporationDate > paging.SearchCriteria.companyIncorperatedGreaterFilter && c.IncorporationDate new Company() { CompanyName = x.CompanyName, IncorporationDate = x.IncorporationDate, CompanyNumber = x.CompanyNumber }).Take(10);

            query = _context.Companies.Where(c => EF.Functions.FreeText(c.CompanyName, paging.SearchCriteria.companyNameFilter.ToUpper()));

            query = query.Where(x => x.CompanyName.Contains(paging.SearchCriteria.companyNameFilter.ToUpper()));

(为简单起见,我暂时排除了其他过滤器)

【问题讨论】:

  • 您能否更新您的问题并显示您在 SSMS 中使用的 SQL 查询?
  • 请与我们分享确切 SQL 正在提交到服务器 - stackoverflow.com/a/44180537/34092

标签: c# sql linq .net-core


【解决方案1】:

当您在 SSMS 中运行查询时,它可能会被缓存以供后续调用。原始查询可能花费与 EF 查询相似的时间。也就是说,参数化查询有一些缺点 - 虽然您可以在参数化查询中更好地重用执行计划,但这也意味着执行计划不一定是您现在尝试运行的实际查询的最佳选择。

例如,如果您指定一个 CompanyNumber(由于 StartsWith,在索引中很容易找到),您可以先通过 CompanyNumber 过滤数据,从而使名称搜索变得简单(我假设 CompanyNumber 是唯一的,所以要么你得到 0 条记录,要么你得到你通过 CompanyNumber 得到的一条)。如果参数化查询的执行计划针对按名称查找进行了优化,则这可能无法实现。

但归根结底,Contains 是性能杀手。它需要读取表的 CompanyName 字段中的每一个数据字节;这通常意味着它必须读取每一行,并处理其大部分数据。按子字符串搜索看起来很简单,但总是会带来严重的惩罚——它的复杂性与数据大小成线性关系。

一种选择是找到一种方法来避免Contains。用户经常要求他们实际上不需要的功能。 StartsWith 在大多数情况下可能同样有效。但这当然是一个商业决定。

另一种选择是在应用Contains 过滤器之前尽可能减少查询 - 如果您只允许使用其他过滤器缩小搜索范围来搜索公司名称,则可以保存数据库服务器很多工作。这可能很棘手,并且有时会与执行计划冲突问题发生冲突——您可能需要添加一些方法来避免对两个截然不同的查询使用相同的执行计划; EF 中的一种简单方法是动态构建查询,而不是尝试使用一个表达式:

var query = _context.Companies;
if (!string.IsNullOrEmpty(paging.SearchCriteria.companyNameFilter))
  query = query.Where(c => c.CompanyName.Contains(paging.SearchCriteria.companyNameFilter));
if (!string.IsNullOrEmpty(paging.SearchCriteria.companyNumberFilter))
  query = query.Where(c => c.CompanyNumber.StartsWith(paging.SearchCriteria.companyNumberFilter));

// etc. for the rest of the query

这意味着您实际上有多个参数化查询,每个查询都可以有自己的执行计划,更符合查询的实际操作。对于某些极端情况,完全防止执行计划缓存也可能是值得的(这在报告中通常很有用)。

最后一个选项是使用全文搜索。你可以找到很多关于如何完成这项工作的教程。这实质上是通过将未格式化的字符串数据拆分为单个单词或短语,并对它们进行索引来实现的。这意味着搜索“hello world”不一定会返回名称中包含“hello world”的所有记录,它还可能返回名称中包含“hello world”以外的其他内容的记录。想想谷歌搜索而不是Contains。对于人工编写的文本,这通常是一种很好的方法,但对于不明白为什么您会返回与他正在搜索的内容完全不同的搜索结果的用户来说,这可能会非常令人困惑。如果您需要进行部分搜索(例如,搜索“Computer”可能会返回“Computer, Inc.”,但搜索“Comp”可能不会返回任何内容),它通常也无法正常工作。

第一个选项可能是最快的,并且最接近用户的期望。但是,它有一个弱点,它不能在中间搜索。第二个选项是最正确的,并且可能使您的查询速度大大加快,尤其是在具有良好统计信息的最常见情况下。第三个选项可能与第一个选项一样快,但正确设置可能会很棘手,并且可能会让您的用户感到困惑。它还为您提供了更强大的方式来查询文本数据(例如使用通配符)。

【讨论】:

    【解决方案2】:

    欢迎来到堆栈溢出。看起来您的代码和架构中至少存在这三个问题之一。

    首先:索引

    您已经提到这不能被索引,但至少在 SQL Server 中支持full text indexing

    .Contains

    此方法并不完全适合您正在执行的操作规模。如果可能,也许作为最后的手段,考虑转向参数化查询。然而,目前看来,您希望将业务逻辑保留在 .net 代码中,而不是将其传播到 SQL 中,这是一个值得的计划。

    c.IncorporationDate

    在 SQL Server 中进行日期比较可能有点昂贵。一旦您处理了数百万行,您可能会从correctly partitioned tables and indexes 获得很多的性能优势。

    考虑这些行是否可以更改。名为IncoporationDate 的东西听起来绝对不应该改变。我怀疑您可能想在阅读其余内容后利用这一点。

    【讨论】:

    • 非常感谢您及时详细的回复。作为初学者,我不知道全文索引,所以感谢您让我知道。我阅读了这个并实施了 - 添加目录和填充等,但不幸的是,它没有任何区别。那么,在处理数百万条记录时,参数化查询是最佳方式吗?
    猜你喜欢
    • 2018-03-19
    • 2022-11-12
    • 1970-01-01
    • 1970-01-01
    • 2016-03-12
    • 2016-08-20
    • 2018-04-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多