Запросить паузы после возврата фиксированного количества строк

8

У меня есть представление, которое выполняется быстро (несколько секунд) до 41 записи (например, TOP 41), но занимает несколько минут для 44 или более записей, с промежуточными результатами при запуске с помощью TOP 42или TOP 43. В частности, он вернет первые 39 записей за несколько секунд, а затем остановится почти на три минуты, а затем вернет оставшиеся записи. Этот шаблон одинаков при запросе TOP 44или TOP 100.

Это представление изначально получено из базового представления, добавляя к базе только один фильтр, последний в приведенном ниже коде. Кажется, нет никакой разницы, цепляю ли я дочернее представление от базы или пишу дочернее представление с кодом из встроенной базы. Базовый вид возвращает 100 записей всего за несколько секунд. Я хотел бы думать, что я могу заставить детский взгляд бегать так же быстро, как и основа, а не в 50 раз медленнее. Кто-нибудь видел такое поведение? Какие-нибудь догадки относительно причины или решения?

Это поведение было последовательным в течение последних нескольких часов, так как я протестировал соответствующие запросы, хотя количество строк, возвращаемых до начала замедления, немного увеличилось и уменьшилось. Это не ново; Я смотрю на это сейчас, потому что общее время выполнения было приемлемым (<2 минуты), но я видел эту паузу в связанных файлах журнала, по крайней мере, в течение нескольких месяцев.

блокировка

Я никогда не видел, чтобы запрос был заблокирован, и проблема существует, даже если в базе данных нет других действий (как проверено sp_WhoIsActive). Базовый вид включает в себя NOLOCKвсе, для чего это стоит.

Запросы

Вот урезанная версия дочернего вида с базовым видом для простоты. Это все еще показывает скачок во время выполнения в приблизительно 40 записях.

SELECT TOP 100 PERCENT
    Map.SalesforceAccountID AS Id,
    CAST(C.CustomerID AS NVARCHAR(255)) AS Name,
    CASE WHEN C.StreetAddress = 'Unknown' THEN '' ELSE C.StreetAddress                 END AS BillingStreet,
    CASE WHEN C.City          = 'Unknown' THEN '' ELSE SUBSTRING(C.City,        1, 40) END AS BillingCity,
                                                       SUBSTRING(C.Region,      1, 20)     AS BillingState,
    CASE WHEN C.PostalCode    = 'Unknown' THEN '' ELSE SUBSTRING(C.PostalCode,  1, 20) END AS BillingPostalCode,
    CASE WHEN C.Country       = 'Unknown' THEN '' ELSE SUBSTRING(C.Country,     1, 40) END AS BillingCountry,
    CASE WHEN C.PhoneNumber   = 'Unknown' THEN '' ELSE C.PhoneNumber                   END AS Phone,
    CASE WHEN C.FaxNumber     = 'Unknown' THEN '' ELSE C.FaxNumber                     END AS Fax,
    TransC.WebsiteAddress AS Website,
    C.AccessKey AS AccessKey__c,
    CASE WHEN dbo.ValidateEMail(C.EMailAddress) = 1 THEN C.EMailAddress END,  -- Removing this UDF does not speed things
    TransC.EmailSubscriber
    -- A couple dozen additional TransC fields
FROM
    WarehouseCustomers AS C WITH (NOLOCK)
    INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) ON C.CustomerID = TransC.CustomerID
    LEFT JOIN  Salesforce.AccountsMap AS Map WITH (NOLOCK) ON C.CustomerID = Map.CustomerID
WHERE
        C.DateMadeObsolete IS NULL
    AND C.EmailAddress NOT LIKE '%@volusion.%'
    AND C.AccessKey IN ('C', 'R')
    AND C.CustomerID NOT IN (243566)  -- Exclude specific test records
    AND EXISTS (SELECT * FROM Orders AS O WHERE C.CustomerID = O.CustomerID AND O.OrderDate >= '2010-06-28')  -- Only count customers who've placed a recent order
    AND Map.SalesforceAccountID IS NULL  -- Only count customers not already uploaded to Salesforce
-- Removing the ORDER BY clause does not speed things up
ORDER BY
    C.CustomerID DESC

Этот Id IS NULLфильтр отбрасывает большинство записей, возвращаемых BaseView; без TOPпредложения они возвращают 1100 записей и 267K соответственно.

Статистика

Когда работает TOP 40:

SQL Server parse and compile time:    CPU time = 234 ms, elapsed time = 247 ms.
SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.

