Tengo una base de datos SQL de Azure que funciona con una aplicación API .NET Core. La exploración de los informes de descripción general del rendimiento en Azure Portal sugiere que la mayor parte de la carga (uso de DTU) en mi servidor de base de datos proviene de la CPU, y una consulta específicamente:
Como podemos ver, la consulta 3780 es responsable de casi todo el uso de la CPU en el servidor.
Esto tiene sentido, ya que la consulta 3780 (ver más abajo) es básicamente el quid de la aplicación y los usuarios la llaman con bastante frecuencia. También es una consulta bastante compleja con muchas combinaciones necesarias para obtener el conjunto de datos adecuado que se necesita. La consulta proviene de un sproc que termina luciendo así:
-- @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
)
)
Si le importa, puede encontrar la fuente completa de esta base de datos en GitHub aquí . Fuentes de la consulta anterior:
- 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
Pasé algún tiempo en esta consulta a lo largo de los meses ajustando el plan de ejecución lo mejor que sé, terminando con su estado actual. Las consultas con este plan de ejecución son rápidas en millones de filas (<1 segundo), pero como se señaló anteriormente, están consumiendo la CPU del servidor cada vez más a medida que la aplicación crece en tamaño.
He adjuntado el plan de consulta real a continuación (no estoy seguro de otra forma de compartir eso aquí en el intercambio de pila), que muestra una ejecución de la sproc en producción contra un conjunto de datos devuelto de ~ 400 resultados.
Algunos puntos sobre los que estoy buscando aclaraciones:
Index Seek on
[IX_Cipher_UserId_Type_IncludeAll]
toma el 57% del costo total del plan. Comprendo que el plan es que este costo está relacionado con IO, lo que hace que la tabla Cipher contenga millones de registros. Sin embargo, los informes de rendimiento de Azure SQL me muestran que mis problemas provienen de la CPU en esta consulta, no de E / S, por lo que no estoy seguro de si esto es realmente un problema o no. Además, ya está haciendo una búsqueda de índice aquí, por lo que no estoy realmente seguro de que haya margen de mejora.Las operaciones de Hash Match de todas las uniones parecen ser lo que muestra un uso significativo de la CPU en el plan (¿creo?), Pero no estoy realmente seguro de cómo podría mejorarse esto. La naturaleza compleja de cómo necesito obtener los datos requiere muchas uniones en varias tablas. Ya cortocircuito muchas de estas uniones si es posible (según los resultados de una unión anterior) en sus
ON
cláusulas.
Descargue el plan de ejecución completo aquí: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0
Siento que puedo obtener un mejor rendimiento de la CPU con esta consulta, pero estoy en una etapa en la que no estoy seguro de cómo proceder para ajustar más el plan de ejecución. ¿Qué otras optimizaciones se podrían tener para disminuir la carga de la CPU? ¿Qué operaciones en el plan de ejecución son los peores infractores del uso de la CPU?
UNION ALL
(una paraC.[UserId] = @UserId
y una paraC.[UserId] IS NULL AND ...
). Esto redujo los conjuntos de resultados de unión y eliminó la necesidad de coincidencias hash por completo (ahora haciendo bucles anidados en pequeños conjuntos de combinaciones). La consulta ahora es mucho mejor en la CPU. ¡Gracias!Respuesta wiki comunitaria :
Puede intentar dividir esto en dos consultas y
UNION ALL
volver a unirlas.Su
WHERE
cláusula está sucediendo todo al final, pero si la divide en:C.[UserId] = @UserId
C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1
... cada uno podría obtener un plan lo suficientemente bueno como para que valga la pena.
Si cada consulta aplica el predicado al principio del plan, no tendría que unir tantas filas que finalmente se filtran.
fuente