Что вызывает высокую загрузку ЦП из этого плана запроса / выполнения?

9

У меня есть база данных SQL Azure, которая поддерживает приложение API .NET Core. Просмотр отчетов об обзоре производительности на портале Azure показывает, что большая часть нагрузки (использование DTU) на моем сервере базы данных поступает из ЦП, и один запрос специально:

введите описание изображения здесь

Как мы видим, запрос 3780 отвечает почти за все использование процессора на сервере.

Это в некоторой степени имеет смысл, поскольку запрос 3780 (см. Ниже) представляет собой, в основном, всю суть приложения и часто вызывается пользователями. Это также довольно сложный запрос со многими объединениями, необходимыми для получения необходимого набора данных. Запрос поступает от sproc, который в итоге выглядит следующим образом:

-- @UserId UNIQUEIDENTIFIER

SELECT
    C.[Id],
    C.[UserId],
    C.[OrganizationId],
    C.[Type],
    C.[Data],
    C.[Attachments],
    C.[CreationDate],
    C.[RevisionDate],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Favorites] IS NULL
            OR JSON_VALUE(C.[Favorites], CONCAT('$."', @UserId, '"')) IS NULL
        THEN 0
        ELSE 1
    END [Favorite],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Folders] IS NULL
        THEN NULL
        ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
    END [FolderId],
    CASE 
        WHEN C.[UserId] IS NOT NULL OR OU.[AccessAll] = 1 OR CU.[ReadOnly] = 0 OR G.[AccessAll] = 1 OR CG.[ReadOnly] = 0 THEN 1
        ELSE 0
    END [Edit],
    CASE 
        WHEN C.[UserId] IS NULL AND O.[UseTotp] = 1 THEN 1
        ELSE 0
    END [OrganizationUseTotp]
FROM
    [dbo].[Cipher] C
LEFT JOIN
    [dbo].[Organization] O ON C.[UserId] IS NULL AND O.[Id] = C.[OrganizationId]