(40 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 39112, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 752, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 458, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 2199 ms,  elapsed time = 7644 ms.

Когда работает TOP 45:

(45 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 98268, physical reads 1, read-ahead reads 3, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 1788, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 2152, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times: CPU time = 41980 ms,  elapsed time = 177231 ms.

Я удивлен, увидев, что число чтений скачет в ~ 3 раза для этой скромной разницы в реальном выходе.

Сравнивая планы выполнения, они одинаковы, кроме количества возвращенных строк. Как и в приведенной выше статистике, фактическое количество строк для ранних шагов в TOP 45запросе значительно выше , а не только на 12,5%.

В общих чертах, он сканирует индекс покрытия из Orders и ищет соответствующие записи из WarehouseCustomers; соединение цикла с TransactionalCustomers (удаленный запрос, точный план неизвестен); и объединить это с таблицей сканирования AccountsMap. Удаленный запрос составляет 94% от оценочной стоимости.

Разные заметки

Ранее, когда я выполнял расширенное содержимое представления как отдельный запрос, он выполнялся довольно быстро: 13 секунд для 100 записей. Сейчас я тестирую урезанную версию запроса без подзапросов, и этот гораздо более простой запрос требует трех минут, чтобы запросить более 40 строк, даже если он выполняется как самостоятельный запрос.

Дочернее представление включает в себя значительное количество операций чтения (~ 1M на sp_WhoIsActive), но на этом компьютере (восемь ядер, 32 ГБ ОЗУ, выделенный блок SQL 95%) это обычно не проблема.

Я удалил и воссоздал оба представления несколько раз без изменений.

Данные не включают в себя поля TEXT или BLOB. Одно поле включает UDF; удаление не предотвращает паузу.

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

Примечания Re: решение

Исправление оказалось простым: замена LEFT JOINкарты на NOT EXISTSпредложение. Это вызывает только одно крошечное различие в плане запроса: присоединение к таблице TransactionCustomers (удаленный запрос) после присоединения к таблице Map, а не до. Это может означать, что он запрашивает только необходимые записи с удаленного сервера, что сократит передаваемый объем в ~ 100 раз.

Обычно я первый, чтобы поболеть за NOT EXISTS; это часто быстрее чем LEFT JOIN...WHERE ID IS NULLконструкция, и немного более компактно. В этом случае это неудобно, потому что проблемный запрос построен на существующем представлении, и хотя поле, необходимое для предотвращения объединения, предоставляется базовым представлением, оно сначала преобразуется из целого числа в текст. Поэтому для достойной производительности я должен отбросить двухслойный шаблон и вместо этого иметь два почти идентичных представления, причем второе включает NOT EXISTSпредложение.

Спасибо всем за помощь в устранении этой проблемы! Это может быть слишком специфично для моих обстоятельств, чтобы помочь кому-то еще, но, надеюсь, нет. Если ничто иное, это пример того, NOT EXISTSчтобы быть более чем немного быстрее, чем LEFT JOIN...WHERE ID IS NULL. Но реальный урок, вероятно, состоит в том, чтобы гарантировать, что удаленные запросы объединяются настолько эффективно, насколько это возможно; План запроса утверждает, что он составляет 2% от стоимости, но он не всегда оценивает точно.

Джон на все руки
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт 9

Ответы:

4

Несколько вещей, чтобы попробовать:

  1. Проверьте свои индексы

    • Все JOINключевые поля проиндексированы? Если вы часто используете это представление, я бы зашел так далеко, что добавил бы отфильтрованный индекс для критериев в представлении. Например...

    • CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)

  2. Обновить статистику

    • Могут быть проблемы с устаревшей статистикой. Если вы можете качать его, я бы сделал FULLSCAN. Если имеется большое количество строк, возможно, что данные значительно изменились, не вызывая автоматический пересчет.
  3. Очистить запрос

    • Сделайте Map JOINa NOT EXISTS- вам не нужны никакие данные из этой таблицы, так как вам нужны только несовпадающие записи

    • Удалить ORDER BY. Я знаю, что комментарии говорят, что это не имеет значения, но мне очень трудно в это поверить. Это может не иметь значения для ваших меньших наборов результатов, так как страницы данных уже кэшированы.

JNK
источник
Интересный момент re: отфильтрованный индекс. Запрос не использует его автоматически, но я протестирую его с подсказкой. Я обновил статистику и могу проверить эту и другие ваши рекомендации позже сегодня; После EOWD мне нужно разрешить накопление невыполненных работ, чтобы я мог проверить приличный набор данных.
Джон на все руки
Я пробовал разные комбинации этих твиков, и ключом, кажется, является анти-соединение с картой. Как LEFT JOIN...WHERE Id IS NULL, я получаю эту паузу; как NOT EXISTSпункт, время выполнения - секунды. Я удивлен, но не могу спорить с результатами!
Джон на все руки
2

Улучшение 1 Удалите вложенный запрос для заказов и преобразуйте его в объединение

FROM
WarehouseCustomers AS C WITH (NOLOCK)
INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) 
                                                        ON C.CustomerID = TransC.CustomerID
