У меня есть база данных 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 здесь . Источники из запроса выше:
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Stored%20Procedures/CipherDetails_ReadByUserId.sql
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Functions/UserCipherDetails.sql
- https://github.com/bitwarden/core/blob/master/src/Sql/dbo/Functions/CipherDetails.sql
Я потратил некоторое время на этот запрос в течение нескольких месяцев, настраивая план выполнения, насколько я знаю, в конечном итоге в его текущем состоянии. Запросы с таким планом выполнения выполняются быстро через миллионы строк (<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.
Я чувствую, что из этого запроса я могу получить более высокую производительность ЦП, но я нахожусь на этапе, когда я не уверен, как продолжить настройку плана выполнения. Какие еще оптимизации можно было бы уменьшить загрузку процессора? Какие операции в плане выполнения являются худшими нарушителями использования ЦП?
UNION ALL
(один дляC.[UserId] = @UserId
и один дляC.[UserId] IS NULL AND ...
). Это уменьшило наборы результатов объединения и полностью исключило необходимость в хэш-сопоставлениях (теперь выполняются вложенные циклы для небольших наборов объединений). Запрос теперь намного лучше на CPU. Спасибо!Ответ сообщества вики :
Вы можете попытаться разделить это на два запроса и
UNION ALL
соединить их вместе.Ваше
WHERE
предложение происходит все в конце, но если вы разделите его на:C.[UserId] = @UserId
C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1
... у каждого может быть достаточно хороший план, чтобы он того стоил.
Если каждый запрос применяет предикат в начале плана, вам не нужно объединять столько строк, которые в конечном итоге отфильтрованы.
источник