Скорость сравнения строк SQL 'like' против 'patindex' - PullRequest
15 голосов
/ 08 ноября 2011

У меня был следующий запрос (упрощенно) ...

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)))

Ответы [ 3 ]

4 голосов
/ 08 ноября 2011

Такая повторяющаяся разница в производительности наиболее вероятна из-за разницы в планах выполнения для двух запросов.

Пусть SQL Server возвращает фактический план выполнения при выполнении каждого запроса и сравнивает планы выполнения.

Кроме того, выполняйте каждый запрос дважды и выбрасывайте время для первого запуска, когда вы сравниваете производительность двух запросов. (Первый запуск запроса может потребовать значительных усилий (синтаксический анализ операторов и ввод-вывод базы данных). Второй запуск даст вам истекшее время, которое более корректно по сравнению с другим запросом.

Может кто-нибудь объяснить, почему LIKE намного медленнее, чем PATINDEX?

План выполнения каждого запроса, вероятно, объяснит разницу.

Это просто вопрос того, насколько эффективно написаны две функции?

Дело не в том, насколько эффективно написаны функции. Что действительно важно, так это сгенерированный план выполнения. Важно то, что предикаты можно использовать и оптимизатор выбирает использование доступных индексов.


* * Тысяча двадцать-одина [EDIT]

В быстром тесте, который я провел, я вижу разницу в планах выполнения. С оператором LIKE в предикате соединения план включает в себя операцию "Table Spool (Lazy Spool)" над таблицей 2 после операции "Computer Scalar". С функцией PATINDEX я не вижу операции "Table Spool" в плане. Но планы, которые я получаю, могут значительно отличаться от планов, которые вы получаете, учитывая различия в запросах, таблицах, индексах и статистике.

[EDIT]

Единственное отличие, которое я вижу в выходных данных плана выполнения для двух запросов (кроме имен заполнителей выражений), заключается в вызовах трех внутренних функций (LikeRangeStart, LikeRangeEnd и LikeRangeInfo вместо одного вызова). функции PATINDEX. Эти функции, по-видимому, вызываются для каждой строки в наборе результатов, а полученное выражение используется для сканирования внутренней таблицы во вложенном цикле.

Таким образом, похоже, что три вызова функций для оператора LIKE могут быть более дорогостоящими (по истечении времени), чем один вызов функции PATINDEX. (В плане объяснения показаны те функции, которые вызываются для каждой строки во внешнем наборе результатов соединения с вложенным циклом; для большого числа строк даже небольшая разница в затраченном времени может быть умножена достаточно много раз, чтобы продемонстрировать существенную разницу в производительности.)


После запуска некоторых тестовых примеров в моей системе, я все еще озадачен результатами, которые вы видите.

Возможно, это проблема производительности вызовов функции PATINDEX по сравнению с вызовами трех внутренних функций (LikeRangeStart, LikeRangeEnd, LikeRangeInfo.)

Возможно, что с теми, которые выполняются на достаточно большом наборе результатов, небольшая разница во времени может быть умножена на значительную разницу.

Но на самом деле я нахожу несколько удивительным, что выполнение запроса с использованием оператора LIKE займет значительно больше времени, чем эквивалентный запрос с использованием функции PATINDEX.

2 голосов
/ 09 ноября 2011

Я совсем не убежден тезисом, что это дополнительные издержки функций LikeRangeStart, LikeRangeEnd, LikeRangeInfo, которые несут ответственность за несоответствие времени.

Это простоне воспроизводится (по крайней мере, в моем тесте, сортировка по умолчанию и т. д.).Когда я пробую следующее

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)

, которое дает по существу один и тот же план для обоих, но также содержит эти различные внутренние функции, я получаю следующее:

LIKE

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 вместо табличной переменной, предполагаемое число строк, входящих в агрегат потока, будет существенно отличаться.

The *Версия 1020 * оценивается в 330 596, а PATINDEX - в 1 875 000.

Я заметил, что у вас также есть хеш-соединение в вашем плане.Возможно, из-за того, что версия PATINDEX оценивает большее количество строк, чем LIKE, этот запрос получает больший объем памяти, поэтому ему не нужно разливать операцию хэширования на диск.Попробуйте отследить хеш-предупреждения в Profiler, чтобы убедиться, что это так.

1 голос
/ 08 ноября 2011

Возможно, это вопрос кеширования БД ...

Попробуйте сбросить кэш перед выполнением каждого запроса с помощью помощников DBCC:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...