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.
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).
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.
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 = 1
es una consulta diferente y, por lo tanto, un plan de consulta diferente que tener WHERE StatusID = 2
.
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.
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;