Con la llegada de SQL Server 2025, Microsoft introducirá una serie de mejoras orientadas a optimizar el rendimiento del motor. Una de ellas es la nueva configuración OPTIMIZED_SP_EXECUTESQL, pensada específicamente para entornos que hacen uso intensivo de SQL dinámico a través de sp_executesql. En este artículo profundizo en su funcionamiento, cómo activarla, qué impacto tiene en la caché de planes y cómo se comporta bajo carga concurrente.
Contexto: el problema con sp_executesql y el plan cache
Cuando ejecutamos consultas dinámicas con sp_executesql, especialmente en entornos con alta concurrencia, SQL Server puede compilar múltiples planes para lo que, conceptualmente, es la misma consulta. Aunque el texto del lote (batch text) sea idéntico y se usen parámetros, el compilador no siempre consigue identificarlo como la misma ejecución, lo que termina generando entradas repetidas en la plan cache.
Esto da lugar a varios problemas conocidos:
- Bloat en la plan cache, con decenas o cientos de planes similares almacenados.
- Consumo innecesario de CPU por compilaciones redundantes.
- Contención en la compilación, cuando múltiples sesiones intentan compilar la misma consulta al mismo tiempo.
Con OPTIMIZED_SP_EXECUTESQL, SQL Server introduce un nuevo mecanismo para mitigar estos efectos.
¿Qué es OPTIMIZED SP_EXECUTESQL?
Se trata de una configuración a nivel de base de datos que se encuentra desactivada por defecto y que puede activarse mediante ALTER DATABASE SCOPED CONFIGURATION. Su objetivo principal es optimizar la forma en que SQL Server gestiona la compilación y almacenamiento en caché de planes asociados a ejecuciones de sp_executesql.
Cuando se activa esta opción, el comportamiento cambia significativamente:
- La primera ejecución de un lote dinámico mediante sp_executesql compila el plan normalmente.
- Otras sesiones concurrentes que ejecutan el mismo lote esperan a que finalice la compilación.
- Una vez compilado, todas las sesiones reutilizan el mismo plan, del mismo modo que lo harían si fuera un procedimiento almacenado.
Requisitos y dependencias
Lo primero, obviamente, es tener SQL Server 2025 y tu base de datos en un nivel de compatibilidad 170. Además, si en tu base de datos tienes activada la opción de actualización automática de estadísticas, Microsoft recomienda habilitar también ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY (más bien es un requisito pero, ¿has visto qué bien me ha quedado lo de “Microsoft recomienda”?). Esto asegura que el nuevo comportamiento no introduzca contención innecesaria durante las actualizaciones estadísticas, en escenarios de alta concurrencia.

Cómo activarlo
Para habilitar la funcionalidad, puedes ejecutar el siguiente bloque:
USE [TuBaseDeDatos];
GO
ALTER DATABASE SCOPED CONFIGURATION SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = ON;
GO
ALTER DATABASE SCOPED CONFIGURATION SET OPTIMIZED_SP_EXECUTESQL =ON;
GO
Recuerda, necesitas estar en nivel de compatibilidad 170 (SQL Server 2025) para que la opción sea válida.
Puedes comprobar el estado actual de la configuración con:
SELECT name, value, value_for_secondary
FROM sys.database_scoped_configurations
WHERE name IN ('OPTIMIZED_SP_EXECUTESQL', 'ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY');
Demostración práctica
Vamos a ejecutar una serie de pruebas en una base de datos StackOverflow (modo de compatibilidad 170) para observar el impacto real de esta configuración en la plan cache.
Paso 1: Asegurar que la opción está desactivada
USE StackOverflow2010;
GO
ALTER DATABASE SCOPED CONFIGURATION SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = OFF;
GO
ALTER DATABASE SCOPED CONFIGURATION SET OPTIMIZED_SP_EXECUTESQL = OFF;
GO
Paso 2: Limpiar la caché de planes
DBCC FREEPROCCACHE;
GO
Paso 3: Ejecutar carga concurrente
Usaremos SQLQueryStress para lanzar la siguiente consulta, que ejecuta sp_executesql de forma concurrente:
DECLARE @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.Posts WHERE PostTypeId = @PostTypeId AND Score > @Score';
DECLARE @params NVARCHAR(MAX) = N'@PostTypeId INT, @Score INT';
EXEC sys.sp_executesql @sql, @params, @PostTypeId = 1, @Score = 2000;
Parámetros en SQLQueryStress:
- Iteraciones por hilo: 10
- Número de hilos: 100

