¿Qué está causando un alto uso de la CPU de este plan de consulta / ejecución?

9

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:

ingrese la descripción de la imagen aquí

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:

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 ONclá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?

kspearrin
fuente

Respuestas:

4

Puede ver la CPU de nivel de operador y las métricas de tiempo transcurrido en SQL Server Management Studio, aunque no puedo decir qué tan confiables son para las consultas que finalizan tan rápido como la suya. Su plan solo tiene operadores de modo de fila, por lo que las métricas de tiempo se aplican a ese operador, así como a los operadores en el subárbol debajo de él. Utilizando la unión de bucle anidado como ejemplo, SQL Server le dice que todo el subárbol tomó 60 ms de tiempo de CPU y 80 ms de tiempo transcurrido:

costos de subárbol

La mayor parte de ese tiempo de subárbol se gasta en la búsqueda de índice. Index busca tomar CPU también. Parece que su índice tiene exactamente las columnas necesarias, por lo que no está claro cómo podría reducir los costos de CPU de ese operador. Además de las búsquedas, la mayor parte del tiempo de CPU en el plan se gasta en las coincidencias hash que implementan sus uniones.

Esta es una gran simplificación excesiva, pero la CPU tomada por esas uniones hash dependerá del tamaño de la entrada para la tabla hash y del número de filas procesadas en el lado de la sonda. Observando algunas cosas sobre este plan de consulta:

  • A lo sumo 461 filas devueltas tienen C.[UserId] = @UserId. Estas filas no se preocupan por las uniones en absoluto.
  • Para las filas que necesitan las combinaciones, SQL Server no puede aplicar ninguno de los filtros antes (excepto OU.[UserId] = @UserId).
  • El filtro elimina casi todas las filas procesadas cerca del final del plan de consulta (lectura de derecha a izquierda): [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)

Sería más natural escribir su consulta como a UNION ALL. La primera mitad de la UNION ALLlata puede incluir filas donde C.[UserId] = @UserIdy la segunda mitad puede incluir filas donde C.[UserId] IS NULL. Ya está haciendo dos búsquedas de índice [dbo].[Cipher](una para @UserIdy otra para NULL), por lo que parece poco probable que la UNION ALLversión sea más lenta. Escribir las consultas por separado le permitirá realizar parte del filtrado temprano, tanto en el lado de la compilación como en el de la sonda. Las consultas pueden ser más rápidas si necesitan procesar menos datos intermedios.

No sé si su versión de SQL Server admite esto, pero si eso no ayuda, podría intentar agregar un índice de almacén de columnas a su consulta para que sus uniones hash sean elegibles para el modo por lotes . Mi forma preferida es crear una tabla vacía con un CCI y dejar unir a esa tabla. Las uniones de hash pueden ser mucho más eficientes cuando se ejecutan en modo por lotes en comparación con el modo de fila.

Joe Obbish
fuente
Como se sugirió, pude volver a escribir el sproc con 2 consultas que UNION ALL(una para C.[UserId] = @UserIdy una para C.[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!
kspearrin
0

Respuesta wiki comunitaria :

Puede intentar dividir esto en dos consultas y UNION ALLvolver a unirlas.

Su WHEREcláusula está sucediendo todo al final, pero si la divide en:

  • Una consulta donde C.[UserId] = @UserId
  • Otro donde 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.

revs usuario126897
fuente