¿Cómo medir o encontrar el costo de crear un plan de consulta?

18

Tengo un caso típico en el que la detección de parámetros hace que un plan de ejecución "malo" aterrice en la memoria caché del plan, lo que hace que las ejecuciones posteriores de mi procedimiento almacenado sean muy lentas. Puedo "resolver" este problema con variables locales OPTIMIZE FOR ... UNKNOWN, y OPTION(RECOMPILE). Sin embargo, también puedo sumergirme en la consulta e intentar optimizarla.

Estoy tratando de determinar si debería : dado el tiempo limitado para solucionar problemas, me gustaría saber el costo de no hacerlo. Tal como lo veo, si me quedo con OPTION(RECOMPILE), el efecto neto es que se recrea un plan de consulta cada vez que se ejecuta la consulta. Entonces, creo que necesito saber:

¿Cómo saber cuál es el costo de crear un plan de consulta?

Para responder a mi propia pregunta, busqué en Google (por ejemplo, con esta consulta ) y revisé la documentación de las columnas para el dm_exec_query_statsDMV . También he inspeccionado la ventana de salida en SSMS para "Plan de consulta real" para encontrar esta información. Por último, he buscado DBA.SE . Ninguno de esos condujo a una respuesta.

¿Alguien me puede decir? ¿Es posible encontrar o medir el tiempo necesario para la creación del plan?

Jeroen
fuente
55
Recomendaría obtener una copia de Inside the SQL Server Query Optimizer de Benjamin Nevarez . Es gratis. El Capítulo 5 'El proceso de optimización' puede ayudarlo a calcular el tiempo de compilación de su consulta. Por lo menos, es informativo sobre lo que pasa el optimizador para crear un plan de consulta.
Mark Sinkinson

Respuestas:

18

¿Cómo saber cuál es el costo de crear un plan de consulta?

Puede ver las propiedades del nodo raíz en el plan de consulta, por ejemplo:

Extracto de propiedades de raíz
(captura de pantalla del Sentry One Plan Explorer gratuito )

Esta información también está disponible consultando el caché del plan, por ejemplo, utilizando una consulta basada en las siguientes relaciones:

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

Fragmento de resultados

Para un tratamiento completo de las opciones que tiene para manejar este tipo de consultas, vea el artículo recientemente actualizado de Erland Sommarskog .

Paul White reinstala a Monica
fuente
4

Suponiendo que el "costo" es en términos de tiempo (aunque no estoy seguro de qué más podría ser en términos de ;-), al menos, debería ser capaz de tener una idea de ello haciendo algo como lo siguiente:

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

El primer elemento informado en la pestaña "Mensajes" debería ser:

Tiempo de análisis y compilación de SQL Server:

Ejecutaría esto al menos 10 veces y promediaría los milisegundos "CPU" y "transcurrido".

Lo ideal sería ejecutar esto en Producción para poder obtener una estimación de tiempo real, pero rara vez se permite a las personas borrar el caché del plan en Producción. Afortunadamente, a partir de SQL Server 2008, fue posible borrar un plan específico del caché. En cuyo caso puede hacer lo siguiente:

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

Sin embargo, dependiendo de la variabilidad de los valores que se pasan para los parámetros que causan el plan en caché "incorrecto", hay otro método para considerar que es un punto medio entre OPTION(RECOMPILE)y OPTION(OPTIMIZE FOR UNKNOWN): SQL dinámico. Sí lo dije. E incluso me refiero a SQL dinámico no parametrizado. Aquí es por qué.

Claramente tiene datos que tienen una distribución desigual, al menos en términos de uno o más valores de parámetros de entrada. Las desventajas de las opciones mencionadas son:

  • OPTION(RECOMPILE)generará un plan para cada ejecución y nunca podrá beneficiarse de ninguna reutilización del plan, incluso si los valores de los parámetros pasados ​​nuevamente son idénticos a las ejecuciones anteriores. Para los procs que se llaman con frecuencia, una vez cada pocos segundos o con más frecuencia, esto lo salvará de la horrible situación ocasional, pero aún lo dejará en una situación no tan buena.

  • OPTION(OPTIMIZE FOR (@Param = value)) generará un plan basado en ese valor en particular, que podría ayudar en varios casos pero aún así dejarlo abierto al problema actual.

  • OPTION(OPTIMIZE FOR UNKNOWN)generará un plan basado en lo que equivale a una distribución promedio, lo que ayudará a algunas consultas pero perjudicará a otras. Esto debería ser lo mismo que la opción de usar variables locales.

