Eu tenho um banco de dados SQL do Azure que habilita um aplicativo da API do .NET Core. A navegação nos relatórios de visão geral de desempenho no Portal do Azure sugere que a maior parte da carga (uso de DTU) no meu servidor de banco de dados é proveniente da CPU e uma consulta especificamente:
Como podemos ver, a consulta 3780 é responsável por quase todo o uso da CPU no servidor.
Isso faz sentido, uma vez que a consulta 3780 (veja abaixo) é basicamente o ponto crucial do aplicativo e é chamada pelos usuários com bastante frequência. Também é uma consulta bastante complexa, com muitas associações necessárias para obter o conjunto de dados adequado. A consulta vem de um sproc que acaba assim:
-- @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
)
)
Se você se importa, a fonte completa desse banco de dados pode ser encontrada no GitHub aqui . Fontes da consulta acima:
- 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
Passei algum tempo nessa consulta ao longo dos meses, ajustando o plano de execução da melhor maneira possível, terminando com seu estado atual. As consultas com este plano de execução são rápidas em milhões de linhas (<1 s), mas, como observado acima, estão consumindo a CPU do servidor cada vez mais à medida que o aplicativo aumenta de tamanho.
Anexei o plano de consulta real abaixo (não tenho certeza de nenhuma outra maneira de compartilhar isso aqui na troca de pilhas), que mostra uma execução do sproc em produção contra um conjunto de dados retornado de ~ 400 resultados.
Estou procurando esclarecimentos sobre alguns pontos:
A pesquisa de índice
[IX_Cipher_UserId_Type_IncludeAll]
leva 57% do custo total do plano. Meu entendimento do plano é que esse custo está relacionado ao IO, o que faz com que a tabela Cipher contenha milhões de registros. No entanto, os relatórios de desempenho SQL do Azure estão me mostrando que meus problemas decorrem da CPU nesta consulta, não de E / S, portanto, não tenho certeza se isso é realmente um problema ou não. Além disso, ele já está fazendo uma busca de índice aqui, então não tenho certeza se há espaço para melhorias.As operações de Hash Match de todas as junções parecem ser o que está mostrando um uso significativo da CPU no plano (eu acho?), Mas não tenho muita certeza de como isso poderia ser melhorado. A natureza complexa de como eu preciso obter os dados exige muitas junções em várias tabelas. Eu já curto-circuito muitas dessas junções, se possível (com base nos resultados de uma junção anterior) em suas
ON
cláusulas.
Faça o download do plano de execução completo aqui: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0
Sinto que posso obter melhor desempenho da CPU com essa consulta, mas estou em um estágio em que não tenho certeza de como prosseguir com o ajuste do plano de execução. Que outras otimizações poderiam ser necessárias para diminuir a carga da CPU? Quais operações no plano de execução são os piores infratores do uso da CPU?
UNION ALL
(uma paraC.[UserId] = @UserId
e uma paraC.[UserId] IS NULL AND ...
). Isso reduziu os conjuntos de resultados da junção e removeu completamente a necessidade de correspondências de hash (agora fazendo loops aninhados em conjuntos de junções pequenas). A consulta agora é muito melhor na CPU. Obrigado!Resposta do wiki da comunidade :
Você pode tentar dividir isso em duas consultas e
UNION ALL
juntá-las novamente.Sua
WHERE
cláusula está acontecendo no final, mas se você a dividir em:C.[UserId] = @UserId
C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1
... cada um pode ter um plano bom o suficiente para fazer valer a pena.
Se cada consulta aplicar o predicado no início do plano, você não precisará associar tantas linhas que serão filtradas.
fonte