【问题标题】:SQL String comparison speed 'like' vs 'patindex'SQL 字符串比较速度 'like' 与 'patindex'
【发布时间】:2011-12-24 13:02:55
【问题描述】:

我有一个查询如下(简化)...

SELECT     *
FROM       table1 AS a
INNER JOIN table2 AS b ON (a.name LIKE '%' + b.name + '%')

对于我的数据集,这需要大约 90 秒的时间来执行,所以我一直在寻找加速它的方法。无缘无故,我想我会尝试 PATINDEX 而不是 LIKE...

SELECT     *
FROM       table1 AS a
INNER JOIN table2 AS b ON (PATINDEX('%' + b.name + '%', a.name) > 0)

在同一数据集上,这会在眨眼间执行并返回相同的结果。

谁能解释为什么 LIKE 比 PATINDEX 慢得多?鉴于 LIKE 只是返回一个 BOOLEAN 而 PATINDEX 正在返回实际位置,我原以为后者会更慢,或者仅仅是两个函数的编写效率问题?

好的,这是每个查询的完整内容,然后是其执行计划。 “#StakeholderNames”只是我匹配的可能名称的临时表。

我已提取实时数据并多次运行每个查询。第一个大约需要 17 秒(比实时数据库上原来的 90 秒要少一些),第二个不到 1 秒...