Sin embargo, el SQL dinámico, cuando se hace correctamente , permitirá que los diversos valores que se pasan tengan sus propios planes de consulta separados que son ideales (bueno, tanto como lo serán). El costo principal aquí es que a medida que aumenta la variedad de valores que se pasan, aumenta el número de planes de ejecución en la memoria caché y ocupan memoria. Los costos menores son:

  • necesidad de validar parámetros de cadena para evitar inyecciones SQL

  • posiblemente necesite configurar un Certificado y un Usuario basado en Certificado para mantener una abstracción de seguridad ideal ya que Dynamic SQL requiere permisos de tabla directos.

Entonces, así es como manejé esta situación cuando tuve procesos que se llamaron más de una vez por segundo y golpearon varias tablas, cada una con millones de filas. Lo intenté, OPTION(RECOMPILE)pero esto resultó ser demasiado perjudicial para el proceso en el 99% de los casos que no tenían el problema de detección de parámetros / mal plan de caché. Y tenga en cuenta que uno de estos procesos tenía aproximadamente 15 consultas y solo 3 - 5 de ellos se convirtieron a SQL dinámico como se describe aquí; El SQL dinámico no se usó a menos que fuera necesario para una consulta particular.

  1. Si hay múltiples parámetros de entrada para el procedimiento almacenado, descubra cuáles se usan con columnas que tienen distribuciones de datos muy dispares (y, por lo tanto, causan este problema) y cuáles se usan con columnas que tienen distribuciones más uniformes (y no deberían causando este problema).

  2. Cree la cadena de SQL dinámico utilizando parámetros para los parámetros de entrada de proceso que están asociados con columnas distribuidas uniformemente. Esta parametrización ayuda a reducir el aumento resultante en los planes de ejecución en la memoria caché relacionada con esta consulta.

  3. Para los parámetros restantes que están asociados con distribuciones muy variadas, se deben concatenar en el SQL dinámico como valores literales. Dado que una consulta única está determinada por cualquier cambio en el texto de la consulta, tener WHERE StatusID = 1es una consulta diferente y, por lo tanto, un plan de consulta diferente que tener WHERE StatusID = 2.

  4. Si alguno de los parámetros de entrada de proceso que se van a concatenar en el texto de la consulta son cadenas, entonces deben validarse para proteger contra la inyección de SQL (aunque es menos probable que esto ocurra si las cadenas que se pasan son generadas por aplicación y no un usuario, pero aún así). Al menos haga esto REPLACE(@Param, '''', '''''')para asegurarse de que las comillas simples se conviertan en comillas simples escapadas.

  5. Si es necesario, cree un Certificado que se utilizará para crear un Usuario y firme el procedimiento almacenado de manera tal que los permisos directos de la tabla se otorguen solo al nuevo Usuario basado en el Certificado y no [public]a los Usuarios que de otro modo no deberían tener dichos permisos. .

Proceso de ejemplo:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;
Solomon Rutzky
fuente
¡Gracias por tomarse (bastante) tiempo para responder! Sin embargo, soy un poco escéptico sobre el primer bit sobre cómo obtener el tiempo de compilación, dado que es un factor 3 menor que el resultado que obtengo al usar el enfoque de @ PaulWhite . - La segunda de bits de SQL dinámico es interesante (aunque también requeriría tiempo para poner en práctica, por lo menos algo más que golpear una OPTIONen mi consulta), y no me lastimar demasiado mucho ya que este procedimiento almacenado es bien aprovechada en las pruebas de integración. - En cualquier caso: ¡gracias por tus ideas!
Jeroen