Esto generará 1000 ejecuciones concurrentes de la misma consulta dinámica.
Paso 4: Analizar la plan cache
Una vez completadas las ejecuciones, inspeccionamos el número de planes generados:
SELECT
qs.plan_handle,
qs.sql_handle,
qs.creation_time,
qs.execution_count,
st.text AS sql_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text LIKE '%SELECT COUNT(*) FROM dbo.Posts%'
ORDER BY qs.creation_time DESC;
Resultado esperado (sin optimización): múltiples planes distintos (en pruebas reales, se observan entre 20 y 30 para este escenario), aunque la consulta sea la misma.

Esto evidencia el comportamiento ineficiente por defecto: múltiples planes para la misma carga.
Paso 5: Activar la optimización
USE StackOverflow;
GO
ALTER DATABASE SCOPED CONFIGURATION SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = ON;
GO
ALTER DATABASE SCOPED CONFIGURATION SET OPTIMIZED_SP_EXECUTESQL = ON;
GO
Paso 6: Limpiar de nuevo la caché
DBCC FREEPROCCACHE;
GO
Paso 7: Ejecutar la misma carga
Volvemos a lanzar los 100 hilos concurrentes con SQLQueryStress, exactamente como antes.
Paso 8: Analizar resultados
Ejecutamos de nuevo la consulta sobre la plan cache:
SELECT
qs.plan_handle,
qs.sql_handle,
qs.creation_time,
qs.execution_count,
st.text AS sql_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text LIKE '%SELECT COUNT(*) FROM dbo.Posts%'
ORDER BY qs.creation_time DESC;
Resultado esperado (con optimización): una sola entrada en la caché de planes, con una execution_count acumulada de 1000. El compilador ha tratado la consulta dinámica como si fuese un procedimiento almacenado.

Este comportamiento elimina de raíz el bloat en la caché y evita la compilación paralela.
¿Qué hace SQL Server exactamente?
El mecanismo detrás de esta optimización replica el modelo clásico de los procedimientos almacenados:
- La primera ejecución de una consulta concreta compila el plan.
- El resto de ejecuciones esperan a que ese plan esté disponible.
- Una vez compilado, todas las ejecuciones reutilizan ese mismo plan.
Este comportamiento garantiza una mayor eficiencia tanto en uso de CPU como en gestión de memoria. Ya no veremos un plan por cada sesión, sino un único plan compartido y referenciado por todas.
Consideraciones finales
Este cambio solo aplica a sp_executesql. No afecta a EXEC() ni a otros mecanismos de ejecución dinámica.
Las consultas deben ser realmente idénticas (texto y parámetros) para que se detecte la coincidencia.
Si tu aplicación genera SQL dinámico con pequeñas variaciones en los literales o estructura, el beneficio será menor.
La mejora se aprecia especialmente en entornos con alta concurrencia o patrones repetitivos.
Conclusión
OPTIMIZED_SP_EXECUTESQL es una de las mejoras más importantes en la gestión del plan cache que hemos visto en años. Su activación puede suponer una diferencia notable en servidores que sufrían problemas de rendimiento por exceso de compilaciones o saturación de memoria con planes efímeros.
Es una de esas configuraciones que, bien aplicada, no solo mejora el rendimiento, sino que permite aprovechar mejor los recursos existentes. No es una solución mágica, pero sí una herramienta eficaz para un problema conocido.
Si trabajas con aplicaciones que generan SQL dinámico intensivo y tienes pensado actualizar SQL Server 2025 cuando sea estable, este cambio debería estar en tu lista de tareas prioritarias.
Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