SELECT              sh.StakeholderID,
                    sh.HoldingID,
                    i.AgencyCommissionImportID,
                    1

    FROM            AgencyCommissionImport AS i
    INNER JOIN      #StakeholderNames AS sn ON REPLACE(REPLACE(i.ClientName,' ',''), ',','') LIKE '%' + sn.Name + '%'
    INNER JOIN      Holding AS h ON (h.ProviderName = i.Provider) AND (h.HoldingReference = i.PlanNumber)
    INNER JOIN      StakeholderHolding AS sh ON (sn.StakeholderID = sh.StakeholderID) AND (h.HoldingID = sh.HoldingID)
    WHERE           i.AgencyCommissionFileID = @AgencyCommissionFileID
                AND (i.MatchTypeID = 0)
                AND ((i.MatchedHoldingID IS NULL)
                    OR (i.MatchedStakeholderID IS NULL))

   |--Table Insert(OBJECT:([tempdb].[dbo].[#Results]), SET:([#Results].[StakeholderID] = [AttivoGroup_copy].[dbo].[StakeholderHolding].[StakeholderID] as [sh].[StakeholderID],[#Results].[HoldingID] = [AttivoGroup_copy].[dbo].[StakeholderHolding].[HoldingID] as [sh].[HoldingID],[#Results].[AgencyCommissionImportID] = [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[AgencyCommissionImportID] as [i].[AgencyCommissionImportID],[#Results].[MatchTypeID] = [Expr1014],[#Results].[indx] = [Expr1013]))
        |--Compute Scalar(DEFINE:([Expr1014]=(1)))
             |--Compute Scalar(DEFINE:([Expr1013]=getidentity((1835869607),(2),N'#Results')))
                  |--Top(ROWCOUNT est 0)
                       |--Hash Match(Inner Join, HASH:([h].[ProviderName], [h].[HoldingReference])=([i].[Provider], [i].[PlanNumber]), RESIDUAL:([AttivoGroup_copy].[dbo].[Holding].[ProviderName] as [h].[ProviderName]=[AttivoGroup_copy].[dbo].[AgencyCommissionImport].[Provider] as [i].[Provider] AND [AttivoGroup_copy].[dbo].[Holding].[HoldingReference] as [h].[HoldingReference]=[AttivoGroup_copy].[dbo].[AgencyCommissionImport].[PlanNumber] as [i].[PlanNumber] AND [Expr1015] like [Expr1016]))
                            |--Nested Loops(Inner Join, OUTER REFERENCES:([sh].[HoldingID]))
                            |    |--Nested Loops(Inner Join, OUTER REFERENCES:([sn].[StakeholderID]))
                            |    |    |--Compute Scalar(DEFINE:([Expr1016]=('%'+#StakeholderNames.[Name] as [sn].[Name])+'%', [Expr1017]=LikeRangeStart(('%'+#StakeholderNames.[Name] as [sn].[Name])+'%'), [Expr1018]=LikeRangeEnd(('%'+#StakeholderNames.[Name] as [sn].[Name])+'%'), [Expr1019]=LikeRangeInfo(('%'+#StakeholderNames.[Name] as [sn].[Name])+'%')))
                            |    |    |    |--Table Scan(OBJECT:([tempdb].[dbo].[#StakeholderNames] AS [sn]))
                            |    |    |--Clustered Index Seek(OBJECT:([AttivoGroup_copy].[dbo].[StakeholderHolding].[PK_StakeholderHolding] AS [sh]), SEEK:([sh].[StakeholderID]=#StakeholderNames.[StakeholderID] as [sn].[StakeholderID]) ORDERED FORWARD)
                            |    |--Clustered Index Seek(OBJECT:([AttivoGroup_copy].[dbo].[Holding].[PK_Holding] AS [h]), SEEK:([h].[HoldingID]=[AttivoGroup_copy].[dbo].[StakeholderHolding].[HoldingID] as [sh].[HoldingID]) ORDERED FORWARD)
                            |--Compute Scalar(DEFINE:([Expr1015]=replace(replace([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[ClientName] as [i].[ClientName],' ',''),',','')))
                                 |--Clustered Index Scan(OBJECT:([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[PK_AgencyCommissionImport] AS [i]), WHERE:([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[AgencyCommissionFileID] as [i].[AgencyCommissionFileID]=[@AgencyCommissionFileID] AND [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchTypeID] as [i].[MatchTypeID]=(0) AND ([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchedHoldingID] as [i].[MatchedHoldingID] IS NULL OR [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchedStakeholderID] as [i].[MatchedStakeholderID] IS NULL)))


SELECT              sh.StakeholderID,
                    sh.HoldingID,
                    i.AgencyCommissionImportID,
                    1

    FROM            AgencyCommissionImport AS i
    INNER JOIN      #StakeholderNames AS sn ON (PATINDEX('%' + sn.Name + '%', REPLACE(REPLACE(i.ClientName,' ',''), ',','')) > 0)
    INNER JOIN      Holding AS h ON (h.ProviderName = i.Provider) AND (h.HoldingReference = i.PlanNumber)
    INNER JOIN      StakeholderHolding AS sh ON (sn.StakeholderID = sh.StakeholderID) AND (h.HoldingID = sh.HoldingID)
    WHERE           i.AgencyCommissionFileID = @AgencyCommissionFileID
                AND (i.MatchTypeID = 0)
                AND ((i.MatchedHoldingID IS NULL)
                    OR (i.MatchedStakeholderID IS NULL))

   |--Table Insert(OBJECT:([tempdb].[dbo].[#Results]), SET:([#Results].[StakeholderID] = [AttivoGroup_copy].[dbo].[StakeholderHolding].[StakeholderID] as [sh].[StakeholderID],[#Results].[HoldingID] = [AttivoGroup_copy].[dbo].[StakeholderHolding].[HoldingID] as [sh].[HoldingID],[#Results].[AgencyCommissionImportID] = [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[AgencyCommissionImportID] as [i].[AgencyCommissionImportID],[#Results].[MatchTypeID] = [Expr1014],[#Results].[indx] = [Expr1013]))
        |--Compute Scalar(DEFINE:([Expr1014]=(1)))
             |--Compute Scalar(DEFINE:([Expr1013]=getidentity((1867869721),(2),N'#Results')))
                  |--Top(ROWCOUNT est 0)
                       |--Hash Match(Inner Join, HASH:([h].[ProviderName], [h].[HoldingReference])=([i].[Provider], [i].[PlanNumber]), RESIDUAL:([AttivoGroup_copy].[dbo].[Holding].[ProviderName] as [h].[ProviderName]=[AttivoGroup_copy].[dbo].[AgencyCommissionImport].[Provider] as [i].[Provider] AND [AttivoGroup_copy].[dbo].[Holding].[HoldingReference] as [h].[HoldingReference]=[AttivoGroup_copy].[dbo].[AgencyCommissionImport].[PlanNumber] as [i].[PlanNumber] AND patindex([Expr1015],[Expr1016])>(0)))
                            |--Nested Loops(Inner Join, OUTER REFERENCES:([sh].[HoldingID]))
                            |    |--Nested Loops(Inner Join, OUTER REFERENCES:([sn].[StakeholderID]))
                            |    |    |--Compute Scalar(DEFINE:([Expr1015]=('%'+#StakeholderNames.[Name] as [sn].[Name])+'%'))
                            |    |    |    |--Table Scan(OBJECT:([tempdb].[dbo].[#StakeholderNames] AS [sn]))
                            |    |    |--Clustered Index Seek(OBJECT:([AttivoGroup_copy].[dbo].[StakeholderHolding].[PK_StakeholderHolding] AS [sh]), SEEK:([sh].[StakeholderID]=#StakeholderNames.[StakeholderID] as [sn].[StakeholderID]) ORDERED FORWARD)
                            |    |--Clustered Index Seek(OBJECT:([AttivoGroup_copy].[dbo].[Holding].[PK_Holding] AS [h]), SEEK:([h].[HoldingID]=[AttivoGroup_copy].[dbo].[StakeholderHolding].[HoldingID] as [sh].[HoldingID]) ORDERED FORWARD)
                            |--Compute Scalar(DEFINE:([Expr1016]=replace(replace([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[ClientName] as [i].[ClientName],' ',''),',','')))
                                 |--Clustered Index Scan(OBJECT:([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[PK_AgencyCommissionImport] AS [i]), WHERE:([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[AgencyCommissionFileID] as [i].[AgencyCommissionFileID]=[@AgencyCommissionFileID] AND [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchTypeID] as [i].[MatchTypeID]=(0) AND ([AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchedHoldingID] as [i].[MatchedHoldingID] IS NULL OR [AttivoGroup_copy].[dbo].[AgencyCommissionImport].[MatchedStakeholderID] as [i].[MatchedStakeholderID] IS NULL)))

【问题讨论】:

  • 您是否检查了这两个查询的查询计划?另外,您使用的是哪种 SQL(SQLServer、MySQL、Oracle 等)?
  • 90 秒 vs 眨眼可能表明执行计划有很大的不同(连接类型)或发生了其他事情(阻塞或从磁盘读取与从缓存读取)。我非常怀疑这纯粹是因为 CPU 时间从 patindex 更改为 like 造成的。请发布执行计划和SET STATISTICS IO ON; SET STATISTICS TIME ON;的输出
  • 也许这是数据库缓存的问题?您是否尝试过在使用 DBCC 帮助程序运行每个查询之前重置缓存? (DBCC DROPCLEANBUFFERS, DBCC FREEPROCCACHE)
  • @Oleg - 前导通配符意味着索引也无济于事。
  • 感谢 cmets。回答一些问题:它是 SQLServer,我使用的数据集是我们的客户端现在正在使用的实时数据(已修复性能问题),因此我必须将其拉回我们的测试站点才能运行执行计划.完成后我会更新。

标签: sql sql-server string performance tsql


【解决方案1】:

也许这是数据库缓存的问题......

在使用 DBCC 帮助器运行每个查询之前尝试重置缓存:

【讨论】:

    【解决方案2】:

    这种可重复的性能差异很可能是由于两个查询的执行计划不同。

    让 SQL Server 在运行每个查询时返回实际的执行计划,并比较执行计划。

    此外,当您比较两个查询的性能时,将每个查询运行两次,并丢弃第一次运行的时间。 (第一次查询运行可能包括很多繁重的工作(语句解析和数据库 i/o)。第二次运行会为您提供比其他查询更有效的经过时间。

    谁能解释为什么 LIKE 比 PATINDEX 慢这么多?

    每个查询的执行计划可能会解释差异。

    仅仅是这两个函数编写效率的问题吗?

    这并不是函数编写效率的问题。真正重要的是生成的执行计划。重要的是谓词是否可搜索以及优化器是否选择使用可用索引。


    [编辑]

    在我运行的快速测试中,我发现执行计划有所不同。在连接谓词中使用 LIKE 运算符时,该计划包括在 "Computer Scalar" 操作之后对 table2 的 "Table Spool (Lazy Spool)" 操作。使用 PATINDEX 函数,我在计划中看不到 "Table Spool" 操作。但考虑到查询、表、索引和统计数据的差异,我得到的计划可能与你得到的计划有很大不同。

    [编辑]

    我在两个查询的执行计划输出中看到的唯一区别(除了表达式占位符名称)是对三个内部函数(LikeRangeStartLikeRangeEndLikeRangeInfo 的调用代替了一个调用到PATINDEX 函数。这些函数似乎是为结果集中的每一行调用的,结果表达式用于在嵌套循环中扫描内表。

    因此,看起来对LIKE 运算符的三个函数调用可能比对PATINDEX 函数的单个调用更昂贵(从时间上来说)。 (解释计划显示了为嵌套循环连接的外部结果集中的每一行调用的那些函数;对于大量行,即使经过时间的微小差异也可以乘以足够多的倍以表现出显着的性能差异。)


    在我的系统上运行了一些测试用例后,我仍然对您看到的结果感到困惑。

    可能是调用 PATINDEX 函数与调用三个内部函数(LikeRangeStart、LikeRangeEnd、LikeRangeInfo)的性能问题。

    如果在“足够大”的结果集上执行这些操作,经过时间的微小差异可能会乘以显着差异。

    但实际上,我发现使用 LIKE 运算符的查询比使用 PATINDEX 函数的等效查询的执行时间要长得多,这有点令人惊讶。

    【讨论】:

    • 嗯!我刚刚在我们现有的测试数据库上使用类似大小(但不同)的数据集重新运行了查询,并且没有那么大的差异。因此,看起来它可能取决于实际数据。正如我上面所说,我会尽快拉回实时数据,以准确复制我之前所做的事情,看看执行计划是什么样的。
    • 顺便说一句,我确实多次运行查询,每次都得到相同的结果(当我有原始数据可以使用时!)。
    • 我不同意这种分析。您能否提供一个重现,其中这些内部功能的存在与其他类似的计划会产生任何重大差异?
    【解决方案3】:

    我完全不相信 LikeRangeStartLikeRangeEndLikeRangeInfo 函数的额外开销是造成时间差异的原因。

    它根本无法重现(至少在我的测试中,默认排序规则等)。当我尝试以下操作时

    SET STATISTICS IO OFF;
    SET STATISTICS TIME OFF;
    
    DECLARE @T TABLE (name sysname )
    INSERT INTO @T
    SELECT TOP 2500 name + '...' + 
       CAST(ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS VARCHAR)
    FROM sys.all_columns
    
    SET STATISTICS IO ON;
    SET STATISTICS TIME ON;
    PRINT '***'
    SELECT     COUNT(*)
    FROM       @T AS a
    INNER JOIN @T AS b ON (a.name LIKE '%' + b.name + '%')
    
    PRINT '***'
    SELECT     COUNT(*)
    FROM       @T AS a
    INNER JOIN @T AS b ON (PATINDEX('%' + b.name + '%', a.name) > 0)
    

    它为两者提供了基本相同的计划,但也包含这些不同的内部功能,我得到以下内容。

    喜欢

    Table '#5DB5E0CB'. Scan count 2, logical reads 40016
    CPU time = 26953 ms,  elapsed time = 28083 ms.
    

    PATINDEX

    Table '#5DB5E0CB'. Scan count 2, logical reads 40016
    CPU time = 28329 ms,  elapsed time = 29458 ms.
    

    但我确实注意到,如果我用 #temp 表代替表变量,则进入流聚合的估计行数会显着不同。

    LIKE 版本估计有 330,596 个,PATINDEX 估计有 1,875,000 个。

    我注意到您的计划中还有一个哈希联接。可能是因为PATINDEX 版本估计的行数似乎比LIKE 更多,因此该查询获得了更大的内存授权,因此不必将散列操作溢出到磁盘。尝试在 Profiler 中跟踪哈希警告,看看是否是这种情况。

    【讨论】:

      猜你喜欢
      • 2011-05-03
      • 2014-02-23
      • 1970-01-01
      • 2014-01-24
      • 2010-11-12
      • 2014-11-26
      • 2014-07-15
      • 2023-04-03
      相关资源
      最近更新 更多