LEFT JOIN
    [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
LEFT JOIN
    [dbo].[CollectionCipher] CC ON C.[UserId] IS NULL AND OU.[AccessAll] = 0 AND CC.[CipherId] = C.[Id]
LEFT JOIN
    [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[GroupUser] GU ON C.[UserId] IS NULL AND CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
    [dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
WHERE
    C.[UserId] = @UserId
    OR (
        C.[UserId] IS NULL
        AND OU.[Status] = 2
        AND O.[Enabled] = 1
        AND (
            OU.[AccessAll] = 1
            OR CU.[CollectionId] IS NOT NULL
            OR G.[AccessAll] = 1
            OR CG.[CollectionId] IS NOT NULL
        )
)

Если вам интересно, полный источник этой базы данных можно найти на GitHub здесь . Источники из запроса выше:

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

Я приложил фактический план запроса ниже (не уверен в каком-либо другом способе поделиться этим здесь при обмене стеками), который показывает выполнение sproc в рабочем состоянии с возвращенным набором данных с ~ 400 результатами.

Некоторые моменты, которые я ищу для уточнения:

  • Поиск индекса [IX_Cipher_UserId_Type_IncludeAll]занимает 57% от общей стоимости плана. Насколько я понимаю, план состоит в том, что эта стоимость связана с IO, что означает, что таблица Cipher содержит миллионы записей. Однако отчеты о производительности Azure SQL показывают, что мои проблемы связаны с процессором в этом запросе, а не с вводом-выводом, поэтому я не уверен, является ли это проблемой на самом деле или нет. Кроме того, здесь уже выполняется поиск по индексу, поэтому я не уверен, что есть место для улучшения.

  • Похоже, что операции Hash Match из всех объединений показывают значительное использование ЦП в плане (я думаю?), Но я не совсем уверен, как это можно улучшить. Сложный характер получения данных требует большого количества объединений нескольких таблиц. Я уже замкнул многие из этих объединений, если это возможно (на основе результатов предыдущего объединения) в их ONпредложениях.

Загрузите полный план выполнения здесь: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0.

Я чувствую, что из этого запроса я могу получить более высокую производительность ЦП, но я нахожусь на этапе, когда я не уверен, как продолжить настройку плана выполнения. Какие еще оптимизации можно было бы уменьшить загрузку процессора? Какие операции в плане выполнения являются худшими нарушителями использования ЦП?

kspearrin
источник

Ответы:

4

Вы можете просмотреть показатели ЦП и затраченного времени на уровне оператора в SQL Server Management Studio, хотя я не могу сказать, насколько они надежны для запросов, которые выполняются так же быстро, как и ваши. В вашем плане есть только операторы режима строки, поэтому метрики времени применяются к этому оператору, а также к операторам в поддереве под ним. Используя в качестве примера соединение с вложенным циклом, SQL Server сообщает вам, что на все поддерево ушло 60 мс процессорного времени и 80 мс прошедшего времени:

расходы на поддерево

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

Это огромное упрощение, но ЦП, занятый этими хэш-соединениями, будет зависеть от размера входных данных для хеш-таблицы и количества строк, обрабатываемых на стороне зонда. Рассмотрим несколько вещей об этом плане запроса:

  • Максимум 461 возвращенный ряд имеет C.[UserId] = @UserId. Эти строки не заботятся о соединениях вообще.
  • Для строк, которые нуждаются в объединениях, SQL Server не может применить какую-либо фильтрацию раньше (кроме OU.[UserId] = @UserId).
  • Почти все обработанные строки удаляются в конце плана запроса (чтение справа налево) фильтром: [vault].[dbo].[Cipher].[UserId] as [C].[UserId]=[@UserId] OR ([vault].[dbo].[OrganizationUser].[AccessAll] as [OU].[AccessAll]=(1) OR [vault].[dbo].[CollectionUser].[CollectionId] as [CU].[CollectionId] IS NOT NULL OR [vault].[dbo].[Group].[AccessAll] as [G].[AccessAll]=(1) OR [vault].[dbo].[CollectionGroup].[CollectionId] as [CG].[CollectionId] IS NOT NULL) AND [vault].[dbo].[Cipher].[UserId] as [C].[UserId] IS NULL AND [vault].[dbo].[OrganizationUser].[Status] as [OU].[Status]=(2) AND [vault].[dbo].[Organization].[Enabled] as [O].[Enabled]=(1)

Было бы более естественно написать ваш запрос в виде UNION ALL. Первая половина UNION ALLможет включать строки where, C.[UserId] = @UserIdа вторая половина может включать строки where C.[UserId] IS NULL. Вы уже выполняете два поиска индекса [dbo].[Cipher](один для @UserIdи один для NULL), поэтому кажется маловероятным, что UNION ALLверсия будет медленнее. Написание запросов по отдельности позволит вам выполнить некоторую фильтрацию заранее, как на стороне сборки, так и на стороне зонда. Запросы могут быть быстрее, если им нужно обрабатывать меньше промежуточных данных.

Я не знаю, поддерживает ли ваша версия SQL Server это, но если это не поможет, попробуйте добавить индекс columnstore к вашему запросу, чтобы сделать ваши хеш-объединения подходящими для пакетного режима . Мой предпочтительный способ состоит в том, чтобы создать пустую таблицу с CCI на ней и оставить соединение с этой таблицей слева. Хеш-объединения могут быть гораздо более эффективными, когда они запускаются в пакетном режиме по сравнению с режимом строки.

Джо Оббиш
источник
Как и предполагалось, я смог переписать sproc с двумя запросами UNION ALL(один для C.[UserId] = @UserIdи один для C.[UserId] IS NULL AND ...). Это уменьшило наборы результатов объединения и полностью исключило необходимость в хэш-сопоставлениях (теперь выполняются вложенные циклы для небольших наборов объединений). Запрос теперь намного лучше на CPU. Спасибо!
kspearrin
0

Ответ сообщества вики :

Вы можете попытаться разделить это на два запроса и UNION ALLсоединить их вместе.

Ваше WHEREпредложение происходит все в конце, но если вы разделите его на:

  • Один запрос где C.[UserId] = @UserId
  • Еще где C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

... у каждого может быть достаточно хороший план, чтобы он того стоил.

Если каждый запрос применяет предикат в начале плана, вам не нужно объединять столько строк, которые в конечном итоге отфильтрованы.

оборота user126897
источник