LEFT JOIN  Salesforce.AccountsMap AS Map WITH (NOLOCK) 
                                                        ON C.CustomerID = Map.CustomerID
INNER Join Orders AS O 
                                                        ON C.CustomerID = O.CustomerID

 WHERE
    C.DateMadeObsolete IS NULL
    AND C.EmailAddress NOT LIKE '%@volusion.%'
    AND C.AccessKey IN ('C', 'R')
    AND C.CustomerID NOT IN (243566)
    AND O.OrderDate >= '2010-06-28'
    AND Map.SalesforceAccountID IS NULL

Улучшение 2 - Храните отфильтрованные записи TransactionalCustomers в локальной временной таблице

Select 
    CAST(C.CustomerID AS NVARCHAR(255)) AS Name,
    CASE WHEN C.StreetAddress = 'Unknown' THEN '' ELSE C.StreetAddress                 END AS BillingStreet,
    CASE WHEN C.City          = 'Unknown' THEN '' ELSE SUBSTRING(C.City,        1, 40) END AS BillingCity,
                                                       SUBSTRING(C.Region,      1, 20)     AS BillingState,
    CASE WHEN C.PostalCode    = 'Unknown' THEN '' ELSE SUBSTRING(C.PostalCode,  1, 20) END AS BillingPostalCode,
    CASE WHEN C.Country       = 'Unknown' THEN '' ELSE SUBSTRING(C.Country,     1, 40) END AS BillingCountry,
    CASE WHEN C.PhoneNumber   = 'Unknown' THEN '' ELSE C.PhoneNumber                   END AS Phone,
    CASE WHEN C.FaxNumber     = 'Unknown' THEN '' ELSE C.FaxNumber                     END AS Fax,
    C.AccessKey AS AccessKey__c
Into #Temp
From  WarehouseCustomers C
Where C.DateMadeObsolete IS NULL
        AND C.EmailAddress NOT LIKE '%@volusion.%'
        AND C.AccessKey IN ('C', 'R')
        AND C.CustomerID NOT IN (243566)

Последний запрос

FROM
#Temp AS C WITH (NOLOCK)
INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) 
                                                            ON C.CustomerID = TransC.CustomerID
LEFT JOIN Salesforce.AccountsMap AS Map WITH (NOLOCK) 
                                                            ON C.CustomerID = Map.CustomerID
INNER Join Orders AS O 
                                                            ON C.CustomerID = O.CustomerID

WHERE
C.DateMadeObsolete IS NULL
AND C.EmailAddress NOT LIKE '%@volusion.%'
AND C.AccessKey IN ('C', 'R')
AND C.CustomerID NOT IN (243566)
AND O.OrderDate >= '2010-06-28'
AND Map.SalesforceAccountID IS NULL

Пункт 3 - Я предполагаю, что у вас есть индексы на CustomerID, EmailAddress, OrderDate

Панкадж Гарг
источник
1
Re: «Улучшение» 1 - EXISTSобычно быстрее, чем JOINв этом случае, и устраняет потенциальные ошибки. Я не думаю, что это будет улучшение вообще.
JNK
1
Однако проблема двоякая: это потенциально ИЗМЕНИТ РЕЗУЛЬТАТЫ, и если обе таблицы не имеют уникального кластеризованного индекса для полей, используемых в объединении, это будет менее эффективно, чем EXISTS. Подпункты не всегда плохие.
JNK
@PankajGarg: Спасибо за предложения, к сожалению, обычно на одного клиента приходится несколько заказов, поэтому EXISTSобязательно. Кроме того, с точки зрения, я не могу кэшировать повторно использованные данные о клиентах, хотя я играл с фиктивной TVF без параметров.
Джон на все руки