Está en la página 1de 60

2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

¿Lento en la aplicación, rápido en SSMS?
Comprender los misterios del rendimiento
Un texto SQL de Erland Sommarskog , SQL Server MVP. Última revisión : 2021-04-12.

Los derechos de autor se aplican a este texto. Consulte aquí las convenciones de fuentes utilizadas en este artículo.
Este artículo también está disponible en ruso , traducido por Dima Piliugin.

1. Introducción
Cuando leo varios foros sobre SQL Server, con frecuencia veo preguntas de carteles profundamente desconcertados. Han
identificado una consulta lenta o un procedimiento almacenado lento en su aplicación. Toman el lote de SQL de la
aplicación y lo ejecutan en SQL Server Management Studio (SSMS) para analizarlo, solo para descubrir que la respuesta
es instantánea. En este punto, se inclinan a pensar que SQL Server tiene que ver con la magia. Un misterio similar es
cuando un desarrollador ha extraído una consulta en su procedimiento almacenado para ejecutarlo de forma independiente
solo para descubrir que se ejecuta mucho más rápido, o mucho más lento, que dentro del procedimiento.

No, SQL Server no se trata de magia. Pero si no tiene una buena comprensión de cómo SQL Server compila consultas y
mantiene su caché de plan, puede parecer que sí. Además, hay algunas combinaciones desafortunadas de diferentes valores
predeterminados en diferentes entornos. En este artículo, intentaré aclarar por qué obtiene este comportamiento
aparentemente inconsistente. Explico cómo SQL Server compila un procedimiento almacenado, qué es el rastreo de
parámetros y por qué es parte de la ecuación en la gran mayoría de estas situaciones confusas. Explico cómo SQL Server
usa el caché y por qué puede haber múltiples entradas para un procedimiento en el caché. Una vez que haya llegado hasta
aquí, comprenderá por qué la consulta se ejecuta mucho más rápido en SSMS.

Para comprender cómo abordar ese problema de rendimiento en su aplicación, debe seguir leyendo. Primero hago una
pequeña pausa en el tema de la detección de parámetros para analizar algunas situaciones en las que existen otras razones
para la diferencia en el rendimiento. A esto le siguen dos capítulos sobre cómo tratar los problemas de rendimiento en los
que está involucrada la detección de parámetros. El primero es acerca de la recopilación de información. En el segundo
capítulo analizo algunos escenarios, tanto situaciones del mundo real que he encontrado como otras más genéricas, y
posibles soluciones. Luego viene un capítulo en el que analizo cómo se compila el SQL dinámico e interactúa con el caché
del plan y por qué hay más razones por las que puede experimentar diferencias en el rendimiento entre SSMS y la
aplicación con SQL dinámico. En el último capítulo, analizo cómo puede usar Query Store, una función que se introdujo
en SQL 2016 para la resolución de problemas. Al final hay una sección con enlaces a documentos técnicos de Microsoft y
documentos similares en esta área.

Tabla de contenido
1. Introducción
1.1 presunciones
2. Cómo SQL Server compila un procedimiento almacenado
2.1 ¿Qué es un procedimiento almacenado?
2.2 Cómo SQL Server genera el plan de consulta
2.3 Colocar el plan de consulta en la memoria caché
2.4 Diferentes planes para diferentes entornos
2.5 La configuración predeterminada
2.6 Los efectos de la recompilación de sentencias
2.7 Recompilación de instrucciones y variables de tabla y parámetros con valores de tabla
2.8 La historia hasta ahora
3. No siempre se trata de rastrear parámetros...
3.1 Sustitución de variables y parámetros
3.2 Bloqueo
3.3 Configuración de la base de datos
3.4 Vistas indexadas y similares
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 1/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

3.5 Un problema con los servidores vinculados


3.6Inglés
¿Podría ser MARTE?
Español

3.7 El efecto de las transacciones
4. Obtención de información para resolver problemas de rastreo de parámetros
4.1 Obtener los hechos necesarios
4.2 ¿Cuál es la Declaración Lenta?
4.3 Obtener los planes y parámetros de consulta con Management Studio
4.4 Obtener los planes y parámetros de consulta directamente desde la memoria caché del plan
4.5 Plan de consulta en vivo
4.6 Obtener el plan de ejecución real más reciente
4.7 Obtención de planes de consulta y parámetros de un seguimiento
4.8 Obtener definiciones de tablas e índices
4.9 Búsqueda de información sobre estadísticas
5. Ejemplos de cómo solucionar problemas de detección de parámetros
5.1 Una no solución
5.2 El mejor índice depende de la entrada
5.3 Condiciones de búsqueda dinámica
5.4 Revisión de indexación
5.5 El caso de la caché de aplicaciones
5.6 Arreglando mal SQL
6. SQL dinámico
6.1 ¿Qué es SQL dinámico?
6.2 El texto de consulta es la clave hash
6.3 La importancia del esquema predeterminado
6.4 Auto-parametrización
6.5 Ejecución de consultas de aplicaciones en SSMS
6.6 Guías de planes y congelación de planes
7. Uso del almacén de consultas
7.1 Introducción al Almacén de consultas
7.2 Encontrar claves de caché
7.3 Búsqueda de valores de parámetros rastreados
7.4 Forzar planes con Query Store
7.5 Conclusión sobre el almacén de consultas
8. Observaciones finales
8.1 Otras lecturas
9. Revisiones

1.1 presunciones
La esencia de este artículo se aplica a todas las versiones de SQL Server desde SQL 2005 en adelante. El artículo incluye
varias consultas para inspeccionar el caché del plan. Tenga en cuenta que para ejecutar estas consultas necesita tener el
permiso de nivel de servidor VER ESTADO DEL SERVIDOR .

Para los ejemplos de este artículo, utilizo la base de datos de ejemplo Northwind . Esta es una antigua base de datos de
demostración de Microsoft, que encontrará en el archivo Neptuno.sql . (He modificado la versión original para reemplazar
los tipos LOB heredados con tipos MAX ).

Este no es un artículo para principiantes, pero asumo que el lector tiene experiencia laboral en programación SQL. No
necesita tener ninguna experiencia previa en el ajuste del rendimiento, pero sin duda ayuda si ha mirado un poco los planes
de consulta y si tiene algún conocimiento básico de los índices. No explicaré los conceptos básicos en profundidad, ya que
mi enfoque está un poco más allá de ese punto. Este artículo no le enseñará todo sobre el ajuste del rendimiento, pero al
menos será un comienzo.

Todas las capturas de pantalla y los resultados de este artículo se recopilaron con SSMS 18.8 en una instancia de SQL
Server que ejecuta SQL 2019 CU8. Si usa una versión diferente de SSMS y/o SQL Server, es posible que vea resultados
ligeramente diferentes. Sin embargo, le recomiendo que utilice la versión más reciente de SSMS que puede descargar aquí
.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 2/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

2. Cómo
Inglés SQL Español
Server compila un procedimiento almacenado
En este capítulo veremos cómo SQL Server compila un procedimiento almacenado y usa el caché del plan. Si su
aplicación no usa procedimientos almacenados, sino que envía instrucciones SQL directamente, la mayor parte de lo que
digo en este capítulo sigue siendo aplicable. Pero hay más complicaciones con SQL dinámico, y dado que los hechos
acerca de los procedimientos almacenados son lo suficientemente confusos, he aplazado la discusión sobre SQL dinámico
a un capítulo separado .

2.1 ¿Qué es un procedimiento almacenado?


Puede parecer una pregunta tonta, pero la pregunta a la que me refiero es ¿Qué objetos tienen planes de consulta propios?
SQL Server crea planes de consulta para estos tipos de objetos:

Procedimientos almacenados.
Funciones escalares definidas por el usuario. (Pero ver más abajo.)
Funciones con valores de tabla de varios pasos.
Desencadenadores.

Con una terminología más general y estricta, debería hablar de módulos , pero dado que los procedimientos almacenados
son, con mucho, el tipo de módulo más utilizado, prefiero hablar de procedimientos almacenados para mantenerlo simple.

Para otros tipos de objetos además de los cuatro enumerados anteriormente, SQL Server no crea planes de consulta.
Específicamente, SQL Server no crea planes de consulta para vistas y funciones de tabla en línea. Consultas como:

SELECCIONE abc, def DESDE mi vista


SELECCIONA a, b, c DESDE mytablefunc(9)

no son diferentes de las consultas ad-hoc que acceden a las tablas directamente. Al compilar la consulta, SQL Server
expande la vista/función en la consulta y el optimizador funciona con el texto de consulta expandido.

Hay una cosa más que debemos entender acerca de lo que constituye un procedimiento almacenado. Digamos que tiene
dos procedimientos, donde el exterior llama al interior:

CREAR PROCEDIMIENTO Outer_sp AS

...

EXEC Inner_sp

...

Supongo que la mayoría de la gente piensa que Inner_sp es independiente de Outer_sp y, de hecho, lo es. El plan de
ejecución para Outer_sp no incluye el plan de consulta para Inner_sp , solo la invocación del mismo. Sin embargo, hay
una situación muy similar en la que he notado que los carteles en los foros de SQL a menudo tienen una imagen mental
diferente, a saber, SQL dinámico:

CREAR PROCEDIMIENTO Some_sp AS

DECLARAR @sql nvarchar(MAX),

@params nvarchar(MAX)

SELECCIONAR @sql = 'SELECCIONAR...'

...

EJECUTIVO sp_executesql @sql, @params, @par1, ...

Es importante comprender que esto no es diferente de los procedimientos almacenados anidados. La cadena SQL generada
no forma parte de Some_sp ni aparece en ninguna parte del plan de consulta de Some_sp , pero tiene un plan de consulta
y una entrada de caché propia. Esto se aplica, sin importar si el SQL dinámico se ejecuta a través de EXEC() o
sp_executesql .

A partir de SQL 2019, las funciones escalares definidas por el usuario se han convertido en un caso borroso. En SQL
2019, Microsoft introdujo la incorporación de funciones escalares, lo que es una gran mejora para el rendimiento. No hay
una sintaxis específica para hacer una función escalar en línea, sino que SQL Server decide por sí mismo si es posible en
línea una determinada función. Para confundir más las cosas, la inserción no ocurre en todos los contextos. Por ejemplo, si
tiene una columna calculada que llama a una UDF escalar, la inserción no se realizará, incluso si la función como tal
califica para ello. Por lo tanto, una función escalar definida por el usuario puede tener una entrada de caché propia, pero
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 3/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

primero debe mirar el plan de la consulta con la que está trabajando; es posible que la lógica de la función interna se haya
ampliadoInglés
en el plan. Esto no es
Español nada que discutiremos más adelante en este artículo,

2.2 Cómo SQL Server genera el plan de consulta


Visión general
Cuando ingresa un procedimiento almacenado con CREATE PROCEDURE (o CREATE FUNCTION para una función o
CREATE TRIGGER para un disparador), SQL Server verifica que el código sea sintácticamente correcto y también verifica
que no haga referencia a columnas que no existen. (Pero si hace referencia a tablas que no existen, puede salirse con la
suya, debido a una característica incorrecta conocida como resolución con nombre diferido). Sin embargo, en este punto,
SQL Server no crea ningún plan de consulta, sino que simplemente almacena el texto de la consulta. en la base de datos

No es hasta que un usuario ejecuta el procedimiento, que SQL Server crea el plan. Para cada consulta, SQL Server analiza
las estadísticas de distribución que ha recopilado sobre los datos de las tablas de la consulta. A partir de esto, realiza una
estimación de cuál puede ser la mejor manera de ejecutar la consulta. Esta fase se conoce como optimización . Si bien el
procedimiento se compila de una sola vez, cada consulta se optimiza por sí sola y no se intenta analizar el flujo de
ejecución. Esto tiene una ramificación muy importante: el optimizador no tiene idea de los valores de tiempo de ejecución
de las variables. Sin embargo, sí sabe qué valores especificó el usuario para los parámetros del procedimiento.

Parámetros y Variables
Considere la tabla Pedidos en la base de datos Northwind y estos tres procedimientos:

CREAR PROCEDIMIENTO List_orders_1 AS

SELECCIONE * DESDE Pedidos DONDE Fecha de pedido > '20000101'

Vamos

CREAR PROCEDIMIENTO List_orders_2 @fromdate datetime AS

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

Vamos

CREAR PROCEDIMIENTO List_orders_3 @fromdate datetime AS

DECLARAR @fromdate_copy datetime

SELECCIONE @fromdate_copy = @fromdate

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate_copy

Vamos

Nota : Usar SELECT * en el código de producción es una mala práctica. Lo uso en este artículo para mantener los ejemplos concisos.

Luego ejecutamos los procedimientos de esta manera:

EXEC Lista_pedidos_1

EXEC List_orders_2 '20000101'

EXEC List_orders_3 '20000101'

Antes de ejecutar los procedimientos, habilite Incluir plan de ejecución real en el menú Consulta . (También hay un botón
en la barra de herramientas y Ctrl-M es el atajo de teclado normal). Si observa los planes de consulta para los
procedimientos, verá que los primeros dos procedimientos tienen planes idénticos:

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 4/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Es decir, SQL Server busca el índice en OrderDate y utiliza una búsqueda clave para obtener los demás datos. El plan
para la tercera
Inglés ejecución

es diferente:
Español

En este caso, SQL Server escanea la tabla. (Tenga en cuenta que en un índice agrupado, las páginas hoja contienen los
datos, por lo que un escaneo de índice agrupado y un escaneo de tabla son esencialmente lo mismo). ¿Por qué esta
diferencia? Para entender por qué el optimizador toma ciertas decisiones, siempre es una buena idea mirar con qué
estimaciones está trabajando. Si pasa el mouse sobre los dos operadores Buscar y el operador Escanear , verá ventanas
emergentes similares a las siguientes.

List_orders_1 List_orders_2

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 5/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

List_orders_3

El elemento interesante es el Número estimado de filas por ejecución . Para los primeros dos procedimientos, SQL Server
estima que se devolverá una fila, pero para List_orders_3 , la estimación es de 249 filas. Esta diferencia en las
estimaciones explica la diferente elección de planes. Búsqueda de índice + Búsqueda de clavees una buena estrategia para
devolver una cantidad menor de filas de una tabla. Pero cuando más filas coinciden con los criterios de búsqueda, el costo
aumenta y existe una mayor probabilidad de que SQL Server necesite acceder a la misma página de datos más de una vez.
En el caso extremo en el que se devuelven todas las filas, una exploración de tabla es mucho más eficiente que buscar y
buscar. Con un escaneo, SQL Server tiene que leer cada página de datos exactamente una vez, mientras que con la
búsqueda + búsqueda clave, cada página se visitará una vez por cada fila de la página. La tabla Pedidos en Northwind
tiene 830 filas, y cuando SQL Server estima que se devolverán hasta 249 filas, concluye (correctamente) que la
exploración es la mejor opción.

¿De dónde vienen estas estimaciones?


Ahora sabemos por qué el optimizador llega a diferentes planes de ejecución: porque las estimaciones son diferentes. Pero
eso solo lleva a la siguiente pregunta: ¿por qué las estimaciones son diferentes? Ese es el tema clave de este artículo.

En el primer procedimiento, la fecha es una constante, lo que significa que SQL Server solo necesita considerar
exactamente este caso. Interroga las estadísticas de la tabla Pedidos , lo que indica que no hay filas con una Fecha de
pedido en el tercer milenio. (Todos los pedidos en la base de datos Northwind son de 1996 a 1998). Dado que las
estadísticas son estadísticas, SQL Server no puede estar seguro de que la consulta no devuelva ninguna fila, por lo que se
conforma con una estimación de una sola fila.

En el caso de List_orders_2 , la consulta es contra una variable, o más precisamente un parámetro. Al realizar la
optimización, SQL Server sabe que el procedimiento fue invocado con el valor 2000-01-01. Dado que no realiza ningún
análisis de flujo, no puede decir con certeza si el parámetro tendrá este valor cuando se ejecute la consulta. Sin embargo,
utiliza el valor de entrada para obtener una estimación, que es la misma que para List_orders_1 : una sola fila. Esta
estrategia de observar los valores de los parámetros de entrada al optimizar un procedimiento almacenado se conoce como
rastreo de parámetros .

En el último procedimiento, todo es diferente. El valor de entrada se copia en una variable local, pero cuando SQL Server
crea el plan, no comprende esto y se dice a sí mismo que no sé cuál será el valor de esta variable . Debido a esto, aplica
una suposición estándar, que para una operación de desigualdad como >es una tasa de aciertos del 30 %. El 30 % de 830 es
de hecho 249.
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 6/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Aquí hay una variación del tema:


Inglés Español

CREAR PROCEDIMIENTO List_orders_4 @fromdate datetime = NULL AS

SI @fromdate ES NULO

SELECCIONE @desdefecha = '19900101'

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

En este procedimiento, el parámetro es opcional y, si el usuario no completa el parámetro, se enumeran todas las órdenes.
Digamos que el usuario invoca el procedimiento como:

EXEC Lista_pedidos_4

El plan de ejecución es idéntico al plan para List_orders_1 y List_orders_2 . Es decir, Index Seek + Key Lookup , a
pesar de que se devuelven todas las órdenes. Si observa la ventana emergente del operador Index Seek , verá que es
idéntica a la ventana emergente de List_orders_2 pero en un sentido, el número real de filas. Al compilar el
procedimiento, SQL Server no sabe que el valor de @fromdate cambia, pero compila el procedimiento suponiendo que
@fromdate tiene el valor NULL . Dado que todas las comparaciones con NULL dan como resultado DESCONOCIDO, la
consulta no puede devolver ninguna fila si @fromdate todavía tiene este valor en tiempo de ejecución. Si SQL Server
tomara el valor de entrada como la verdad final, podría construir un plan con solo un escaneo constante que no acceda a la
tabla en absoluto (ejecute la consulta SELECT * FROM Orders WHERE OrderDate > NULLpara ver un ejemplo de esto). Pero
SQL Server debe generar un plan que devuelva el resultado correcto sin importar el valor que tenga @fromdate en tiempo
de ejecución. Por otro lado, no hay obligación de construir un plan que sea el mejor para todos los valores. Por lo tanto,
dado que se supone que no se devolverán filas, SQL Server se conforma con Index Seek . (La estimación sigue siendo que
se devolverá una fila. Esto se debe a que SQL Server nunca usa una estimación de 0 filas).

Este es un ejemplo de cuando el sniffing de parámetros falla, y en este caso particular puede ser mejor escribir el
procedimiento de esta manera:

CREAR PROCEDIMIENTO List_orders_5 @fromdate datetime = NULL AS

DECLARAR @fromdate_copy datetime

SELECCIONE @fromdate_copy = coalesce(@fromdate, '19900101')

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate_copy

Con List_orders_5 siempre obtiene un escaneo de índice agrupado .

Puntos clave
En esta sección hemos aprendido tres cosas muy importantes:

Una constante es una constante, y cuando una consulta incluye una constante, SQL Server puede usar el valor de la
constante con plena confianza, e incluso tomar esos atajos para no acceder a una tabla en absoluto, si puede deducir de
las restricciones que no habrá filas. ser devuelto.
Para un parámetro, SQL Server no conoce el valor de tiempo de ejecución, pero "olfatea" el valor de entrada al
compilar la consulta.
Para una variable local, SQL Server no tiene idea del valor de tiempo de ejecución y aplica suposiciones estándar.
(Cuáles son las suposiciones depende del operador y de lo que se puede deducir de la presencia de índices únicos).

Y hay un corolario de esto: si extrae una consulta de un procedimiento almacenado y reemplaza variables y parámetros
con constantes, ahora tiene una consulta bastante diferente. Más sobre esto más adelante.

Antes de continuar, un poco más sobre lo que puede ver en las versiones modernas de SSMS. Aquí miramos las ventanas
emergentes para ver las estimaciones y los valores reales. Sin embargo, si observa el plan gráfico, puede ver que debajo de
los operadores dice cosas como 0 de 249. Esto significa 0 filas reales de 249 estimadas. A continuación se muestra un
detalle del plan para List_orders_4 . Cuando dice 830 de 1, significa que se devolvieron 830 filas, pero que la estimación
fue una sola fila.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 7/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

Esto no significa que en las versiones modernas de SSMS nunca necesite mirar las ventanas emergentes. Hay más valores
reales y estimados que vale la pena mirar. Por ejemplo, a menudo comparo el Número de ejecuciones con el Número
estimado de ejecuciones . Pero sin duda es útil tener el número estimado y real de filas directamente en el plan gráfico, ya
que una gran diferencia entre los dos suele ser parte de la respuesta a por qué una consulta es lenta, y veremos esto más
adelante en Este artículo.

2.3 Colocar el plan de consulta en la memoria caché


Si SQL Server compilara un procedimiento almacenado, es decir, optimizara y creara un plan de consulta, cada vez que se
ejecuta el procedimiento, existe un gran riesgo de que SQL Server se desmorone por todos los recursos de CPU que
necesitaría. Inmediatamente necesito calificar esto, porque no es cierto para todos los sistemas. En un gran almacén de
datos donde un puñado de analistas de negocios ejecuta consultas complicadas que tardan un minuto en ejecutarse en
promedio, no habría ningún daño si hubiera una compilación cada vez; más bien, podría ser beneficioso. Pero en una base
de datos OLTP donde muchos usuarios ejecutan procedimientos almacenados con consultas cortas y simples, esta
preocupación es muy real.

Por esta razón, SQL Server almacena en caché el plan de consulta para un procedimiento almacenado, de modo que
cuando el próximo usuario ejecute el procedimiento, se puede omitir la fase de compilación y la ejecución puede comenzar
directamente. El plan permanecerá en el caché, hasta que algún evento obligue al plan a salir del caché. Ejemplos de tales
eventos son:

La memoria caché del búfer de SQL Server se utiliza por completo, y SQL Server necesita superar la antigüedad de
los búferes que no se han utilizado durante algún tiempo desde la memoria caché. La memoria caché del búfer incluye
datos de tablas y planes de consulta.
Alguien ejecuta ALTER PROCEDURE en el procedimiento.
Alguien ejecuta sp_recompile en el procedimiento.
Alguien ejecuta el comando DBCC FREEPROCCACHE que borra todo el caché del plan.
Se reinicia SQL Server. Dado que el caché es solo de memoria, el caché no se conserva durante los reinicios.
El cambio de ciertos parámetros de configuración (con sp_configure o a través de las páginas de propiedades del
servidor en SSMS) expulsa todo el caché del plan.

Si ocurre tal evento, se creará un nuevo plan de consulta la próxima vez que se ejecute el procedimiento. SQL Server
volverá a "olfatear" los parámetros de entrada y, si los valores de los parámetros son diferentes esta vez, el nuevo plan de
consulta puede ser diferente del plan anterior.

Hay otros eventos que no provocan que todo el plan de procedimiento se desaloje de la memoria caché, pero que
desencadenan la recompilación de una o más sentencias individuales en el procedimiento. La recompilación se produce la
próxima vez que se ejecuta la instrucción. Esto se aplica incluso si el evento ocurrió después de que el procedimiento
comenzó a ejecutarse. Aquí hay ejemplos de tales eventos:

Cambiar la definición de una tabla que aparece en el extracto.


Quitar o agregar un índice para una tabla que aparece en la declaración. Esto incluye reconstruir un índice con ALTER
INDEX o DBCC DBREINDEX . (Sin embargo, REORGANIZE no desencadenará una recompilación).
Estadísticas nuevas o actualizadas para una tabla en el extracto. Las estadísticas pueden ser creadas y actualizadas por
SQL Server automáticamente. El DBA también puede crear y actualizar estadísticas con los comandos CREAR
ESTADÍSTICAS y ACTUALIZAR ESTADÍSTICAS . Sin embargo, las estadísticas modificadas no siempre provocan la
recompilación. La regla básica es que debería haber habido un cambio en los datos para que se active la
recompilación. Consulte esta publicación de blog de Kimberly Tripp para obtener más detalles.
Alguien ejecuta sp_recompile en una tabla a la que se hace referencia en la declaración.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 8/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Estas listas no son exhaustivas, pero debe observar una cosa que no está allí: ejecutar el procedimiento con diferentes
valores para los parámetros
Inglés 
de entrada de la ejecución original. Es decir, si la segunda invocación de List_orders_2 es:
Español

EXEC List_orders_2 '19900101'

La ejecución seguirá usando el índice en OrderDate , a pesar de que la consulta ahora recupera todos los pedidos. Esto
lleva a una observación muy importante: los valores de los parámetros de la primera ejecución del procedimiento tienen un
gran impacto para las ejecuciones posteriores. Si este primer conjunto de valores por algún motivo es atípico, es posible
que el plan almacenado en caché no sea óptimo para futuras ejecuciones. Esta es la razón por la cual la detección de
parámetros es tan importante.

Nota : para obtener una lista completa de lo que puede causar que se eliminen los planes o que se vuelvan a compilar las declaraciones,
consulte el documento técnico sobre Almacenamiento en caché de planes que se encuentra en la sección Lecturas adicionales .

2.4 Diferentes planes para diferentes entornos


Hay un plan para el procedimiento en el caché. Eso significa que todo el mundo puede usarlo, o? No, en esta sección
aprenderemos que puede haber múltiples planes para el mismo procedimiento en el caché. Para entender esto,
consideremos este ejemplo artificial:

CREAR PROCEDIMIENTO List_orders_6 AS

SELECCIONE *

DESDE Pedidos

DONDE Fecha de pedido > '12/01/1998'

Vamos

ESTABLECER FORMATO DE FECHA dmy

Vamos

EXEC Lista_pedidos_6

Vamos

ESTABLECER FORMATO DE FECHA mdy

Vamos

EXEC Lista_pedidos_6

Vamos

Si ejecuta esto, notará que la primera ejecución devuelve muchas órdenes, mientras que la segunda ejecución no devuelve
órdenes. Y si miras los planes de ejecución, verás que también son diferentes. Para la primera ejecución, el plan es un
escaneo de índice agrupado (que es la mejor opción con tantas filas devueltas), mientras que el segundo plan de ejecución
usa búsqueda de índice con búsqueda de clave (que es la mejor opción cuando no se devuelven filas).

¿Cómo pudo pasar esto? ¿ SET DATEFORMAT provocó la recompilación? No, eso no sería inteligente. En este ejemplo, las
ejecuciones se suceden una tras otra, pero también podrían ser enviadas en paralelo por diferentes usuarios con diferentes
configuraciones para el formato de fecha. Tenga en cuenta que la entrada de un procedimiento almacenado en la memoria
caché del plan no está vinculada a una determinada sesión o usuario, sino que es global para todos los usuarios conectados.

En cambio, la respuesta es que SQL Server crea una segunda entrada de caché para la segunda ejecución del
procedimiento. Podemos ver esto si echamos un vistazo al caché del plan con esta consulta:

SELECCIONE qs.plan_handle, a.attrlist

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA (SELECCIONE epa.attribute + '=' + convert(nvarchar(127), epa.value) + ' '

DESDE sys.dm_exec_plan_attributes(qs.plan_handle) epa

DONDE epa.is_cache_key = 1

ORDEN POR epa.atributo

FOR XML PATH('')) COMO una(lista de atributos)

DONDE est.objectid = object_id ('dbo.List_orders_6')

Y est.dbid = db_id('Northwind')

Recordatorio : necesita el permiso de nivel de servidor VER ESTADO DEL SERVIDOR para ejecutar consultas en el caché del plan.

El DMV (Vista de administración dinámica) sys.dm_exec_query_stats tiene una entrada para cada consulta actualmente
en el caché del plan. Si un procedimiento tiene varias declaraciones, hay una fila por declaración. De interés aquí es
sql_handle y plan_handle . Utilizo sql_handle para determinar con qué procedimiento se relaciona la entrada de la
memoria caché (más adelante veremos ejemplos en los que también recuperamos el texto de la consulta) para que

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 9/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

podamos filtrar todas las demás entradas de la memoria caché. La mayoría de las veces, usa plan_handle para recuperar el
plan de consulta
Inglés en sí, Español 
y veremos un ejemplo de esto más adelante, pero en esta consulta accedo a un DMV que devuelve
los atributos del plan de consulta. Más específicamente, devuelvo los atributos que son claves de caché. Cuando hay más
de una entrada en la caché para el mismo procedimiento, las entradas tienen al menos una diferencia en las claves de
caché. Una clave de caché es una configuración de tiempo de ejecución que, por un motivo u otro, requiere un plan de
consulta diferente. La mayoría de estos ajustes se controlan con un comando SET , pero no todos.

La consulta anterior devuelve dos filas, lo que indica que hay dos entradas para el procedimiento en la memoria caché. La
salida puede verse así:

plan_handle attrlist

------------------------------- ------------------- ------------------------------

0x0500070064EFCA5DB8A0A90500... compat_level=150 date_first=7 date_format=1

set_opciones=4347 id_usuario=1

0x0500070064EFCA5DB8A0A80500... compat_level=150 date_first=7 date_format=2

set_opciones=4347 id_usuario=1

Para ahorrar espacio, abrevié los identificadores del plan y eliminé muchos de los valores en la columna attrlist . También
he doblado esa columna en dos líneas. Si ejecuta la consulta usted mismo, puede ver la lista completa de claves de caché, y
son bastantes de ellas. Si busca el tema de sys.dm_exec_plan_attributes en Books Online, verá descripciones de muchos
de los atributos del plan, pero también notará que no todas las claves de caché están documentadas. En este artículo, no me
sumergiré en todas las claves de caché, ni siquiera en las documentadas, sino que me centraré solo en las más importantes.

Como dije, el ejemplo es artificial, pero ilustra bien por qué los planes de consulta deben ser diferentes: diferentes
formatos de fecha pueden producir resultados diferentes. Un ejemplo algo más normal es este:
EXEC sp_recompilar List_orders_2

Vamos

ESTABLECER FORMATO DE FECHA dmy

Vamos

EXEC List_orders_2 '12/01/1998'

Vamos

ESTABLECER FORMATO DE FECHA mdy

Vamos

EXEC List_orders_2 '12/01/1998'

Vamos

(La sp_recompile inicial es para asegurarse de que se vacíe el plan del ejemplo anterior). Este ejemplo produce los
mismos resultados y los mismos planes que con List_orders_6 anterior. Es decir, los dos planes de consulta usan el valor
del parámetro real cuando se crea el plan respectivo. La primera consulta utiliza el 12 de enero de 1998 y la segunda el 1
de diciembre de 1998.

Una clave de caché muy importante es set_options . Esta es una máscara de bits que proporciona la configuración de una
serie de opciones SET que pueden estar activadas o desactivadas . Si busca más en el tema de sys.dm_exec_plan_attributes
, encontrará una lista que detalla qué opción SET describe cada bit. (También verá que hay algunos elementos más que no
están controlados por el comando SET ). Por lo tanto, si dos conexiones tienen cualquiera de estas opciones configuradas
de manera diferente, las conexiones usarán diferentes entradas de caché para el mismo procedimiento y, por lo tanto,
podría estar utilizando diferentes planes de consulta, posiblemente con una gran diferencia en el rendimiento.

Una forma de traducir el atributo set_options es ejecutar esta consulta:

SELECCIONE convertir (binario (4), 4347)

Esto nos dice que el valor hexadecimal para 4347 es 0x10FB. Luego podemos buscar en Books Online y seguir la tabla
para saber que las siguientes opciones de SET están vigentes: ANSI_PADDING , Parallel Plan,
CONCAT_NULL_YIELDS_NULL , ANSI_WARNINGS , ANSI_NULLS , QUOTED_IDENTIFIER , ANSI_NULL_DFLT_ON y
ARITHABORT .

También puede usar esta función con valores de tabla que he escrito y ejecutado:
SELECCIONE Set_option DESDE setoptions (4347) ORDENAR POR Set_option

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 10/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Nota : Es posible que se pregunte qué está haciendo Parallel Plan aquí, sobre todo porque el plan en el ejemplo no es paralelo. Cuando
SQL Server crea un planEspañol
Inglés 
paralelo para una consulta, más adelante también puede crear un plan no paralelo si la carga de la CPU en el
servidor es tal que no es defendible ejecutar un plan paralelo. Parece que para un plan que siempre es en serie, el bit para el plan
paralelo está configurado en set_options .

Para simplificar la discusión, podemos decir que cada una de estas opciones SET ( ANSI_PADDING , ANSI_NULLS , etc.)
es una clave de caché en sí misma. El hecho de que se sumen en un valor numérico singular es solo una cuestión de
empaque.

2.5 La configuración predeterminada


Casi todas las opciones de SET ON / OFF que son claves de caché existen por motivos heredados. Originalmente, en un
pasado oscuro y distante, SQL Server incluía una serie de comportamientos que violaban el estándar ANSI para SQL. Con
SQL Server 6.5, Microsoft introdujo todas estas opciones SET (excepto ARITHABORT , que ya estaba en el producto en
4.x), para permitir a los usuarios utilizar SQL Server de forma compatible con ANSI. En SQL 6.5, tenía que usar las
opciones SET explícitamente para cumplir con ANSI, pero con SQL 7, Microsoft cambió los valores predeterminados para
los clientes que usaban las nuevas versiones de las API ODBC y OLE DB. Las opciones SET aún permanecían para
proporcionar compatibilidad con versiones anteriores para clientes más antiguos.

Nota: En caso de que tenga curiosidad sobre el efecto que tienen estas opciones SET, lo remito a Books Online. Algunos de ellos son
bastante sencillos de explicar, mientras que otros son demasiado confusos. Para comprender este artículo, solo necesita comprender que
existen y qué impacto tienen en el caché del plan.

Por desgracia, Microsoft no cambió los valores predeterminados con total consistencia, e incluso hoy en día los valores
predeterminados dependen de cómo se conecte, como se detalla en la tabla a continuación.

Aplicaciones que utilizan SSMS SQLCMD, Biblioteca DB


ADO .Net, ODBC u OLE DB OSQL, BCP, (muy antigua)
Agente SQL Server
ANSI_NULL_DFLT_ON EN EN EN APAGADO
ANSI_NULLS EN EN EN APAGADO
ANSI_PADDING EN EN EN APAGADO
ANSI_ADVERTENCIAS EN EN EN APAGADO
CONCAT_NULL_YIELDS_NULL EN EN EN APAGADO
QUOTED_IDENTIFICADOR EN EN APAGADO APAGADO
ARITHABORTO APAGADO EN APAGADO APAGADO

Es posible que vea a dónde está llegando esto. Su aplicación se conecta con ARITHABORT OFF , pero cuando ejecuta la
consulta en SSMS, ARITHABORT está ON y, por lo tanto, no reutilizará la entrada de caché que usa la aplicación, pero
SQL Server compilará el procedimiento nuevamente, olfateando los valores de sus parámetros actuales, y usted puede
obtener un plan diferente al de la aplicación. Ahí tienes una probable respuesta a la pregunta inicial de este artículo. Hay
algunas posibilidades más que veremos en el próximo capítulo, pero, con mucho, la razón más común para que la
aplicación sea lenta y rápida en SSMS es la detección de parámetros y los diferentes valores predeterminados para
ARITHABORT.. (Si eso era todo lo que quería saber, puede dejar de leer. Si desea solucionar su problema de rendimiento,
¡espere! Y no, poner SET ARITHABORT ON en el procedimiento no es la solución).

Además del comando SET y los valores predeterminados anteriores, ALTER DATABASE le permite decir que una determinada
opción SET siempre debe estar activada de forma predeterminada en una base de datos y, por lo tanto, anular el valor
predeterminado establecido por la API. Estas opciones están pensadas para aplicaciones muy antiguas que ejecutan DB-
Library, como se indica en la columna de arriba a la derecha. Si bien la sintaxis puede indicarlo, no puede especificar que
una opción deba estar DESACTIVADA de esta manera. Además, tenga en cuenta que si prueba estas opciones desde
Management Studio, puede parecer que no funcionan, ya que SSMS envía comandos SET explícitos , anulando cualquier
valor predeterminado. También hay una configuración a nivel de servidor para el mismo propósito, la opción de
configuración de opciones de usuarioque es un poco máscara. Puede configurar los bits individuales en la máscara desde
las páginas de Conexión de las Propiedades del servidor en Management Studio. En general, no recomiendo controlar los
valores predeterminados de esta manera, ya que, en mi opinión, sirven principalmente para aumentar la confusión.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 11/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

No siempre se aplica la configuración de tiempo de ejecución de una opción. Cuando crea un procedimiento, una vista,
una tabla,
esto:
Inglés 
etc., la configuración
Español de ANSI_NULLS y QUOTED_IDENTIFIER se guarda con el objeto. Es decir, si ejecutas

ESTABLECER ANSI_NULLS, QUOTED_IDENTIFIER DESACTIVADO

Vamos

CREAR PROCEDIMIENTO estúpido @x int AS

SI @x = NULL PRINT "@x es NULL"

Vamos

ESTABLECER ANSI_NULLS, QUOTED_IDENTIFIER EN

Vamos

EXEC estúpido NULL

se imprimirá

@x es NULO

(Cuando QUOTED_IDENTIFIER está DESACTIVADO , las comillas dobles( ") son un delimitador de cadena al igual que las
comillas simples( '). Cuando la configuración está ACTIVADA , las comillas dobles delimitan los identificadores de la
misma manera que lo hacen los corchetes ( []) y la instrucción PRINT produciría un error de compilación.)

Además, la configuración de ANSI_PADDING se guarda por columna de la tabla donde corresponda, es decir, los tipos de
datos varchar y varbinary .

Todas estas opciones y diferentes valores predeterminados son ciertamente confusos, pero aquí hay algunos consejos.
Primero, recuerde que las primeras seis de estas siete opciones existen solo para proporcionar compatibilidad con
versiones anteriores, por lo que hay pocas razones por las que deba tener alguna de ellas DESACTIVADA . Sí, hay
situaciones en las que puede parecer que algunos de ellos compran un poco más de comodidad si están APAGADOS , pero
no caigas en esa tentación. Una complicación aquí, sin embargo, es que las herramientas de SQL Server lanzan comandos
SET para algunas de estas opciones cuando escribe objetos. Afortunadamente, producen principalmente comandos SET ON que
son inofensivos. (Pero cuando escribe una tabla, los scripts pueden tener un SET ANSI_PADDING OFFal final. Puede
controlar esto en Herramientas ->Opciones ->Scripting donde puede configurar los comandos Script ANSI_PADDING en
Falso, lo cual recomiendo).

A continuación, cuando se trata de ARITHABORT , debe saber que en SQL 2005 y versiones posteriores, esta
configuración no tiene ningún impacto siempre que ANSI_WARNINGS esté activado . (Para ser precisos: no tiene impacto
siempre que el nivel de compatibilidad sea 90 o superior). Por lo tanto, no hay razón para activarlo por el bien del asunto.
Y cuando se trata de SQL Server Management Studio, es posible que desee hacerse un favor y abrir este cuadro de diálogo
y desmarcar SET ARITHABORT como se resalta:

Esto cambiará su configuración predeterminada para ARITHABORT cuando se conecte con SSMS. No le ayudará a hacer
que su aplicación se ejecute más rápido, pero al menos no tendrá que quedarse perplejo al obtener un rendimiento
diferente en SQL Server Management Studio.

Como referencia, a continuación se muestra cómo debería verse la página ANSI. Una recomendación muy fuerte: ¡nunca
cambies nada en esta página!

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 12/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

Cuando se trata de SQLCMD y OSQL, acostúmbrese a usar siempre la opción, lo que hace que estas herramientas se
ejecuten con QUOTED_IDENTIFIER ON . La opción correspondiente para BCP es . (Para confundir, tiene un efecto más
para BCP que analizo en mi artículo Uso de las herramientas de carga masiva en SQL Server ). Es un poco más difícil en
el Agente, ya que no hay forma de cambiar el valor predeterminado para el Agente, al menos yo no he encontrado
ninguno. Por otra parte, si solo ejecuta procedimientos almacenados desde los pasos de su trabajo, esto no es un problema,
ya que la configuración guardada para los procedimientos almacenados tiene prioridad. Pero si ejecutara lotes sueltos de
SQL desde trabajos del Agente, podría enfrentar el problema con diferentes planes de consulta en el trabajo y SSMS
debido a los diferentes valores predeterminados para -I-q-qIDENTIFICADOR_COTIZADO . Para dichos trabajos, siempre
debe incluir el comando SET QUOTED_IDENTIFIER ON como el primer comando en el paso del trabajo.

Ya vimos SET DATEFORMAT , y hay dos opciones más en ese grupo: LANGUAGE y DATEFIRST . El idioma
predeterminado se configura por usuario y existe una opción de configuración para todo el servidor que controla cuál es el
idioma predeterminado para los nuevos usuarios. El idioma predeterminado controla el idioma predeterminado para los
otros dos. Dado que son claves de caché, esto significa que dos usuarios con diferentes idiomas predeterminados tendrán
diferentes entradas de caché y, por lo tanto, pueden tener diferentes planes de consulta.

Mi recomendación es que intente evitar depender por completo de la configuración de idioma y fecha en SQL Server. Por
ejemplo, en la medida en que use literales de fecha, use un formato que siempre se interprete igual, como AAAAMMDD.
(Para obtener más detalles sobre los formatos de fecha, consulte el artículo La guía definitiva para los tipos de datos de
fecha y hora del MVP de SQL Server, Tibor Karaszi). propio que confiar en la configuración de idioma en SQL Server.

2.6 Los efectos de la recompilación de sentencias


Para obtener una imagen completa de cómo SQL Server crea el plan de consulta, debemos estudiar qué sucede cuando se
vuelven a compilar las declaraciones individuales . Anteriormente, mencioné algunas situaciones en las que puede suceder,
pero en ese momento no entré en detalles.

El siguiente procedimiento ciertamente es artificial, pero sirve para demostrar lo que sucede.

CREAR PROCEDIMIENTO List_orders_7 @fromdate datetime,

@ix bit AS

SELECCIONE @fromdate = dateadd(AÑO, 2, @fromdate)

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

SI @ix = 1 CREAR prueba de ÍNDICE EN Órdenes (ShipVia)

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

Vamos

EXEC List_orders_7 '19980101', 1

Cuando ejecute esto y observe el plan de ejecución real, verá que el plan para el primer SELECT es un escaneo de índice
agrupado , que concuerda con lo que hemos aprendido hasta ahora. SQL Server rastrea el valor 1998-01-01 y estima que la
consulta devolverá 267 filas, que son demasiadas para leer con Index Seek + Key Lookup . Lo que SQL Server no sabe es
que el valor de @fromdate cambia antes de que se ejecuten las consultas. Sin embargo, el plan para la segunda consulta,
idéntica, es precisamente Index Seek + Key Lookup y la estimación es que se devolverá una fila. Esto se debe a que
CREAR ÍNDICEestablece una marca de que el esquema de la tabla Pedidos ha cambiado, lo que desencadena una
recompilación de la segunda instrucción SELECT . Al volver a compilar la declaración, SQL Server rastrea el valor del
parámetro que está vigente en este punto y, por lo tanto, encuentra el mejor plan.

Vuelva a ejecutar el procedimiento, pero con parámetros diferentes (tenga en cuenta que la fecha es dos años anterior):

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 13/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
EXEC List_orders_7 '19960101', 0

Los planos son los mismos que


Inglés Español
en la primera ejecución, lo que resulta un poco más emocionante de lo que puede parecer a
primera vista. En esta segunda ejecución, la primera consulta se vuelve a compilar debido al índice agregado, pero esta vez
el escaneo es el plan "correcto", ya que recuperamos alrededor de un tercio de los pedidos. Sin embargo, dado que la
segunda consulta no se vuelve a compilar ahora, la segunda consulta se ejecuta con Index Seek de la ejecución anterior,
aunque ahora no es un plan eficiente.

Antes de continuar, limpie:


Prueba DROP INDEX ON Órdenes

PROCEDIMIENTO DE ABANDONO List_orders_7

Como dije, este ejemplo es artificial. Lo hice de esa manera, porque quería un ejemplo compacto que fuera fácil de
ejecutar. En una situación de la vida real, es posible que tenga un procedimiento que utilice el mismo parámetro en dos
consultas en tablas diferentes. El DBA crea un nuevo índice en una de las tablas, lo que hace que se vuelva a compilar la
consulta en esa tabla, mientras que la otra consulta no. La conclusión clave aquí es que los planes para dos declaraciones
en un procedimiento pueden haber sido compilados para diferentes valores de parámetros "olfateados".

Cuando hemos visto esto, parece lógico que esto se pueda extender también a las variables locales. Pero este no es el caso:
CREAR PROCEDIMIENTO List_orders_8 AS

DECLARAR @fromdate fechahora

SELECCIONE @desdefecha = '20000101'

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

CREAR prueba de ÍNDICE EN Órdenes (ShipVia)

SELECCIONE * DESDE Pedidos DONDE OrderDate > @fromdate

Prueba DROP INDEX ON Órdenes

Vamos

EXEC Lista_pedidos_8

En este ejemplo, obtenemos un escaneo de índice agrupado para ambas declaraciones SELECT , a pesar de que el segundo
SELECT se vuelve a compilar durante la ejecución y el valor de @fromdate se conoce en este punto.

2.7 Recompilación de instrucciones y variables de tabla y parámetros con valores de tabla


Hasta ahora he hablado de parámetros y variables escalares. Pasemos ahora a las variables de la tabla, donde las cosas
funcionan de manera diferente y también hay una diferencia entre las diferentes versiones de SQL Server. Considere este
guión:
ALTER DATABASE Northwind SET COMPATIBILITY_lEVEL = 140

Vamos

CREAR PROCEDIMIENTO List_orders_9 AS

DECLARAR @ids TABLE (a int NOT NULL PRIMARY KEY)

INSERTAR @ids (a)

SELECCIONE OrderID FROM Pedidos

SELECCIONE CONTEO(*)

DESDE Pedidos O

DONDE EXISTE (SELECCIONE *

DE @ids yo

DONDE O.OrderID = ia)

CREAR prueba de ÍNDICE EN Órdenes (ShipVia)

SELECCIONE CONTEO(*)

DESDE Pedidos O

DONDE EXISTE (SELECCIONE *

DE @ids yo

DONDE O.OrderID = ia)

Prueba DROP INDEX ON Órdenes

Vamos

EXEC Lista_pedidos_9

Vamos

PROCEDIMIENTO DE ABANDONO List_orders_9

Tenga en cuenta que la primera declaración fallará si está en SQL 2016 o anterior. En tal caso, simplemente ignore el error,
pero no podrá ejecutar la segunda parte de este laboratorio.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 14/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Cuando ejecute esto, obtendrá un total de cuatro planes de ejecución. Los dos de interés son los planes segundo y cuarto
que provienen
Inglés de las dos 
consultas idénticas SELECT COUNT (*). He incluido las partes interesantes de los planes aquí:
Español

   

En el primer plan, el de la izquierda, SQL Server estima que hay una fila en la variable de la tabla (recuerde que cuando
dice "830 de 1", significa 830 reales y 1 estimado), y como consecuencia de eso estimación, el optimizador se conforma
con un operador de unión de bucles anidados junto con una búsqueda de índice agrupado en la tabla de pedidos . Esta es
una mala elección en este caso, ya que se devuelven todas las filas. Pero @ids es una variable local y SQL Server no tiene
conocimiento de cuántas filas hay en la tabla cuando el procedimiento se compila inicialmente. La creación de un índice
desencadena una recompilación del segundo SELECTdeclaración antes de que se ejecute, y en contraste con una variable
escalar local, SQL Server "olfatea" la cardinalidad de @ids , y puede ver en la captura de pantalla a la derecha que la
estimación ahora es 830, y esto conduce a una mejor opción de plan con una combinación de fusión .

Si está en SQL 2019, cambie 140 a 150 en la primera línea de arriba y vuelva a ejecutar. Lo que encontrará es que ahora la
primera ejecución también tiene una estimación correcta para @ids , y el plan es un Merge Join . Esto se debe a la mejora
en SQL 2019. Es un patrón común declarar una variable de tabla, completarla con muchas filas y luego usarla en una
consulta. Esto a menudo conduce a un bajo rendimiento, porque el plan está optimizado para una fila en la variable de la
tabla cuando hay muchas. Por esta razón, Microsoft introdujo la compilación diferida para sentencias que hacen referencia
a variables de tabla. Es decir, si el nivel de compatibilidad es 150, SQL Server no compila ningún plan de consulta para los
dos SELECTsentencias cuando se inicia el procedimiento, pero difiere esto hasta que la ejecución llega a esas sentencias.
El plan que se crea en este punto se coloca en caché y se reutiliza en ejecuciones posteriores.

Ambos comportamientos, el comportamiento original con la detección de la variable local en la recompilación de


declaraciones, y la compilación diferida introducida en SQL 2019 son generalmente beneficiarios. Sin embargo, existe el
riesgo de que pueda encontrarse con problemas similares a la detección de parámetros. Digamos que en la primera
ejecución hay 2000 filas en la variable de la tabla, pero en las ejecuciones posteriores solo hay de tres a cinco filas. Estas
últimas ejecuciones luego se ejecutarán con un plan optimizado para 2000 filas, que puede no ser el mejor para estas
ejecuciones más pequeñas.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 15/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Nota : es posible deshabilitar la compilación diferida de declaraciones con variables de tabla, ya sea para una consulta específica con
una sugerencia Español
Inglés de consulta 
o en el nivel de base de datos con una configuración de ámbito de base de datos. Observe que esta
configuración no se aplica a la detección de la cardinalidad de una variable de tabla cuando se vuelve a compilar una instrucción.

Finalmente, veamos los parámetros con valores de tabla . Para estos, nunca obtendrá ninguna suposición ciega de una fila,
pero SQL Server detectará la cardinalidad del parámetro cuando realice la compilación inicial del procedimiento. Aquí hay
un ejemplo:

CREAR TIPO temptype COMO TABLA (a int NOT NULL PRIMARY KEY)

Vamos

CREAR PROCEDIMIENTO List_orders_10 @ids temptype READONLY AS

SELECCIONE CONTEO(*)

DESDE Pedidos O

DONDE EXISTE (SELECCIONE *

DE @ids yo

DONDE O.OrderID = ia)

Vamos

DECLARAR @ids temptype

INSERTAR @ids (a)

SELECCIONE OrderID FROM Pedidos


EXEC List_orders_10 @ids

Vamos

DECLARAR @ids temptype

INSERTAR @ids (a) VALORES (11000)

EXEC List_orders_10 @ids

Vamos

PROCEDIMIENTO DE ABANDONO List_orders_10

TIPO DE GOTA tipo temporal

El plan de consulta para este procedimiento es el mismo que para la segunda consulta SELECT en List_orders_9 , es decir,
Merge Join + Clustered Index Scan of Orders , ya que SQL Server ve las 830 filas en @ids cuando se compila la
consulta. El plan de ejecución es el mismo para la segunda ejecución de List_orders_10 , aunque esta vez el plan de
ejecución no es óptimo. Sabemos por qué sucede esto: SQL Server reutiliza el plan de ejecución en caché.

2.8 La historia hasta ahora


En este capítulo, hemos visto cómo SQL Server compila un procedimiento almacenado y qué importancia tienen los
valores de los parámetros reales para la compilación. Hemos visto que SQL Server coloca el plan para el procedimiento en
caché, de modo que el plan pueda reutilizarse más tarde. También hemos visto que puede haber más de una entrada para el
mismo procedimiento almacenado en el caché. Hemos visto que hay una gran cantidad de claves de caché diferentes, por
lo que potencialmente puede haber muchos planes para un solo procedimiento almacenado. Pero también hemos aprendido
que muchas de las opciones SET que son claves de caché son opciones heredadas que nunca debe cambiar.

En la práctica, la opción SET más importante es ARITHABORT , porque el valor predeterminado de esta opción es diferente
en una aplicación y en SQL Server Management Studio. Esto explica por qué puede detectar una consulta lenta en su
aplicación y luego ejecutarla a buena velocidad en SSMS. La aplicación utiliza un plan que se compiló para un conjunto
diferente de valores de parámetros rastreados que los valores reales, mientras que cuando ejecuta la consulta en SSMS, es
probable que no haya ningún plan para ARITHABORT ON en la memoria caché, por lo que SQL Server generará un plan
que se ajuste a los valores de sus parámetros actuales.

También ha entendido que puede verificar que este sea el caso ejecutando este comando en su ventana de consulta:
DESACTIVAR ARITHABORT

y con gran probabilidad, ahora obtendrá el comportamiento lento de la aplicación también en SSMS. Si esto sucede, sabrá
que tiene un problema de rendimiento relacionado con el rastreo de parámetros. Lo que quizás aún no sepa es cómo
abordar este problema de rendimiento, y en los siguientes capítulos discutiré las posibles soluciones, antes de volver al
tema de la compilación, esta vez para consultas ad-hoc, también conocido como SQL dinámico.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 16/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Nota : Siempre hay estas divertidas variaciones. Una aplicación con la que trabajé durante muchos años emitió SET ARITHABORT
ON cuando Español
se conectó, por
Inglés 
lo que nunca deberíamos haber visto este comportamiento confuso en SSMS. Excepto que lo hicimos. Un
componente anterior de la aplicación también emitió el comando SET NO_BROWSETABLE ON en la conexión. Nunca he podido
comprender el impacto de este comando SET no documentado, pero creo recordar que está relacionado con las primeras versiones de
ADO "clásico". Y sí, esta configuración es una clave de caché.

3. No siempre se trata de rastrear parámetros...


Antes de profundizar en cómo abordar los problemas de rendimiento relacionados con la detección de parámetros, que es
un tema bastante amplio, primero me gustaría cubrir un par de casos en los que la detección de parámetros no está
involucrada, pero en los que, sin embargo, puede experimentar un rendimiento diferente en la aplicación y SSMS.

3.1 Sustitución de variables y parámetros


Ya he tocado esto, pero vale la pena extenderse un poco.

Ocasionalmente, veo personas en los foros que me dicen que su procedimiento almacenado es lento, pero cuando ejecutan
la misma consulta fuera del procedimiento, es rápido. Después de algunas publicaciones en el hilo, se revela la verdad: la
consulta con la que están luchando se refiere a variables, ya sean variables locales o parámetros. Para solucionar el
problema de la consulta por sí solo, han reemplazado las variables con constantes. Pero como hemos visto, la consulta
independiente resultante es bastante diferente, y SQL Server puede hacer estimaciones más precisas con constantes en
lugar de variables y, por lo tanto, llega a un mejor plan. Además, SQL Server no tiene que considerar que la constante
puede tener un valor diferente la próxima vez que se ejecute la consulta.

Un error similar es convertir los parámetros en variables. Di que tienes:

CREAR PROCEDIMIENTO some_sp @par1 int AS

...

-- Alguna consulta que se refiere a @par1

Desea solucionar esta consulta por su cuenta, así que lo hace:

DECLARAR @par1 int

SELECCIONE @par1 = 4711

-- consulta va aquí

Por lo que ha aprendido aquí, sabe que esto es muy diferente de cuando @par1 realmente es un parámetro. SQL Server no
tiene idea del valor de @par1 cuando lo declara como una variable local y hará suposiciones estándar.

Pero si tiene un procedimiento almacenado de 1000 líneas y una consulta es lenta, ¿cómo lo ejecuta de forma
independiente con gran fidelidad, de modo que tenga las mismas presunciones que en el procedimiento almacenado?

Una forma de abordar esto es incrustar la consulta en sp_executesql :

EXEC sp_executesql N'-- Alguna consulta que se refiere a @par1', N'@par1 int', 4711

Deberá duplicar las comillas simples en la consulta para poder ponerlo en un carácter literal. Si la consulta se refiere a
variables locales, debe asignarlas en el bloque de SQL dinámico y no pasarlas como parámetros para que tenga las mismas
presunciones que en el procedimiento almacenado.

Otra opción es crear un procedimiento ficticio con la declaración problemática; esto evita duplicar las comillas. Para evitar
la basura en la base de datos, puede crear un procedimiento almacenado temporal:

CREAR PROCEDIMIENTO #test @par1 int AS

-- consulta va aquí.

Al igual que con SQL dinámico, asegúrese de que las variables locales también se declaren localmente en su dummy.
Tendré que agregar la advertencia de que no he investigado si SQL Server tiene ajustes o limitaciones especiales al
optimizar los procedimientos almacenados temporales. No es que vea por qué debería haber alguno, pero me han quemado
antes...

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 17/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

3.2 Bloqueo
Inglés
No debe olvidar que una posible razón por la que el procedimiento se ralentizó en la aplicación fue simplemente una
Español

cuestión de bloqueo. Cuando probó la consulta tres horas más tarde en SSMS, el bloqueador había completado su trabajo.
Si encuentra que no importa cómo ejecute el procedimiento en SSMS, con o sin ARITHABORT , el procedimiento siempre
es rápido, el bloqueo comienza a parecer una explicación probable. La próxima vez que se alarme porque el procedimiento
es lento, debe comenzar su investigación con un análisis de bloqueo. Ese es un tema que está completamente fuera del
alcance de este artículo, pero para una buena herramienta para investigar el bloqueo, consulte mi beta_lockinfo .

3.3 Configuración de la base de datos


Digamos que ejecuta una consulta como esta en dos bases de datos diferentes:
SELECCIONE ...

DESDE dbo.sometable

ÚNETE a dbo.someothertable EN...

ÚNETE a dbo.yetanothertable EN...

DÓNDE ...

Encuentra que la consulta se realiza de manera muy diferente en las dos bases de datos. Puede haber muchas razones para
estas diferencias. Tal vez los tamaños de los datos sean completamente diferentes en una o más de las tablas, o los datos se
distribuyan de manera diferente. Puede ser que las estadísticas sean diferentes, incluso si los datos son idénticos. También
podría ser que una base de datos tenga un índice que la otra base de datos no tenga.

Digamos que la consulta en su lugar va:

SELECCIONE ...

DESDE thatdb.dbo.sometable

ÚNETE a thatdb.dbo.someothertable EN...

ÚNASE a thatdb.dbo.yetanothertable EN...

DÓNDE ...

Es decir, la consulta se refiere a las tablas en notación de tres partes. Y, sin embargo, descubre que la consulta se ejecuta
más rápido en SSMS que en la aplicación o viceversa. Pero cuando tiene la idea de cambiar la base de datos en SSMS para
que sea la misma que en la aplicación, también obtiene un rendimiento lento en SSMS. ¿Qué está pasando? Ciertamente,
no puede ser nada de lo que discutí anteriormente, ya que las tablas son las mismas, sin importar la base de datos desde la
que emita la consulta.

La respuesta es que puede ser un rastreo de parámetros, porque si observa el resultado de la consulta que introduje en la
sección Diferentes planes para diferentes configuraciones , verá que dbid es una de las claves de caché. Es decir, si
ejecuta la misma consulta en las mismas tablas de dos bases de datos diferentes, obtiene diferentes entradas de caché y, por
lo tanto, puede haber diferentes planes. Y, como hemos aprendido, una de las posibles razones de esto es el rastreo de
parámetros. Pero también podría ser que la configuración de las dos bases de datos sea diferente:

Por lo tanto, si esto te ocurre, ejecuta esta consulta:


SELECCIONE * DESDE sys.databases DONDE nombre EN ('slowdb', 'fastdb')

(Obviamente, debe reemplazar slowdb y fastdb con los nombres de las bases de datos reales desde las que ejecutó).
Compare las filas y tome nota de las diferencias. Lejos de todas las columnas que ves afectan la elección del plan. El más
importante, con diferencia, es el nivel de compatibilidad. Cuando Microsoft realiza mejoras en el optimizador en una
nueva versión de SQL Server, solo hacen que estas mejoras estén disponibles en el nivel de compatibilidad más reciente
porque, aunque están destinadas a ser mejoras, siempre habrá consultas que se verán afectadas negativamente por el
cambio y correr mucho más lento.

Si está en SQL 2016 o posterior, también debe buscar en sys.database_scoped_configurations y comparar la


configuración entre las dos bases de datos. (Esta vista no contiene la identificación de la base de datos o el nombre, pero
cada base de datos tiene su propia versión). Algunas de estas configuraciones afectan el trabajo del optimizador, lo que
podría conducir a diferentes planes.

Lo que haría una vez que haya identificado la diferencia depende de la situación. Pero, en general, recomiendo no cambiar
los valores predeterminados. (A partir de SQL 2017, sys.database_scoped_configurations tiene una columna

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 18/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

is_value_default que es 1, si la configuración actual es la configuración predeterminada).


Inglés plan lento proviene de una base de datos con un nivel de compatibilidad más bajo,
Español
Por ejemplo, si encuentra que el
considere cambiar el nivel de compatibilidad para esa base de datos. Pero si el plan lento ocurre con la base de datos con el
nivel de compatibilidad más alto, no debe cambiar la configuración, sino trabajar con la consulta y los índices disponibles
para ver qué se puede hacer. En mi experiencia, cuando una consulta retrocede al pasar a una versión más nueva del
optimizador, generalmente hay algo que es problemático, y solo tuvo suerte de que la consulta se ejecutara rápido en la
versión anterior.

Hay una excepción bastante obvia a la regla de apegarse a los valores predeterminados: si encuentra que en la base de
datos rápida, la configuración del ámbito de la base de datos QUERY_OPTIMIZER_HOTFIXES está establecida en 1, debe
considerar habilitar esta configuración para la otra base de datos también, como esto le dará acceso a las correcciones del
optimizador lanzadas después del lanzamiento original de la versión de SQL Server que está utilizando.

Solo para que quede claro: la configuración de la base de datos no solo se aplica a consultas con tablas en notación de tres
partes, sino que también puede explicar por qué la misma consulta o procedimiento almacenado con tablas en notación de
una o dos partes obtiene diferentes planes y rendimiento en bases de datos aparentemente similares.

3.4 Vistas indexadas y similares


Tiene una consulta en un procedimiento almacenado que es lento. Pero cuando coloca la consulta en un procedimiento
temporal como el que mencioné anteriormente, es rápido. Cuando compara los planes de consulta, descubre que la versión
rápida usa una vista indexada, un índice en una columna calculada o un índice filtrado, pero el procedimiento lento no.

Para que el optimizador considere cualquiera de estos tipos de índices, estas configuraciones deben estar ACTIVADAS :
QUOTED_IDENTIFIER , ANSI_NULLS , ANSI_WARNINGS , ANSI_PADDING y CONCAT_NULL_YIELDS_NULL . Además,
NUMERIC_ROUNDABORT debe estar APAGADO . De estas configuraciones, QUOTED_IDENTIFIER y ANSI_NULLS se
guardan con el procedimiento. Entonces, en el escenario que describí en el párrafo anterior, una causa probable es que el
procedimiento almacenado se creó con QUOTED_IDENTIFIER y/o ANSI_NULLS configurados en OFF. Puede investigar la
configuración almacenada para su procedimiento con esta consulta:

SELECCIONE objectpropertyex(object_id('your_sp'), 'IsQuotedIdentOn'),

objectpropertyex(object_id('su_sp'), 'IsAnsiNullsOn')

Si ve 0 en cualquiera de estas columnas, asegúrese de que el procedimiento se vuelve a cargar con la configuración
adecuada.

Hay dos razones relacionadas con las herramientas de SQL que pueden hacer que esto suceda. Primero, como noté
anteriormente, SQLCMD se conecta de forma predeterminada con SET QUOTED_IDENTIFIER OFF , por lo que si
implementa procedimientos con SQLCMD, esto puede suceder. ¡ Use la ‑Iopción para forzar QUOTED_IDENTIFIER ON !
La otra razón es algo que encontraría principalmente en una base de datos que comenzó su vida en SQL 2000 o anterior.
SQL 2000 venía con una herramienta Enterprise Manager que siempre emitía SET ANSI_NULLS OFF y SET
QUOTED_IDENTIFIER OFFantes de crear un procedimiento almacenado, y esto no era nada que pudiera configurar. Más
tarde, cuando la base de datos comenzó a mantenerse con SSMS, SSMS escribió fielmente estas configuraciones y, a
menos que alguien las cambiara manualmente, se mantuvieron a lo largo de los años.

Puede usar esta consulta para encontrar todos los procedimientos con configuraciones incorrectas en su base de datos:
SELECCIONA o.nombre

DESDE sys.sql_modules m

ÚNASE a sys.objects o ON m.object_id = o.object_id

DONDE (m.uses_quoted_identifier = 0 o

m.usos_ansi_nulls = 0)

Y o.escriba NOT IN ('R', 'D')

Normalmente, los problemas relacionados con las vistas indexadas y similares se deben a la configuración almacenada,
pero obviamente, si la aplicación se entrometiera con cualquiera de las otras cuatro opciones de SET , por ejemplo, enviar
SET ANSI_WARNINGS OFF al conectarse, esto también descalificaría este tipo de índices. de ser utilizado, y por lo tanto
ser motivo de lento en la aplicación, rápido en SSMS .

Finalmente, si tiene una base de datos con nivel de compatibilidad 80 (que no es compatible con SQL 2012 y versiones
posteriores), debe saber que hay una configuración más que debe estar ACTIVADA para que se consideren estos índices, y
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 19/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

esa es nuestra favorita, ARITHABORT . .


Inglés Español

3.5 Un problema con los servidores vinculados
Esta sección se refiere a un problema con los servidores vinculados que ocurre principalmente cuando el servidor remoto
es anterior a SQL 2012 SP1, pero también puede aparecer con versiones posteriores en algunas circunstancias.

Considere esta consulta:

SELECCIONE C.*

DESDE SQL_2008.Northwind.dbo.Orders O

UNIRSE Clientes C ON O.CustomerID = C.CustomerID

DONDE O.OrderID > 20000

Ejecuté esta consulta dos veces, inicié sesión como dos usuarios diferentes. El primer usuario es administrador del
sistema en ambos servidores, mientras que el segundo usuario es un usuario simple con solo permisos SELECT . Para
asegurarme de obtener diferentes entradas de caché, utilicé diferentes configuraciones para ARITHABORT .

Cuando ejecuté la consulta como sysadmin , obtuve este plan:

Cuando ejecuté la consulta como usuario simple, el plan era diferente:

¿Cómo es que los planes son diferentes? Ciertamente no es un rastreo de parámetros porque no hay parámetros. Como
siempre, cuando un plan de consulta tiene una forma u operador inesperado, es una buena idea mirar las estimaciones, y si
observa los números debajo de los operadores de consulta remota , puede ver que las estimaciones son diferentes. Cuando
ejecuté como administrador del sistema , la estimación fue de 1 fila, que es un número correcto, ya que no hay pedidos
en Northwinddonde el ID de pedido excede 20000. (Recuerde que el optimizador nunca asume cero filas de las
estadísticas). Pero cuando ejecuté la consulta como un usuario simple, la estimación fue de 249 filas. Reconocemos este
número en particular como el 30 % de 830 pedidos, o la estimación para una operación de desigualdad cuando el
optimizador no tiene información. Anteriormente, esto se debía a un valor de variable desconocido, pero en este caso no
hay ninguna variable que pueda ser desconocida. No, son las propias estadísticas las que faltan.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 20/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Siempre que una consulta acceda a las tablas del servidor local únicamente, el optimizador siempre podrá acceder a las
estadísticas de todas las
Inglés 
tablas de la consulta. Esto sucede internamente en SQL Server y no hay controles adicionales para
Español
ver si el usuario tiene permiso para ver las estadísticas. Pero esto es diferente con tablas en servidores vinculados. Cuando
SQL Server accede a un servidor vinculado, el optimizador necesita recuperar las estadísticas en la misma conexión que se
usa para recuperar los datos, y necesita usar comandos T-SQL, por lo que los permisos entran en juego. Y, a menos que se
haya configurado la asignación de inicio de sesión, esos son los permisos del usuario que ejecuta la consulta.

Al usar Profiler o Extended Events, puede ver que el optimizador recupera las estadísticas en dos pasos. Primero llama al
procedimiento sp_table_statistics2_rowset que devuelve información sobre las estadísticas de columna que hay, así como
la información de cardinalidad y densidad de las columnas. En el segundo paso, ejecuta DBCC SHOW_STATISTICS para
obtener las estadísticas de distribución completas. (Veremos más de cerca este comando más adelante en este artículo).

Nota : para ser exactos, el optimizador no se comunica en absoluto con el servidor vinculado, pero interactúa con el proveedor OLE
DB para la fuente de datos remota y solicita al proveedor que devuelva la información que necesita el optimizador.

Para que sp_table_statistics2_rowset se ejecute correctamente, el usuario debe tener el permiso VER DEFINICIÓN en la
tabla. Este permiso está implícito si el usuario tiene el permiso SELECT en la tabla (sin el cual el usuario no podría ejecutar
la consulta). Por lo tanto, a menos que al usuario se le haya negado explícitamente VER DEFINICIÓN , este procedimiento
no es una gran preocupación.

DBCC SHOW_STATISTICS es un asunto diferente. Durante mucho tiempo, la ejecución de este comando requería la
pertenencia a la función de servidor sysadmin o a una de las funciones de base de datos db_owner o db_ddladmin . Esto
se cambió en SQL 2012 SP1, por lo que, a partir de esta versión, solo se necesita el permiso SELECCIONAR .

Y es por eso que obtuve resultados diferentes. Como puede ver por el nombre del servidor, mi servidor vinculado era una
instancia que ejecutaba SQL 2008. Por lo tanto, cuando me conecté como un usuario que era administrador de sistemas en
la instancia remota, obtuve las estadísticas de distribución completas que indicaban que no hay filas con ID de pedido >
20000 y la estimación era una fila. Pero cuando se ejecuta como usuario simple, DBCC SHOW_STATISTICS falló con un
error de permiso. Este error no se propagó, sino que el optimizador aceptó que no había estadísticas y utilizó suposiciones
predeterminadas. Dado que obtuvo información de cardinalidad de sp_table_statistics2_rowset , supo que la tabla remota
tiene 830 filas, de ahí la estimación de 249 filas.

Por lo que dije anteriormente, esto no debería ser un problema si el servidor vinculado ejecuta SQL 2012 SP1 o posterior,
pero aún puede haber obstáculos. Si el usuario no tiene permiso SELECCIONAR en todas las columnas de la tabla, es
posible que haya estadísticas que el optimizador no pueda recuperar. Además, según Books Online, hay un indicador de
seguimiento (9485) que permite que el DBA evite que el permiso SELECT sea suficiente para ejecutar DBCC
SHOW_STATISTICS . Y lo que es más importante, si se configuró la seguridad de nivel de fila (una función agregada en
SQL 2016) para la tabla, solo tener el permiso SELECCIONAR no es suficiente, ya que esto podría permitir a los usuarios
ver datos a los que no deberían tener acceso. Es decir, para ejecutar DBCC SHOW_STATISTICSen una tabla con filtrado de
nivel de fila habilitado, necesita ser miembro de sysadmin , db_owner o db_ddladmin . (Lo suficientemente interesante,
uno esperaría que se aplicara lo mismo si la tabla tiene habilitado el Enmascaramiento dinámico de datos, pero ese parece
ser el caso).

Por lo tanto, cuando encuentre un problema de rendimiento donde una consulta que accede a un servidor vinculado es
lenta en la aplicación, pero se ejecuta rápido cuando la prueba desde SSMS (donde presumiblemente está conectado como
un usuario avanzado), siempre debe investigar los permisos. en la base de datos remota. (Tenga en cuenta que el acceso al
servidor vinculado puede no estar abierto en la consulta, pero podría estar oculto en una vista).

Si determina que los permisos en la base de datos remota son el problema, ¿qué acciones podría tomar? Otorgar a los
usuarios más permisos en el servidor remoto es, por supuesto, una salida fácil, pero absolutamente no recomendable desde
una perspectiva de seguridad. Otra alternativa es configurar el mapeo de inicio de sesión para que los usuarios inicien
sesión en el servidor remoto con un usuario proxy con poderes suficientes. Una vez más, esto es muy cuestionable desde
una perspectiva de seguridad.

Más bien, necesitaría modificar la consulta. Por ejemplo, puede reescribir la consulta con OPENQUERY para forzar la
evaluación en el servidor remoto. Esto puede ser especialmente útil si la consulta incluye varias tablas remotas, ya que
para la consulta que se ejecuta en el servidor remoto, el optimizador remoto tiene acceso total a las estadísticas de ese
servidor. (Pero también puede ser contraproducente, porque el optimizador local ahora obtiene aún menos información

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 21/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

estadística del servidor remoto). También puede usar la batería completa de sugerencias y guías de planes para obtener el
plan queInglés
desea. Español

También recomendaría que se pregunte a sí mismo (y a las personas que lo rodean): ¿es necesario ese acceso al servidor
vinculado? ¿Tal vez las bases de datos podrían estar en el mismo servidor? ¿Se pueden replicar los datos? ¿Alguna otra
solución? Personalmente, los servidores vinculados es algo que trato de evitar tanto como sea posible. Los servidores
vinculados a menudo significan molestias, en mi experiencia.

Antes de cerrar esta sección, me gusta repetir: lo que importa son los permisos en el servidor remoto, no el servidor local
donde se emite la consulta. También me gustaría señalar que he hecho la vista gorda a lo que puede suceder con otras
fuentes de datos remotas como Oracle, MySQL o Access, ya que tienen diferentes sistemas de permisos que desconozco
por completo. Puede o no ver problemas similares al ejecutar consultas en dichos servidores vinculados.

3.6 ¿Podría ser MARTE?


Recibí este correo de un lector:

Recientemente encontramos una consulta de selección de SQL Server 2016 contra CCI que era
lenta en la aplicación y rápida en SSMS. El tiempo de ejecución de la aplicación fue de 3 a 4 veces
mayor que el de SSMS. Mirando un poco más a fondo, se vio que la proporción del tiempo de CPU
era similar. Esto era cierto ya sea que la consulta se ejecutara en paralelo como de costumbre o en
serie al forzar MAXDOP 1. Una faceta inusual fue que Query Store contabilizaba las ejecuciones en
la misma fila en sys.query_store_query y sys.query_store_plan, ya sea desde la aplicación o desde
SSMS. 

La aplicación estaba usando una cadena de conexión que habilitaba MARS, aunque no era
necesario. Al eliminar esa cláusula de la cadena de conexión, SSMS y la aplicación experimentaron
el mismo tiempo de CPU y el mismo tiempo transcurrido.

Seguiremos buscando para determinar la mecánica de la diferencia. Sospecho que puede deberse
a una diferencia en el comportamiento de bloqueo; tal vez MARS deshabilite la escalada de
bloqueo.

Nota : MARS = Múltiples conjuntos de resultados activos. Si establece esta propiedad en una cadena de conexión, puede ejecutar
varias consultas en la misma conexión de forma intercalada. Su objetivo principal es permitirle enviar declaraciones de
ACTUALIZACIÓN mientras itera a través de un conjunto de resultados.

Las observaciones de Query Store me dejan bastante claro que el plan de ejecución fue el mismo en ambos casos. Por lo
tanto, no puede ser una cuestión de sniffing de parámetros, diferentes opciones de SET o similares. Bastante interesante,
algún tiempo después de haber agregado esta sección al artículo, recibí un correo electrónico de un segundo lector que
había experimentado lo mismo. Es decir, el acceso a un índice de almacén de columnas agrupado fue significativamente
más lento con MARS habilitado.

Esto me mantuvo desconcertado por un tiempo, y había muy poca información para que pudiera hacer un intento de
reproducir el problema. Pero luego recibí un tercer correo sobre este tema, y ​Nick Smith tuvo la amabilidad de
proporcionar una consulta de ejemplo simple:

SELECCIONE TOP 10000 SiteId DE MyTable

Es decir, se trata simplemente de una consulta que devuelve muchas filas. Tenga en cuenta que en este caso no había un
índice de almacén de columnas involucrado, sino solo una tabla normal. Creé un pequeño programa C# sobre este tema y
medí el tiempo de ejecución con y sin MARS. Al principio, no pude discernir ninguna diferencia en absoluto, pero estaba
compitiendo contra mi instancia local. Una vez que apunté mi base de datos en Windows Azure, fue una diferencia de un
factor diez. Cuando me conecté a un servidor al otro lado de la ciudad, la diferencia era un factor de tres o cuatro.

Me parece bastante claro que es un tema de latencia de red. Supongo que la naturaleza intercalada de MARS introduce
conversaciones en el cable, por lo que cuanto más lenta y larga sea la conexión de red, más te afectará la conversación.

Por lo tanto, si encuentra que una consulta que devuelve una gran cantidad de datos se ejecuta lentamente en la aplicación
y mucho más rápido en SSMS, hay motivos para ver si la aplicación especifica MultipleActiveResultSets=trueen la

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 22/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

cadena de conexión. Si lo hace, pregúntate si lo necesitas, y si no, sácalo.


Inglés

Español
¿Puede MARS hacer que una aplicación sea más lenta por alguna otra razón? No veo ninguna razón para creerlo, pero
sigue siendo un poco revelador que mis dos primeros corresponsales mencionaran índices de almacén de columnas.
Además, la latencia de la red realmente no explica la diferencia en la CPU mencionada en la cita anterior. Por lo tanto, si
se encuentra con una situación en la que concluye que MARS ralentiza las cosas y que no hay latencia en la red, me
interesaría saber de usted.

3.7 El efecto de las transacciones


Esto es algo con lo que se topó Daniel López Atán. Estaba investigando un procedimiento que se ejecutaba lento desde la
aplicación, pero cuando lo probó en SSMS fue rápido. Intentó todos los trucos de este artículo, pero nada parecía encajar.
Pero luego notó que la aplicación ejecutaba el procedimiento dentro de una transacción. Así que intentó ajustar el
procedimiento en BEGIN y COMMIT TRANSACTION cuando lo llamó desde SSMS, y ahora también era lento desde
SSMS.

Esto no es nada que verá cada vez que haya una transacción involucrada, pero puede verlo con un código que ejecuta
bucles para actualizar los datos una fila a la vez (o para un cliente a la vez o lo que sea). Y puede cortar en ambos sentidos.
Es decir, puede encontrar que envuelve este tipo de procedimiento en una transacción, en realidad se ejecuta más rápido.
confuso, ¿eh? No te preocupes, hay un patrón.

Digamos que tiene un ciclo sin ninguna transacción donde inserta una fila a la vez. Dado que cada INSERCIÓN es su
propia transacción, SQL Server debe esperar hasta que la transacción se haya fortalecido, hasta que pueda continuar. Por
otro lado, si hay una transacción definida por el usuario alrededor de todo el asunto, SQL Server puede continuar
directamente, sabiendo que si algo falla antes de la confirmación, todo se revertirá. Por lo tanto, el bucle ahora se ejecuta
un poco más rápido.

Sin embargo, por razones que no he entendido completamente, si la transacción no confirmada se vuelve demasiado
grande, la sobrecarga de una escritura comienza a crecer y, finalmente, lleva mucho más tiempo insertar una sola fila que
si no hubiera ninguna transacción. y esto es lo que sucedió en el caso de Daniel. He visto esto yo mismo un par de veces.

Entonces, si su problema de rendimiento incluye bucles, definitivamente debe investigar si se están utilizando
transacciones. Si no lo son, debería considerar envolver el ciclo en una transacción, posiblemente con una confirmación
después de cada mil filas más o menos. Y viceversa, si se trata de una transacción larga y se ejecuta más rápido sin ella,
elimine la transacción, o incluso mejor, consérvela, pero con confirmación después de cada mil filas. Tenga en cuenta que,
en cualquier caso, debe comprender los requisitos comerciales. Pueden ordenar todo o nada, en cuyo caso debe
permanecer en una sola transacción. O pueden exigir que una sola fila incorrecta no deba dar lugar a la pérdida de otras
filas, lo que descarta el uso de una transacción.

4. Obtención de información para resolver problemas de rastreo de


parámetros
Hemos aprendido cómo puede suceder que tenga un procedimiento almacenado que se ejecuta lentamente en la aplicación
y, sin embargo, la misma llamada se ejecuta rápidamente cuando lo prueba en SQL Server Management Studio: debido a
las diferentes configuraciones de ARITHABORT , obtiene diferentes entradas de caché, y dado que SQL Server emplea la
detección de parámetros, puede obtener diferentes planes de ejecución.

Si bien el secreto detrás del misterio ahora se ha revelado, el problema principal aún permanece: ¿cómo aborda el
problema de rendimiento? Por lo que ha leído hasta aquí, ya conoce una solución rápida. Si nunca antes ha visto el
problema y/o la situación es urgente, siempre puede hacer lo siguiente:

EXEC sp_recompilar problema_sp

Como hemos visto, esto vaciará el procedimiento de la memoria caché del plan y, la próxima vez que se invoque, habrá un
nuevo plan de consulta. Y si el problema nunca regresa, considere el caso cerrado.

Pero si el problema sigue apareciendo, y desafortunadamente, este es el resultado más probable, debe realizar un análisis
más profundo y, en esta situación, debe asegurarse de obtener el plan lento antes de ejecutar sp_recompile , o en algún

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 23/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

otro alterar el procedimiento. Debe mantener ese plan lento, de modo que pueda examinarlo y, sobre todo, encontrar los
valores de los parámetros
Inglés

para los que se creó el mal plan. Este es el tema de este capítulo.
Español

Nota : si está en SQL 2016 o posterior y ha habilitado Query Store para su base de datos, puede encontrar toda la información sobre los
planes en las vistas de Query Store, incluso después de que los planes se hayan vaciado. En el último capítulo , presento las versiones
del Almacén de consultas de las consultas que aparecen en este capítulo.

Antes de seguir, una pequeña observación: arriba te recomendé que cambiaras tus preferencias en SSMS, para que por
defecto te conectes con ARITHABORT OFF para evitar este tipo de confusiones. Pero en realidad hay una pequeña
desventaja al tener la misma configuración que la aplicación: es posible que no observe que el problema de rendimiento
está relacionado con la detección de parámetros. Pero si al investigar un problema de rendimiento tiene el hábito de
ejecutar el procedimiento de su problema con ARITHABORT tanto ON como OFF , puede concluir fácilmente si se trata de
un rastreo de parámetros.

4.1 Obtener los hechos necesarios


Toda solución de problemas de rendimiento requiere hechos. Si no tienes datos, estarás en la situación que tan bien
describe Bryan Ferry en la canción Sea Breezes del primer álbum de Roxy Music:

Hemos estado dando vueltas en nuestro estado actual

Esperando que la ayuda venga de arriba

Pero incluso los ángeles allí cometen los mismos errores

Si no tienes hechos, ni siquiera los ángeles podrán ayudarte. Los datos básicos que necesita para solucionar los problemas
de rendimiento relacionados con el rastreo de parámetros son:

1. ¿Cuál es la declaración lenta?


2. ¿Cuáles son los diferentes planes de consulta?
3. ¿Qué valores de parámetros detectó SQL Server?
4. ¿Cuáles son las definiciones de tabla e índice?
5. ¿Cómo se ven las estadísticas de distribución? ¿Está actualizado?

Casi todos estos puntos se aplican a cualquier esfuerzo de optimización de consultas. Solo el tercer punto es exclusivo de
los problemas de detección de parámetros. Eso, y el plural en el segundo punto: quieres mirar dos planes, el plan bueno y
el plan malo. En las siguientes secciones, veremos estos puntos uno por uno.

4.2 ¿Cuál es la Declaración Lenta?


Lo primero en la lista es encontrar la declaración lenta; en la mayoría de los casos, el problema radica en una sola
declaración. Si el procedimiento tiene solo una declaración, esto es trivial. De lo contrario, puede usar Profiler para
averiguarlo; la columna Duración se lo dirá. Simplemente rastree el procedimiento desde la aplicación o ejecute el
procedimiento desde Management Studio (¡con ARITHABORT DESACTIVADO !) y filtre su propio spid.

Otra opción más es utilizar el procedimiento almacenado sp_sqltrace , escrito por Lee Tudor y que me complace alojar en
mi sitio web. sp_sqltrace toma un lote de SQL como parámetro, inicia un seguimiento del lado del servidor, ejecuta el
lote, detiene el seguimiento y luego resume el resultado. Hay una serie de parámetros de entrada para controlar el
procedimiento, por ejemplo, cómo ordenar la salida. Este procedimiento es particularmente útil para determinar la
declaración lenta en un ciclo, si tuviera tal en su procedimiento almacenado, ya que obtiene los totales agregados por
declaración.

4.3 Obtener los planes y parámetros de consulta con Management Studio


En muchos casos, puede encontrar fácilmente los planes de consulta ejecutando el procedimiento en Management Studio,
después de habilitar primero Incluir plan de ejecución real (lo encuentra en el menú Consulta). Esto funciona bien,
siempre y cuando el procedimiento no incluya una multitud de consultas, en cuyo caso la pestaña del Plan de Ejecución
queda demasiado desordenada para trabajar con ella. Veremos estrategias alternativas en las próximas secciones.

Por lo general, ejecutaría el procedimiento de la siguiente manera:

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 24/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
ACTIVAR ARITHABORT

Vamos

Inglés Español
EXEC that_very_sp 4711, 123, 1

Vamos

DESACTIVAR ARITHABORT

Vamos

EXEC that_very_sp 4711, 123, 1

La suposición aquí es que la aplicación se ejecuta con las opciones predeterminadas, en cuyo caso la primera llamada dará
el plan bueno, porque el plan se huele para los parámetros que proporciona, y la segunda llamada se ejecutará con el plan
malo que ya está en la caché del plan. Para determinar las claves de caché en Sway, puede usar la consulta en la sección
Diferentes planes para diferentes configuraciones para ver los valores de clave de caché para los planes en el caché. (Si ya
probó el procedimiento en Management Studio, es posible que tenga dos entradas. La columna conteo_ejecución en
sys.dm_exec_query_stats puede ayudarlo a diferenciar las entradas entre sí; la que tiene el conteo bajo probablemente sea
su intento de SSMS).

Una vez que tenga los planes, puede encontrar fácilmente los valores de los parámetros olfateados. Haga clic con el botón
derecho en el operador que se encuentra más a la izquierda en el plan, el que dice SELECCIONAR , INSERTAR , etc.,
seleccione Propiedades , que abrirá un panel a la derecha. (Esa es la posición predeterminada; el panel es desmontable).
Aquí hay un ejemplo de cómo puede verse:

El primer valor compilado del parámetro es el valor olfateado que le está causando problemas de una forma u otra. Si
conoce su aplicación y su patrón de uso, puede obtener una revelación inmediata cuando vea el valor. Tal vez no, pero al
menos ahora sabe que hay una situación en la que la aplicación llama al procedimiento con este valor posiblemente
extraño. Tenga en cuenta también que puede ver la configuración de algunas de las opciones SET que son claves de caché.

Cuando observa un plan de consulta, no siempre es evidente qué parte del plan es realmente costosa. Pero el grosor de las
flechas es una buena pista. Cuanto más gruesa sea la flecha, más filas pasarán al siguiente operador. Y si está viendo un
plan de ejecución real, el grosor se basa en el número real de filas. El plan gráfico también ofrece información útil para
cada operador de consulta. Como ejemplo, tome este operador de bucles anidados :

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 25/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

Justo debajo del nombre del operador, verá Costo , que se supone que transmite cuánto del costo de la consulta total
contribuye este operador. Mi recomendación es que dejes pasar este valor. El costo se basa únicamente en las estimaciones
y no tiene relación con el costo real. Y mucho menos si las estimaciones son inexactas, y esto no es raro cuando tiene un
problema de rendimiento.

Debajo del costo, se ve el tiempo de ejecución. Esto ciertamente es un número relevante. Sin embargo, debe tener en
cuenta que el tiempo de ejecución de un operador incluye el tiempo de ejecución de los operadores a la derecha que le
envían datos. Por lo tanto, si ve una gran diferencia entre un operador y los operadores a la derecha o si, puede estar en lo
cierto.

Nota : los tiempos de ejecución por operador están disponibles desde SQL 2014 y versiones posteriores, por lo que si tiene SQL 2012
o anterior, no verá esta parte.

Vimos los números debajo del tiempo de ejecución en un capítulo anterior, pero son importantes, por lo que vale la pena
repetirlos. Arriba está el número real de filas y debajo está el número estimado de filas. En la parte inferior, el valor real se
expresa como un porcentaje de la estimación. Cuando el porcentaje es alto, como 83000 % como en este ejemplo, hay una
gran estimación errónea que tiene toda la razón para investigar.

4.4 Obtener los planes y parámetros de consulta directamente desde la memoria caché del plan
No siempre es factible usar SSMS para obtener los planes de consulta y los valores de los parámetros analizados. La
consulta incorrecta puede durar más minutos de los que su paciencia puede aceptar, o el procedimiento incluye tantas
declaraciones que genera un lío en SSMS. No menos importante, esto puede ser un problema si el procedimiento incluye
un bucle que se ejecuta muchas veces.

Una opción para obtener el plan de consulta y los parámetros rastreados es recuperarlo directamente de la memoria caché
del plan. Esto es bastante conveniente con la ayuda de la consulta a continuación, pero existe una limitación obvia con este
método: solo obtiene las estimaciones. Faltan el número real de filas y el número real de ejecuciones, dos valores que son
muy importantes para entender por qué un plan es malo.

La consulta
Esta consulta devolverá las declaraciones, los valores de los parámetros olfateados y los planes de consulta para un
procedimiento almacenado:
DECLARAR @dbname nvarchar(256),

@procname nvarchar(256)

SELECCIONE @dbname = 'Northwind',

@procname = 'dbo.List_orders_11'

; CON datos basados ​


COMO (

SELECCIONE qs.statement_start_offset/2 COMO stmt_start,

qs.statement_end_offset/2 COMO stmt_end,

est.encrypted AS isencrypted, est.text AS sqltext,

epa.value AS set_options, qp.query_plan,

charindex('<Lista de parámetros>', qp.query_plan) + len('<Lista de parámetros>')

como paramstart,

charindex('</ParameterList>', qp.query_plan) como parámetro

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA sys.dm_exec_text_query_plan(qs.plan_handle,

qs.statement_start_offset,

qs.statement_end_offset) qp

APLICACIÓN CRUZADA sys.dm_exec_plan_attributes(qs.plan_handle) epa

DONDE est.objectid = object_id (@procname)

Y est.dbid = db_id(@dbname)

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 26/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
Y epa.attribute = 'set_options'

), siguiente_nivel AS (

Inglés Español

SELECCIONE stmt_start, set_options, query_plan,

CASO CUANDO está encriptado = 1 ENTONCES '-- ENCRIPTADO'

CUANDO stmt_start >= 0

ENTONCES subcadena(sqltext, stmt_start + 1,

CASO stmt_end

CUANDO 0 ENTONCES datalength(sqltext)

ELSE stmt_end - stmt_start + 1

FINAL)

TERMINAR COMO Declaración,

CASO CUANDO paramend > paramstart

ENTONCES CAST (subcadena (query_plan, paramstart,

paramend - paramstart) AS xml)

TERMINAR COMO parámetros


DESDE datos basados

SELECCIONE set_options COMO [SET], n.stmt_start AS Pos, n.Statement,

CR.c.value('@Column', 'nvarchar(128)') como parámetro,

CR.c.value('@ParameterCompiledValue', 'nvarchar(128)') AS [Valor rastreado],

CAST (query_plan AS xml) AS [Query plan]

DESDE siguiente_nivel n

APLICACIÓN CRUZADA n.params.nodes('ColumnReference') COMO CR(c)

ORDENAR POR n.set_options, n.stmt_start, Parámetro

Si nunca antes ha trabajado con estos DMV, agradezco si esto es principalmente un galimatías para usted. Para mantener el
enfoque en el tema principal de este artículo, no explicaré esta consulta ahora, pero volveré a ella un poco más adelante.
Lo único a lo que me gusta prestar atención aquí y ahora es que especifique la base de datos y el procedimiento con el que
desea trabajar al principio. Puede pensar que sería mejor que fuera un procedimiento almacenado, pero es muy probable
que desee agregar o eliminar columnas, según lo que esté buscando.

La salida
Para ver la consulta en acción, puede usar este lote de prueba (y, sí, los ejemplos se vuelven cada vez más complejos a
medida que avanzamos):
CREAR PROCEDIMIENTO List_orders_11 @fromdate datetime,

@custid nchar(5) AS

SELECCIONE @fromdate = dateadd(AÑO, 2, @fromdate)

SELECCIONE *

DESDE Pedidos

DONDE FechaPedido > @desdefecha

Y CustomerID = @custid

SI @custid = 'ALFKI' CREAR prueba de ÍNDICE EN Órdenes (ShipVia)

SELECCIONE *

DESDE Pedidos

DONDE IDCliente = @custid

Y fecha de pedido > @desde fecha


IF @custid = 'ALFKI' DROP INDEX test ON Pedidos

Vamos

ACTIVAR ARITHABORT

EXEC List_orders_11 '19980101', 'ALFKI'

Vamos

DESACTIVAR ARITHABORT

EXEC List_orders_11 '19970101', 'BERGS'

Cuando haya ejecutado este lote, puede ejecutar la consulta anterior. Cuando hago esto, veo este resultado en SSMS (he
dividido la captura de pantalla en dos imágenes para mantener un ancho de página decente):

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 27/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

Estas son las columnas:

SET : el atributo set_options para el plan. Como mencioné anteriormente, esta es una máscara de bits. En esta imagen, se
ven los dos valores más probables. 251 es la configuración predeterminada y 4347 es la configuración predeterminada +
ARITHABORT ON . Si ve otros valores, puede usar las opciones de función para traducir la máscara de bits.

Pos : esta es la posición de la consulta en el procedimiento, contada en caracteres desde el inicio del lote que creó el
procedimiento, incluidos los comentarios que preceden a CREATE PROCEDURE . No es muy útil en sí mismo, pero sirve
para clasificar las sentencias en el orden en que aparecen en el procedimiento.

Sentencia : la sentencia SQL. Tenga en cuenta que las declaraciones se repiten una vez para cada parámetro de la consulta.

Parámetro : el nombre del parámetro. Solo se enumeran los parámetros que aparecen en esta declaración. Como
consecuencia de esto, las declaraciones que no hacen referencia a ningún parámetro no se incluyen en la salida.

Valor detectado : el valor en tiempo de compilación del parámetro, es decir, el valor que el optimizador detectó cuando
creó el plan. A diferencia del panel Propiedades del plan, aquí no verá ningún valor de parámetro real. Como mencioné
anteriormente, el valor rastreado para un parámetro puede ser diferente para diferentes declaraciones en el procedimiento,
y puede ver un ejemplo de esto en la imagen de arriba.

Plan de consulta: el plan de consulta. Puede hacer doble clic en el documento XML para ver el plan gráfico directamente.
Como señalé anteriormente, este es solo el plan estimado. No puede obtener ningún valor real del caché con esta consulta.

La consulta explicada
Esta consulta se refiere a algunos DMV con los que no todos los lectores pueden estar familiarizados. También utiliza
algunas técnicas de consulta con las que quizás no esté muy familiarizado, por ejemplo, XQuery. Ocuparía demasiado
espacio y lo distraería del tema principal para sumergirse en la consulta en su totalidad, por lo que solo lo explicaré
brevemente. Si la consulta y la explicación se te pasan por la cabeza, no te sientas mal por ello. Siempre que comprenda el
resultado, aún puede utilizar la consulta.

La consulta utiliza dos CTE (Common Table Expression). El primer CTE, basedata , incluye todos los accesos a los
DMV. Ya los hemos visto todos menos sys.dm_exec_text_query_plan . Hay dos columnas más que recuperamos de
sys.dm_exec_query_stats , a saber, statement_start_offset y statement_end_offset . Delimitan la declaración para esta
fila y los pasamos a sys.dm_exec_text_query_plan para obtener el plan solo para esta declaración. (Recuerde que el
procedimiento es una sola entrada de caché con un solo plan_handle ). sys.dm_exec_text_query_plan devuelve la
columna query_plan que, contrariamente a lo que puede esperar, esnvarchar(MAX) . La razón de esto es que el XML
para un plan de consulta puede estar tan profundamente anidado que no se puede representar con eltipo de datos xml
integrado de SQL Server. El CTE devuelve el plan de consulta como tal, pero también extrae las posiciones de la parte
del documento donde aparecen los valores de los parámetros.
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 28/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

En el siguiente CTE, next_level , sigo y uso los valores obtenidos en basedata . La expresión CASE extrae la declaración
del textoInglés
devuelto por Español

sys.dm_exec_sql_text . La forma de hacer esto es bastante torpe, sobre todo con esos nombres
largos de columna. Dado que hay pocas razones para modificar esa parte de la consulta, no digo más, pero lo remito a
Books Online. O simplemente créanme cuando digo que funciona. :-) La siguiente columna en el CTE, params , realiza la
extracción real de los valores de los parámetros del documento del plan de consulta y convierte ese elemento al tipo de
datos xml .

En el SELECT final , destruyo el documento de parámetros , de modo que obtengamos una fila por parámetro.
Ciertamente, se puede argumentar que es mejor tener todos los parámetros en una sola fila, ya que en este caso cada
declaración solo aparecerá una vez, y aquí hay una variación del SELECT final que usa más funcionalidad XML para
lograr la agregación de cadenas:

SELECCIONE set_options COMO [SET], n.stmt_start AS Pos, n.Statement,

(SELECCIONE CR.c.valor('@Columna', 'nvarchar(128)') + ' = ' +

CR.c.value('@ParameterCompiledValue', 'nvarchar(512)') + ' '

DESDE n.params.nodes('ColumnReference') COMO CR(c)

PARA RUTA XML(''), TIPO).valor('.', 'nvarchar(MAX)'),

CAST (query_plan AS xml) AS [Query plan]

DESDE siguiente_nivel n

ORDENAR POR n.set_options, n.stmt_start

Nota : en SQL 2017 o superior, esta consulta podría reescribirse usando la función string_agg , pero eso se deja como ejercicio para el
lector.

En el SELECT final , también convierto la columna del plan de consulta a XML, pero como se señaló anteriormente, esto
podría fallar debido a las limitaciones con el tipo de datos xml . Si obtiene un error de este tipo, simplemente comente esa
columna o cambie CAST a TRY_CAST si está en SQL 2012 o superior. ( TRY_CAST devuelve NULL si la conversión falla).

Además de las modificaciones que ya he mencionado, hay varias formas de modificar la consulta para recuperar
información que le parezca interesante. Por ejemplo, podría agregar más columnas de sys.dm_exec_query_stats o más
atributos del plan. Opté por incluir solo el atributo set_options , ya que esta es la clave de caché que es más probable que
varíe. Si desea incluir todas las declaraciones en el procedimiento, incluidas aquellas que no se refieren a ninguno de los
parámetros de entrada, simplemente cambie CROSS APPLY en la penúltima línea a OUTER APPLY .

4.5 Plan de consulta en vivo


Volviendo a Management Studio, hay una opción más para ver los planes de consulta y es un Plan de consulta en vivo . La
situación en la que usaría un plan de consulta en vivo es cuando la consulta no se completa en un tiempo razonable y desea
obtener más información de la que brinda el plan estimado. Con un plan de consulta en vivo, los valores reales del plan se
actualizan a medida que avanza la consulta. Puede ver tanto líneas continuas como discontinuas, donde las líneas
continuas representan partes del plan que se han completado. Al igual que en un plan real, verá cosas como 780 de 560 ,
donde el primer número es el número real de filas procesadas por el operador hasta el momento y el segundo número es la
estimación. También hay un porcentaje, a partir del cual puede detectar errores de estimación graves.

Hay dos formas de ver un plan de consulta en vivo en SSMS. Una es ejecutar una consulta desde una ventana de consulta
y habilitar Incluir estadísticas de consulta en vivo desde el menú Consulta . Esto requiere que esté conectado a SQL 2014
o posterior. SSMS debe ser la versión 16 o posterior; no está disponible en SSMS 2014.

Si no pensó en habilitar las estadísticas de consulta en vivo antes de iniciar la consulta, o si tiene la consulta ejecutándose
en la aplicación en este momento, una segunda opción es usar el Monitor de actividad (que se encuentra en el Explorador
de objetos, haciendo clic con el botón derecho en el nodo Servidor). ) Ábralo y busque el panel Active Expensive Queries .
Es probable que encuentre su consulta lenta aquí. Puede hacer clic con el botón derecho en la consulta y la opción Mostrar
plan de ejecución siempre está ahí. Eso le da el plan de ejecución estimado. Si la infraestructura de generación de perfiles
de ejecución ligera está habilitada, la opción Mostrar plan de ejecución en vivo también está disponible. Esta
infraestructura está disponible si alguno de estos es cierto:

Está en SQL 2019 o posterior y la opción de configuración del ámbito de la base de datos
LIGHTWEIGHT_QUERY_PROFILING está establecida en ON . (Cuál es el valor predeterminado.)
Está en SQL 2017 o SQL 2016 y el indicador de seguimiento 7412 está habilitado. (No es una mala idea tener este
indicador de seguimiento establecido como un parámetro de inicio para SQL Server).

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 29/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Tiene SQL 2017, SQL 2016 o SQL 2014 SP2 o posterior y hay una sesión de eventos extendidos activa que incluye el
evento .
query_thread_profile
Inglés Español

También hay un DMV en este espacio que vale la pena mencionar. sys.dm_exec_query_statistics_xml , introducido en
SQL 2016 SP1, le permite obtener el plan de ejecución para una consulta que se está ejecutando actualmente. Es una
función y acepta un spid como su único parámetro. Si la infraestructura de creación de perfiles de ejecución ligera está
activa, obtendrá los valores reales hasta el momento. De lo contrario, solo obtendrá el plan estimado.

4.6 Obtener el plan de ejecución real más reciente


A partir de SQL 2019, existe la posibilidad de obtener el plan de ejecución real más reciente para una consulta. Para poder
usar esta función, debe habilitar la opción de configuración de ámbito de base de datos LAST_QUERY_PLAN_STATS :
ALTERAR LA CONFIGURACIÓN DEL ALCANCE DE LA BASE DE DATOS ESTABLECER LAST_QUERY_PLAN_STATS = ON

El valor predeterminado para esta configuración es APAGADO . Cuando esta configuración está activada , SQL Server
guardará una copia del plan de ejecución real más reciente para una consulta, que puede recuperar con DMV
sys.dm_exec_query_plan_stats . Aquí hay una demostración:

EXEC List_orders_11 '19970101', 'BERGS'

Vamos

DECLARAR @dbname nvarchar(256),

@procname nvarchar(256)

SELECCIONE @dbname = 'Northwind',

@procname = 'dbo.List_orders_11'

SELECCIONE qp.query_plan

DESDE (SELECCIONE DISTINTO plan_handle, sql_handle DESDE sys.dm_exec_query_stats) qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA sys.dm_exec_query_plan_stats(qs.plan_handle) qp

DONDE est.objectid = object_id (@procname)

Y est.dbid = db_id(@dbname)

Si ejecuta este script y hace clic en el documento XML devuelto, puede ver que los gráficos incluyen los tiempos de
ejecución y el bit "real o estimado". También puede observar que ve el plan para todo el procedimiento almacenado. El
DMV acepta plan_handle como su único parámetro. Es decir, no puede proporcionar
declaración_inicio/final_desplazamiento para extraer el plan para una sola declaración, por lo que para un procedimiento
largo, la salida puede ser un poco difícil de manejar.

Supongo que hay un costo en la sobrecarga de rendimiento para habilitar LAST_QUERY_PLAN_STATS , pero no he
investigado qué tan grande puede ser esa sobrecarga.

4.7 Obtención de planes de consulta y parámetros de un seguimiento


Otra alternativa más para hacerse con los planes de consulta es ejecutar un seguimiento contra la aplicación o contra su
conexión en SSMS. Hay varios eventos de Showplan que puede incluir en un seguimiento. El más versátil es Showplan
XML Statistics Profile , que le brinda la misma información que ve en SSMS cuando habilita Incluir plan de ejecución
real .

Sin embargo, por varias razones, un rastro rara vez es una muy buena alternativa. Para empezar, habilitar la información
del plan de consulta en un seguimiento agrega mucha sobrecarga para generar esa información XML. Y observe que esto
se aplica incluso si limita su filtro a un solo spid. Por la forma en que funciona el motor de rastreo, todos los procesos aún
tienen que generar el evento, por lo que esto puede tener un impacto severo en un servidor ocupado.

A continuación, si ejecuta el seguimiento en Profiler, es probable que le resulte muy difícil configurar un buen filtro que
capture lo que desea ver sin generar mucho ruido. Una posibilidad, una vez que se ha completado el rastreo, es guardar el
rastreo en una tabla en la base de datos, lo que le permite encontrar la información interesante a través de consultas. (Pero
no le pida a Profiler que guarde en una tabla mientras se ejecuta el seguimiento. La sobrecarga de eso es terrible). El plan
está en la columna TextData . Transmítalo a xml y luego podrá verlo como lo describí anteriormente.

Una alternativa un poco mejor es usar el sp_sqltrace de Lee Tudor que mencioné anteriormente. Tiene un parámetro para
solicitar que se recopilen los planes de consulta, y puede optar por recopilar solo planes estimados o planes reales. El

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 30/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

rendimiento general en el servidor aún se ve afectado, pero al menos puede encontrar fácilmente el plan que está
buscando. Sin embargo,
Inglés

sp_sqltrace no funcionará para usted si desea ver una aplicación que utiliza múltiples spids.
Español

También puede usar Extended Events para obtener el plan de consulta capturando el evento
query_post_execution_showplan , pero no es mejor que Trace. Incluso si filtra su sesión de eventos para un spid
específico, SQL Server activa este evento para todos los procesos, por lo que el rendimiento general también se ve
afectado en este caso. (De hecho, realicé algunas pruebas rápidas e indicaron que el efecto de rendimiento es
considerablemente más grave para recopilar planes de ejecución reales con Eventos extendidos que con Seguimiento).

Nota : a partir de SQL 2016 SP2 CU3 y SQL 2017 CU11, hay un evento extendido query_plan_profile que no tiene este problema.
Sin embargo, este evento solo se activa si la consulta tiene la sugerencia QUERY_PLAN_PROFILE. Que, por supuesto, su declaración
lenta aleatoria en su aplicación no tendrá. Puede inyectarlo con una guía de planes, algo que discutiré más adelante en este texto. Pero
no hace falta decir que este es un enfoque terriblemente complicado, y yo mismo no lo he probado.

4.8 Obtener definiciones de tablas e índices


Dejemos atrás el tema de obtener información del plan de consulta y, en su lugar, veamos cómo encontrar información de
tablas e índices.

Supongo que ya está familiarizado con las formas de averiguar cómo se define una tabla, ya sea con sp_help o mediante
secuencias de comandos, por lo que paso directamente al tema de los índices. También se pueden programar o puede usar
sp_helpindex . Pero, en mi opinión, las secuencias de comandos son voluminosas y sp_helpindex no admite funciones
agregadas en SQL 2005 o posterior. Esta consulta puede ser útil:
DECLARAR @tbl nvarchar(265)

SELECCIONE @tbl = 'Pedidos'

SELECCIONE o.nombre, i.index_id, i.nombre, i.type_desc,

subcadena(ikey.cols, 3, len(ikey.cols)) COMO key_cols,

subcadena(inc.cols, 3, len(inc.cols)) COMO include_cols,

stats_date(o.object_id, i.index_id) COMO stats_date,

i.filter_definition

DESDE sys.objetos o

ÚNASE a sys.indexes i ON i.object_id = o.object_id

APLICACIÓN EXTERNA (SELECCIONE ', ' + c.name +

CASO ic.is_descending_key

CUANDO 1 ENTONCES 'DESC'

MÁS ''

FINAL

DESDE sys.index_columns ic

ÚNASE a sys.columns c ON ic.object_id = c.object_id

Y ic.column_id = c.column_id

DONDE ic.object_id = i.object_id

Y ic.index_id = i.index_id

Y ic.is_included_column = 0

ORDENAR POR ic.key_ordinal

PARA LA RUTA XML('')) COMO ikey(cols)

APLICACIÓN EXTERNA (SELECCIONE ', ' + c.name

DESDE sys.index_columns ic

ÚNASE a sys.columns c ON ic.object_id = c.object_id

Y ic.column_id = c.column_id

DONDE ic.object_id = i.object_id

Y ic.index_id = i.index_id

Y ic.is_included_column = 1

ORDENAR POR ic.index_column_id

FOR XML PATH('')) AS inc(columnas)

DONDE o.nombre = @tbl

ORDENAR POR o.name, i.index_id

Como se indica, la consulta no se ejecutará en SQL 2005, sino que simplemente eliminará la última columna,
filter_definition , del conjunto de resultados. Esta columna se aplica a los índices filtrados, una función agregada en SQL
2008. En cuanto a la columna stats_date , consulte la siguiente sección.

4.9 Búsqueda de información sobre estadísticas


Para ver todas las estadísticas de una tabla, puede utilizar esta consulta:
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 31/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
DECLARAR @tbl nvarchar(265)

SELECCIONE
Inglés @tbl =Español
'Pedidos'


SELECCIONE o.name, s.stats_id, s.name, s.auto_created, s.user_created,

subcadena(scols.cols, 3, len(scols.cols)) AS stat_cols,

stats_date(o.object_id, s.stats_id) COMO stats_date,

s.filter_definition

DESDE sys.objetos o

ÚNETE sys.stats s ON s.object_id = o.object_id

APLICACIÓN CRUZADA (SELECCIONE ', ' + c.nombre

DESDE sys.stats_columns sc

ÚNASE a sys.columns c ON sc.object_id = c.object_id

Y sc.column_id = c.column_id

DONDE sc.object_id = s.object_id

Y sc.stats_id = s.stats_id

ORDENAR POR sc.stats_column_id

FOR XML PATH('')) AS scols(cols)

DONDE o.nombre = @tbl

ORDENAR POR o.name, s.stats_id

Al igual que con la consulta de índices, la consulta no se ejecuta para SQL 2005 como se indica, sino que simplemente
elimina filter_definition del conjunto de resultados. auto_created se refiere a las estadísticas que SQL Server crea
automáticamente cuando tiene la ocasión, mientras que user_created se refiere a los índices creados explícitamente con
CREATE STATISTICS . Si ambos son 0, las estadísticas existen debido a un índice.

La columna stats_date devuelve cuándo se actualizaron las estadísticas más recientemente. Si la fecha se remonta al
pasado, es posible que las estadísticas no estén actualizadas. La causa raíz de los problemas relacionados con el rastreo de
parámetros suele ser algo más que estadísticas desactualizadas, pero siempre es una buena idea estar atento a esto. Una
cosa a tener en cuenta es que las estadísticas de las columnas con datos que aumentan de forma monótona, por ejemplo, las
columnas de id y fecha, rápidamente quedan obsoletas, porque las consultas suelen ser para los datos insertados más
recientemente, que siempre están más allá de la última ranura en el histograma ( más sobre histogramas más adelante).

Si cree que las estadísticas están desactualizadas para una tabla, puede usar este comando:
ACTUALIZAR ESTADÍSTICAS tbl

Esto le dará estadísticas de muestra. A menudo, esto le brinda estadísticas que son lo suficientemente buenas, pero a veces
el muestreo no funciona bien. En este caso, puede valer la pena forzar un análisis completo de los datos. Esto se puede
hacer mejor con este comando:
ACTUALIZAR ESTADÍSTICAS tbl CON FULLSCAN, ÍNDICE

Al agregar INDEX al comando, la actualización FULLSCAN solo se realiza para las estadísticas de los índices. Esto puede
reducir considerablemente el tiempo de ejecución de UPDATE STATISTICS , ya que para las estadísticas que no son de
índice, UPDATE STATISTICS escanea la tabla completa para cada estadística. (Mientras que para los índices, escanea el
nivel de la hoja del índice, que suele ser mucho más pequeño).

También puede actualizar las estadísticas de un solo índice. La sintaxis para esto no es lo que puede esperar:

ACTUALIZAR ESTADÍSTICAS tbl indexname CON FULLSCAN

Nota : no hay punto entre el nombre de la tabla y el nombre del índice, solo un espacio.

Tenga en cuenta que después de actualizar las estadísticas, es posible que vea una mejora inmediata en el rendimiento de la
aplicación. Esto no prueba necesariamente que las estadísticas obsoletas fueran el problema. Dado que las estadísticas
actualizadas provocan la recompilación, es posible que se vuelvan a rastrear los parámetros y, por este motivo, obtendrá un
mejor plan.

Para ver las estadísticas de distribución de un índice, use DBCC SHOW_STATISTICS . Este comando toma dos parámetros.
El primero es el nombre de la tabla, mientras que el segundo puede ser el nombre de un índice o estadística. Por ejemplo:
DBCC SHOW_STATISTICS (Pedidos, Fecha de pedido)

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 32/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Esto muestra tres conjuntos de resultados. No los cubriré todos aquí, solo diré que el último conjunto de resultados es el
histograma real para las
Inglés  El histograma refleja la distribución que ha registrado SQL Server sobre los datos de
estadísticas.
Español
la tabla. Así es como se ven las primeras líneas en el ejemplo:

Esto nos dice que según las estadísticas hay exactamente una fila con OrderDate = 1996-07-04 . Luego hay una fila en el
rango de 1996-07-05 a 1996-07-07 y dos filas con OrderDate = 1996-07-08. (Porque RANGE_ROWS es 1 y EQ_ROWS es
2). Luego, el histograma continúa y hay un total de 188 pasos para esta estadística en particular. Nunca hay más de 200
pasos en un histograma. Para obtener detalles completos sobre la salida, consulte el tema DBCC SHOW_STATISTICS en
Books Online. Uno de los libros blancos enumerados en la sección Lecturas adicionales tiene información más valiosa
sobre las estadísticas y el histograma.

Nota : en SQL 2005 hay un error, por lo que si hay un paso para valores NULL, DBCC SHOW_STATISTICS no muestra esta fila.
Este es un error de visualización y el valor aún está en el histograma.

Por lo general, no ejecuta DBCC SHOW_STATISTICS para que todas las estadísticas tengan la información por si acaso,
sino solo cuando cree que la información puede serle útil. Veremos un ejemplo de este tipo en el próximo capítulo.

Para las estadísticas creadas automáticamente, el nombre es algo así como _WA_Sys_00000003_42E1EEFE . Esto es
algo engorroso de usar, por lo que en este caso DBCC SHOW_STATISTICS le permite especificar el nombre de la columna
en su lugar. Tenga en cuenta que no puede usar el nombre de la columna si hay estadísticas creadas por el usuario en la
columna (a través de CREATE INDEX o CREATE STATISTICS ). El ejemplo anterior parece contradecir eso, ya que hay un
índice en la columna OrderDate . ¡ Pero ese índice también se llama OrderDate !

DBCC SHOW_STATISTICS es probablemente el más fácil de usar para la inspección manual, pero si desea filtrar la salida o
procesarla, también hay un DMV, sys.dm_db_stats_histogram , que se introdujo en SQL 2016 SP1 CU2. Aquí hay una
consulta de muestra:
SELECCIONE sh.*

DESDE sys.stats s

APLICACIÓN CRUZADA sys.dm_db_stats_histogram(s.object_id, s.stats_id) sh

DONDE s.object_id = object_id('dbo.Pedidos')

AND s.Name = 'OrderDate'

ORDENAR POR sh.range_high_key

Para este DMV, no tiene la opción de pasar nombres, pero debe pasar identificaciones. También hay un DMV adjunto,
sys.dm_db_stats_properties , que devuelve la información del primer conjunto de resultados de DBCC
SHOW_STATISTICS .

5. Ejemplos de cómo solucionar problemas de detección de parámetros


Es importante entender que la detección de parámetros en sí misma no es un problema; por el contrario, es una
característica, ya que sin ella SQL Server tendría que depender de suposiciones ciegas, lo que en la mayoría de los casos
llevaría a planes de consulta menos óptimos. Pero a veces, el rastreo de parámetros funciona en su contra. Podemos
identificar tres situaciones típicas:

1. El uso de consultas es tal que la detección de parámetros es completamente inapropiada. Es decir, un plan que es
bueno para cierta ejecución puede ser malo para la siguiente.
2. Hay un patrón específico en la aplicación en el que un grupo de llamadas es muy diferente del volumen principal. A
menudo, esta es una llamada que la aplicación realiza al iniciarse o al comienzo de un nuevo día.
3. La estructura del índice para una o más tablas es tal que no hay un índice perfecto para la consulta, pero hay varios
índices medio buenos y es aleatorio qué índice elige el optimizador.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 33/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Puede ser difícil decir de antemano cuál se aplica a su situación, y es por eso que necesita hacer un análisis cuidadoso. En
el apartado anterior te Español
Inglés qué información necesitas, aunque no siempre dejaba claro para qué. Además, hay una
comentaba
cosa más que no mencioné pero que es inmensamente útil: conocimiento íntimo sobre cómo funciona la aplicación y su
patrón de uso.

Dado que hay varias razones posibles por las que el rastreo de parámetros podría causarle un dolor de cabeza, esto
significa que no existe una solución única que pueda aplicar. Más bien, hay una gran cantidad de ellos, dependiendo de
dónde se encuentre la causa raíz. A continuación, daré algunos ejemplos de problemas relacionados con el rastreo de
parámetros y cómo abordarlos. Algunos son ejemplos de la vida real, otros son de naturaleza más genérica. Algunos de los
ejemplos se enfocan más en el análisis, otros toman una mirada directa a la solución.

5.1 Una no solución


Antes de entrar en las soluciones reales, primero permítanme señalar que agregar SET ARITHABORT ON a su
procedimiento no es una solución. Parecerá que funciona cuando lo pruebes. Pero eso es solo porque recreó el
procedimiento que forzó una nueva compilación y luego la siguiente invocación olfateó el conjunto actual de parámetros.
SET ARITHABORT ON es solo un placebo, y ni siquiera uno bueno. Lo más probable es que el problema vuelva. Ni
siquiera le ayudará a evitar la confusión con el rendimiento diferente en la aplicación y SSMS, porque la entrada de caché
general aún tendrá ARITHABORT OFF como su atributo de plan.

Por lo tanto, no ponga SET ARITHABORT ON en sus procedimientos almacenados. En general, le recomiendo
encarecidamente que no use ninguno de los comandos SET que son claves de caché en su código.

5.2 El mejor índice depende de la entrada


Considere este procedimiento:
CREAR PROCEDIMIENTO List_orders_12 @custid nchar(5),

@fromdate fechahora,

@todate fechahora AS

SELECCIONE *

DESDE Pedidos

DONDE IDCliente = @custid

Y OrderDate ENTRE @fromdate Y @todate

Hay un índice no agrupado en CustomerID y otro en OrderDate . Suponga que la actividad de pedidos entre los clientes
varía mucho. Muchos clientes hacen solo unos pocos pedidos al año. Pero algunos clientes son más activos, y algunos
tipos realmente grandes pueden hacer varios pedidos por día.

En la base de datos de Northwind , el cliente más activo es SAVEA con 31 pedidos, mientras que CENTC tiene solo un
pedido. Ejecute lo siguiente:

EXEC List_orders_12 'SAVEA', '19970811', '19970811'

Vamos

sp_recompilar List_orders_12

Vamos

EXEC List_orders_12 'CENTC', '19960101', '19961231'

Es decir, para SAVEA solo miramos los pedidos de un solo día, pero para CENTC miramos los pedidos de todo un año.
Como puede sentir, estas dos invocaciones funcionan mejor con índices diferentes. Aquí está el plan para la primera
invocación:

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 34/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Inglés Español

SQL Server aquí usa el índice para OrderDate que es el más selectivo. El plan para la segunda invocación es diferente:

Aquí CustomerID es la columna más selectiva y SQL Server usa el índice en CustomerID .

Una solución para abordar esto es forzar la recompilación cada vez con la sugerencia de consulta RECOMPILE :
CREAR PROCEDIMIENTO List_orders_12 @custid nchar(5),

@fromdate fechahora,

@todate fechahora AS

SELECCIONE *

DESDE Pedidos

DONDE IDCliente = @custid

Y OrderDate ENTRE @fromdate Y @todate

OPCIÓN (RECOMPILAR)

Con esta sugerencia, SQL Server compilará la consulta cada vez y dado que sabe que el plan no debe reutilizarse, maneja
los valores de los parámetros y las variables locales como constantes.

En muchos casos, forzar la recompilación cada vez está bien, pero hay algunas situaciones en las que no es así:

1. El procedimiento se llama con una frecuencia muy alta y la sobrecarga de compilación daña el sistema.
2. La consulta es muy compleja y el tiempo de compilación tiene un impacto negativo notable en el tiempo de respuesta.

Más concretamente, si bien forzar la recompilación es una solución que casi siempre es factible, no siempre es la mejor
solución. De hecho, el ejemplo que hemos visto en esta sección probablemente no sea muy típico para la situación Lento
en la aplicación, rápido en SSMS . Porque, si tiene un patrón de uso variable, se le alertará sobre el rendimiento variable
dentro de la propia aplicación. Así que hay toda la razón para seguir leyendo y ver si la situación que enfrenta encaja con
los otros ejemplos que presento.

5.3 Condiciones de búsqueda dinámica

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 35/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Es muy común tener formularios donde los usuarios pueden seleccionar entre una serie de condiciones de búsqueda. Por
ejemplo,Inglés
pueden seleccionar
Español
pedidos en una fecha determinada, por un cliente determinado, para un producto
ver
determinado, etc., incluidas combinaciones de parámetros. Dichos procedimientos a veces se implementan con cláusulas
WHERE que van:

DONDE (IDCliente = @custid O @custid ES NULO)

Y (FechaPedido = @fechaPedido O @fechaPedido ES NULO)

...

Como puede imaginar, la detección de parámetros no es beneficiosa para dichos procedimientos. No voy a ocupar mucho
espacio en este problema aquí, por dos razones: 1) Como ya he dicho, el problema con tales procedimientos generalmente
se manifiesta con un rendimiento variable dentro de la aplicación. 2) Tengo un artículo separado dedicado a este tema,
titulado Condiciones de búsqueda dinámica . Breve historia: OPTION (RECOMPILE) a menudo funciona muy bien aquí.

5.4 Revisión de indexación


Hace algún tiempo, uno de mis clientes me contactó porque uno de sus clientes experimentó un grave problema de
rendimiento con una función en su sistema. Mi cliente dijo que el mismo código funcionaba bien en otros sitios y que no
había habido ningún cambio recientemente en la aplicación. (Pero ya sabes, los clientes siempre dicen eso, al parecer).
Pudieron aislar el procedimiento problemático, que incluía una consulta que se parecía a esto:
SELECCIONE DISTINTO c.*

DESDE Table_C c

UNIRSE Table_B b ON c.Col1 = b.Col2

UNIRSE Table_A a ON a.Col4 = b.Col1

DONDE a.Col1 = @p1

Y a.Col2 = @p2

Y a.Col3 = @p3

Cuando se ejecutaba desde la aplicación, la consulta tardaba entre 10 y 15 minutos. Cuando ejecutaron el procedimiento
desde SSMS, descubrieron que el tiempo de respuesta era instantáneo. Fue entonces cuando me llamaron.

Me crearon una cuenta para poder iniciar sesión en el sitio en cuestión. Descubrí que las tres tablas tenían cierto tamaño, al
menos un millón de filas en cada una. Miré los índices de Table_A y descubrí que tenía unos 7-8 índices. De interés para
esta consulta fue:

Un índice Combo_ix no único y no agrupado en ( Col1 , Col2 , Col5 , Col4 ) y tal vez algunas columnas más.
Un índice Col2_ix no único y no agrupado en ( Col2 ).
Un índice Col3_ix no único y no agrupado en ( Col3 ).

Por lo tanto, no había ningún índice que cubriera todas las condiciones en la cláusula WHERE .

Cuando ejecuté el procedimiento en SSMS con la configuración predeterminada, el optimizador eligió el primer índice
para obtener datos de Table_A . Cuando cambié la configuración de ARITHABORT a APAGADO para que coincida con la
aplicación, vi un plan que usaba el índice en Col3 .

En este punto corrí

DBCC SHOW_STATISTICS (Tabla_A, Col3_ix)

La salida se parecía a algo como esto:

RANGO_HI_CLAVE RANGO_ROWS EQ_ROWS DISTINCT_RANGE_ROWS PROMEDIO_RANGE_ROWS


ALBARICOQUE 0 17 0 1
PLÁTANO 0 123462 0 1
ARÁNDANO 0 46 0 1
CEREZA 0 92541 0 1
HIGO 0 1351 0 1
KIWI 0 421121 0 1
LIMÓN 0 6543 0 1
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 36/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

LIMA 0 122131 0 1
MANGOInglés Español
0  95824 0 1
NARANJA 0 10410 0 1
PERA 0 46512 0 1
PIÑA 0 21102 0 1
CIRUELA 0 13 0 1
FRAMBUESA 0 95 0 1
RUIBARBO 0 7416 0 1
FRESA 0 24611 0 1

Es decir, solo había 17 valores distintos para esta columna y con una distribución muy desigual entre estos valores.
También confirmé este hecho ejecutando la consulta:
SELECCIONE Col3, CUENTA(*) DESDE Table_A GRUPO POR Col3 ORDEN POR Col3

Procedí a mirar el plan de consulta incorrecto para ver qué valor había olfateado el optimizador para @p3 . ¡Descubrí que
era - MANZANA, un valor que no está presente en la tabla en absoluto! Es decir, la primera vez que se ejecutó el
procedimiento, SQL Server estimó que la consulta devolvería una sola fila (recuerde que nunca estima cero filas) y que el
índice en Col3 sería el índice más eficiente para encontrar esa única fila. .

Ahora, puede preguntarse cómo puede ser tan desafortunado que el procedimiento se haya ejecutado por primera vez para
APPLE. ¿Fue solo mala suerte? Como no conozco este sistema, no puedo asegurarlo, pero al parecer esto ha sucedido más
de una vez. Tuve la impresión de que este procedimiento se ejecutó muchas veces como parte de una operación más
grande. Decir que la operación siempre comenzaría con APPLE. Recuerde que la reindexación de una tabla desencadena
la recompilación y es muy común ejecutar trabajos de mantenimiento por la noche para controlar la fragmentación. Es
decir, la primera llamada de la mañana puede ser muy decisiva para tu desempeño. (¿Por qué la operación comenzaría con
un valor que no está en la base de datos? Quién sabe, pero tal vez APPLE sea una condición inusual que deba manejarse
primero si existe. O tal vez es solo el alfabeto).

Para esta consulta en particular, hay una gran cantidad de posibles medidas para abordar el problema de rendimiento.

1. OPCIÓN (RECOMPILAR)
2. Agregue el índice "óptimo" en ( Col1 , Col2 , Col3 ) INCLUDE ( Col4 ).
3. Haga que el índice en Col3 se filtre o suéltelo por completo.
4. Use una sugerencia de índice para forzar el uso de cualquiera de los otros índices.
5. La sugerencia de consulta OPTIMIZE FOR .
6. Copie @p3 a una variable local.
7. Cambiar el comportamiento de la aplicación.

Ya hemos analizado cómo forzar la recompilación y, aunque lo más probable es que hubiera resuelto el problema, no es
probable que hubiera sido la mejor solución. A continuación, analizaré las otras opciones con más detalle.

Agregar un nuevo índice


Mi recomendación principal para el cliente fue la segunda en la lista: agregue un índice que coincida perfectamente con la
consulta. Es decir, un índice en el que todas las columnas de las cláusulas WHERE son claves de índice, con la columna
Col3 menos selectiva en último lugar entre las claves. Además de eso, agregue Col4 como una columna incluida, para que
la consulta se pueda resolver solo desde el nuevo índice, sin necesidad de acceder a las páginas de datos.

Sin embargo, agregar un nuevo índice no siempre es una buena solución. Si tiene una tabla a la que se accede de varias
maneras, puede que no sea factible agregar índices que coincidan con todas las condiciones WHERE y JOIN , y mucho
menos agregar índices de cobertura para todas y cada una de las consultas. En particular, esto se aplica a las tablas con una
gran tasa de actualización, como una tabla de pedidos . Cuantos más índices agregue a una tabla, mayor será el costo de
insertar, actualizar y eliminar filas en la tabla.

Cambiar / Soltar el índice en Col3

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 37/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

¿Qué tan útil es realmente el índice en Col3 ? Como no conozco este sistema, es difícil saberlo. Pero, en general, los
índices en columnas con
Inglés pequeño conjunto de valores no son muy selectivos y, por lo tanto, no son tan útiles.
solo un
Español
Entonces, una opción podría ser eliminar el índice en Col3 por completo y, por lo tanto, salvar al optimizador de esta
trampa. Tal vez el índice se agregó en algún momento por error, o se agregó sin comprender cómo evolucionaría la base de
datos con el tiempo. (Habiendo trabajado un poco más con este cliente, parece que agregan índices en todas las columnas
FK como una cuestión de rutina. Lo que puede o no ser una buena idea).

No se puede negar que hay que ser una persona muy valiente para dejar caer un índice por completo. Puede consultar
sys.dm_db_index_usage_stats para ver cuánto se usa el índice. Solo tenga en cuenta que este DMV se borra cuando se
reinicia SQL Server o la base de datos se desconecta.

Dado que la distribución en Col3 es tan desigual, no es improbable que haya consultas que busquen específicamente estos
valores raros. Tal vez FIG sea "Nuevas filas sin procesar". Tal vez FRAMBUESA signifique "errores". En este caso,
podría ser beneficioso convertir a Col3_ix en un índice filtrado , una función agregada en SQL 2008. Por ejemplo, la
definición del índice podría leer:
CREAR ÍNDICE col3_ix EN Table_A (col3)

DONDE col3 IN ('FIG', 'FRAMBUESAS', 'MANZANA', 'ALBARICOQUE')

Esto tiene dos beneficios:

1. El tamaño del índice se reduce en más del 99%.


2. El índice ya no es apto para la consulta del problema. Recuerde que SQL Server debe seleccionar un plan que sea
correcto para todos los valores de entrada, por lo que incluso si el valor del parámetro rastreado es APPLE, SQL
Server no puede usar el índice, porque el plan produciría un resultado incorrecto para KIWI.

Forzar un índice diferente


Si sabe que el índice en Col1 , Col2 siempre será el mejor índice, pero no desea agregar o eliminar ningún índice, puede
forzar el índice con:
SELECCIONE c.*

DESDE Table_C c

UNIRSE Table_B b ON c.Col1 = b.Ccol2

ÚNETE Table_A a CON (ÍNDICE = Combo_ix) EN a.Col4 = b.Col1

DONDE a.Col1 = @p1

Y a.Col2 = @p2

Y a.Col3 = @p3

Nota : en una versión anterior de este artículo, sugerí que puede decir WITH (INDEX (combo_ix, col2_ix))que le dé al optimizador la
opción entre dos índices. Sin embargo, eso era incorrecto. Puede forzar varios índices, pero en ese caso el optimizador se verá obligado
a utilizar ambos índices. Lo cual puede ser útil a veces, pero cuenta eso como "avanzado".

Las sugerencias de índice son muy populares, demasiado populares, se podría decir. Debería pensarlo dos veces antes de
agregar una sugerencia de índice, porque mañana su distribución de datos puede ser diferente y otro índice serviría mejor a
la consulta. Un segundo problema es que si luego decide reorganizar los índices, la consulta fallará con un error debido al
índice faltante.

OPTIMIZAR PARA
OPTIMIZE FOR es una sugerencia de consulta que le permite controlar la detección de parámetros. Para la consulta de
ejemplo, podría verse así:

SELECCIONE c.*

DESDE Table_C c

UNIRSE Table_B b EN c.col1 = b.col2

ÚNASE a Table_A a ON a.col4 = b.col1

DONDE a.col1 = @p1

Y a.col2 = @p2

Y a.col3 = @p3

OPCIÓN (OPTIMIZAR PARA (@p3 = 'KIWI'))

La sugerencia le dice a SQL Server que ignore el valor de entrada, pero que en su lugar compile la consulta como si el
valor de entrada de @p3 fuera KIWI, el valor más común en Col3 . Esto seguramente disuadirá a SQL Server de usar el
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 38/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

índice.
Inglés en SQL 2008 y versiones posteriores, es:
Español
Una segunda opción, disponible
OPCIÓN (OPTIMIZAR PARA (@p3 DESCONOCIDO))

En lugar de codificar cualquier valor en particular, podemos decirle a SQL Server que haga una suposición ciega para
eliminar por completo el rastreo de parámetros para @p3 .

Vale la pena agregar que puede usar OPTIMIZE FOR también con variables declaradas localmente, y no solo con
parámetros. Por desgracia, no puede usar OPTIMIZE FOR con variables de tabla o parámetros con valores de tabla.

Copie @p3 a una variable local


En lugar de usar @p3 directamente en la consulta, puede copiarlo en una variable local y luego usar la variable en la
consulta. Esto tiene el mismo efecto que OPTIMIZE FOR UNKNOWN . Y funciona en cualquier versión de SQL Server.

Cambiar la aplicación
Otra opción más es cambiar el procesamiento en la aplicación para que comience con uno de los valores más comunes.
Realmente no es una solución que recomiendo, porque crea una dependencia adicional entre la aplicación y la base de
datos. Existe el riesgo de que la solicitud se reescriba dos años después para adaptarse a los nuevos requisitos de que se
deben manejar las filas de MELOCOTÓN, y este manejo se agrega primero...

Por otra parte, puede haber una falla general en el proceso. ¿La aplicación solicita valores para una fruta a la vez, cuando
debería obtener valores para todas las frutas a la vez? Ayudé a este cliente con otra consulta. Tuvimos una sesión de ajuste
de consultas en una sala de conferencias y nunca pude obtener un rendimiento realmente bueno de la consulta con la que
estábamos trabajando. Pero hacia el final del día, el desarrollador responsable dijo que sabía cómo continuar, y su solución
fue rediseñar todo el proceso del que formaba parte la consulta problemática.

Resumiendo
Como ha visto en este ejemplo, hay muchas opciones para elegir, demasiadas, puede pensar. Pero el ajuste del rendimiento
a menudo requiere que tenga una bolsa de trucos bien llena, porque diferentes problemas requieren diferentes soluciones.

5.5 El caso de la caché de aplicaciones


En un sistema con el que trabajé durante muchos años, hay una especie de caché de aplicación que guarda los datos en una
base de datos de la memoria principal, llamémosla MemDb. Se utiliza para varios propósitos, pero el propósito principal
es servir como un caché desde el cual el servidor web del cliente puede recuperar datos en lugar de consultar la base de
datos. Por lo general, en la mañana hay una actualización total de los datos. Cuando se actualizan los datos en una tabla de
base de datos, hay un mecanismo de señalización que activa MemDb para ejecutar un procedimiento almacenado para
obtener el delta. Para encontrar lo que ha cambiado, MemDb se basa en columnas de marca de tiempo. (Una versión de
fila de marca de tiempo contiene un valor de 8 bytes que es exclusivo de la base de datos y que crece de forma monótona.
Se actualiza automáticamente cuando se inserta o actualiza la fila. Este tipo de datos también se conoce comorowversion
.) Un procedimiento almacenado típico para recuperar cambios se ve así:
CREAR PROCEDIMIENTO memdb_get_updated_customers @tstamp timestamp AS

SELECCIONE CustomerID, CustomerName, Dirección, ..., tstamp

DE Clientes

DONDE tstamp > @tstamp

Cuando MemDb llama a estos procedimientos, pasa el valor más alto en la columna tstamp para la tabla en cuestión de la
llamada anterior. Cuando se invoca el procedimiento para una actualización, la base de datos de la memoria principal pasa
0x como parámetro para obtener todas las filas.

Nota al margen : el esquema real es más complicado que el anterior; Lo he simplificado para centrarme en lo que es importante para
este artículo. Si está considerando algo similar, tenga en cuenta que, como se muestra, este ejemplo tiene muchos problemas con las
actualizaciones simultáneas, sobre todo si usa alguna forma de aislamiento de instantáneas. SQL 2008 introdujo Change Tracking, que
es una solución más sólida, especialmente diseñada para este propósito. También me gustaría agregar que MemDb no fue idea mía, ni
participé en su diseño.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 39/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

En una ocasión, cuando supervisé el rendimiento de un cliente que acababa de poner en marcha nuestro sistema, noté que
los procedimientos
Inglés de Español

MemDb tenían tiempos de ejecución bastante largos. Dado que se ejecutan muy a menudo para leer
deltas, tienen que ser muy rápidos. Después de todo, una idea con MemDb es eliminar la carga de la base de datos, no
agregarla.

Para que una consulta como la anterior sea rápida, debe haber un índice en tstamp , pero ¿se usará este índice? Por lo que
dije anteriormente, a primera hora de la mañana, MemDb se ejecutaría:
EXEC memdb_get_updated_customers 0x

Luego, un poco más tarde, se ejecutaría algo como:


EXEC memdb_get_updated_customers 0x000000000003E806

No es raro que durante la noche los planes de consulta se salgan de la memoria caché debido a los lotes nocturnos que
consumen mucha memoria. O hay un trabajo de mantenimiento para reconstruir índices que desencadena recompilaciones.
Por lo general, cuando se ejecuta la actualización de la mañana, no hay ningún plan en el caché y el valor rastreado es 0x.
Dado este valor, ¿el optimizador usará el índice en tstamp ? Sí, si es el índice agrupado. Pero dado que una columna de
marca de tiempo se actualiza cada vez que se actualiza una fila, no es una muy buena opción para el índice agrupado, y
todos nuestros índices en las columnas de marca de tiempo no están agrupados. (Y para tablas con una alta frecuencia
de actualización, también un índice no agrupado en una marca de tiempocolumna puede ser cuestionable.) Por lo tanto,
dado que el optimizador ve que el parámetro indica que se recuperarán todas las filas de la tabla, se conforma con una
exploración de la tabla. Este plan se coloca en caché y las llamadas posteriores también escanean la tabla, incluso si solo
buscan las filas más recientes. Y fue este pobre desempeño lo que vi.

Cuando te encuentras en una situación como esta, existen, al igual que en el ejemplo anterior, varias formas de despellejar
al gato. Pero antes de ver las posibilidades, debemos hacer una observación importante: no existe un plan de consulta que
sea bueno para ambos casos. Queremos usar el índice para leer los deltas, pero al hacer la actualización queremos el
escaneo. Por lo tanto, solo se deben aplicar soluciones que puedan generar planes diferentes para los dos casos.

OPCIÓN (RECOMPILAR)
Si bien esta solución cumple con el requisito, no es una buena solución. Esto significa que cada vez que recuperamos un
delta, hay una compilación. Y aunque el procedimiento anterior es simple, algunos de los procedimientos de MemDb son
decentemente complejos con un costo de compilación no trivial.

EJECUTAR CON RECOMPILAR


En lugar de solicitar la recompilación dentro del procedimiento, es mejor hacerlo en el caso especial: la actualización. La
actualización solo ocurre una vez al día normalmente. Y además, la actualización es bastante costosa en sí misma, por lo
que el costo de la recompilación es marginal en este caso. Es decir, cuando MemDb quiera hacer un refresco, debería
llamar al procedimiento de esta forma:
EJECUTAR memdb_get_updated_customers CON RECOMPILAR

Como señalé anteriormente, es probable que no haya un plan en el caché temprano en la mañana, por lo que puede
preguntar cuál es el punto. El punto es que cuando usas WITH RECOMPILE con EXECUTE , el plan no se coloca en caché.
Por lo tanto, la actualización puede ejecutarse con el escaneo, pero el plan en el caché se creará a partir de la primera
recuperación delta. (Y si el plan para leer el delta todavía está en el caché, ese plan permanecerá en el caché).

Para el problema particular de este sistema, esta fue la solución que ordené. Una ventaja adicional para nosotros fue que
había una única ruta de código en MemDb que debía cambiarse.

Sin embargo, hay un pequeño problema con esta solución. Normalmente, cuando llama a un procedimiento almacenado
desde un cliente, utiliza un tipo de comando especial en el que especifica solo el nombre del procedimiento. (Por ejemplo,
CommandType.StoredProcedure en ADO .NET). La API del cliente luego realiza una RPC (llamada a procedimiento
remoto) a SQL Server y nunca hay una instrucción EXECUTE como tal. Muy pocas API de clientes parecen exponer algún
método para especificar WITH RECOMPILE cuando llama a un procedimiento a través de RPC. La solución es enviar el
comando EXECUTE completo , usando CommandType.Text o similar, por ejemplo:
cmd.CommandType = CommandType.Text;

cmd.Text = "EJECUTAR memdb_get_updated_customers @tstamp CON RECOMPILAR";

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 40/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Nota: debe pasar los parámetros a través de la colección de parámetros o el mecanismo correspondiente en su API; no alinee los
valores en la cadena conEspañol
Inglés 
el comando EXEC, ya que esto podría abrir la puerta para la inyección de SQL .

Uso de un procedimiento de envoltorio


Si tiene una situación en la que se da cuenta de que EXECUTE WITH RECOMPILE es la mejor solución, pero no es factible
cambiar el cliente, puede introducir un procedimiento de contenedor. En este ejemplo, se cambiaría el nombre del
procedimiento original a memdb_get_updated_customers_inner y luego escribiría un contenedor que dice:
CREAR PROCEDIMIENTO memdb_get_updated_customers @tstamp timestamp AS

SI @tstamp = 0x

EJECUTAR memdb_get_updated_customers_inner @tstamp CON RECOMPILAR

MÁS

EJECUTAR memdb_get_updated_customers_inner @tstamp

En muchos casos, esta puede ser una solución simple y simple, especialmente si tiene una pequeña cantidad de
procedimientos de este tipo. (En este sistema hay muchos.)

Diferentes rutas de código


Otro enfoque sería tener diferentes rutas de código para los dos casos:
CREAR PROCEDIMIENTO memdb_get_updated_customers @tstamp timestamp AS

SI @tstamp = 0x

EMPEZAR

SELECCIONE CustomerID, CustomerName, Dirección, ..., tstamp

DE Clientes

FINAL

MÁS

EMPEZAR

SELECCIONE CustomerID, CustomerName, Dirección, ..., tstamp

DESDE Clientes CON (INDEX = timestamp_ix)

DONDE tstamp > @tstamp

FINAL

Tenga en cuenta que es importante forzar el índice en la rama ELSE o, de lo contrario, esta rama escaneará la tabla si la
primera llamada al procedimiento es para @tstamp = 0x debido a la detección de parámetros. Si su procedimiento es más
complejo e incluye uniones a otras tablas, no es probable que esta estrategia funcione, incluso si fuerza el índice en tstamp
. Las estimaciones de las uniones serían muy incorrectas y los planes de consulta aún estarían optimizados para obtener
todos los datos, y no el delta.

Diferentes Procedimientos
En la situación que teníamos, había un procedimiento que era particularmente difícil. Recuperó las transacciones de la
cuenta (este es un sistema para el comercio de acciones). La actualización no recuperaría todas las transacciones en la base
de datos, sino solo de los N días más recientes, donde N era un parámetro del sistema leído de la base de datos. En esta
base de datos, N era 20. El procedimiento no leyó de una sola tabla, pero había muchas otras tablas en la consulta. En
lugar de tener un parámetro de marca de tiempo , el parámetro era una identificación. Esto funciona ya que las
transacciones nunca se actualizan, por lo que MemDb solo necesita buscar nuevas transacciones.

Descubrí que en este caso EXECUTE WITH RECOMPILE por sí solo no salvaría el programa, porque había varios otros
problemas en el procedimiento. No encontré más remedio que tener dos procedimientos, uno para la actualización y otro
para el delta. Reemplacé el procedimiento original con un contenedor:
CREAR PROCEDIMIENTO memdb_get_transactions @transid int AS

SI se unen(@transid, 0) = 0

EJECUTAR memdb_get_transactions_refresh

MÁS

EMPEZAR

DECLARAR @maxtransid int

SELECCIONE @maxtransid = MAX(transid) DESDE transacciones

EJECUTAR memdb_get_transactions_delta @transid, @maxtransid

FINAL

Tuve que agregar @maxtransid como parámetro al procedimiento delta, porque con una condición abierta como WHERE
transid > @transid, SQL Server tendería a estimar mal la cantidad de filas que tenía que leer. Hacer que el intervalo se
cerrara solucionó ese problema y, al pasar @maxtransid como parámetro, SQL Server pudo detectar el valor.
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 41/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Desde una perspectiva de mantenibilidad, este no es un paso agradable de tomar, sobre todo cuando la lógica es tan
complicada como en este
Inglés 
caso. Si tiene que hacer esto, es importante que llene el código con comentarios para decirles a
Español
las personas que si cambian un procedimiento, también deben cambiar el otro.

Nota : unos años más tarde, un colega reescribió memdb_get_transactions_refresh para convertirlo en un segundo contenedor. Como
mencioné, lee algunos parámetros del sistema para determinar qué transacciones leer. Para que la actualización funcionara
decentemente, descubrió que tenía que pasar los valores de estos parámetros del sistema, así como el intervalo de ID de transacciones
para leer como parámetros a un procedimiento interno para aprovechar al máximo el rastreo de parámetros. (Todavía estábamos en
SQL 2000 en ese momento. En SQL 2005 o posterior , OPTION (RECOMPILE) habría logrado lo mismo).

5.6 Arreglando mal SQL


También puede haber situaciones en las que la causa raíz del problema sea simplemente SQL mal escrito. Como solo un
ejemplo, considere esta consulta:
SELECCIONE ...

DESDE Pedidos

DONDE (IDCliente = @custid O @custid ES NULO)

Y (EmployeeID = @empid O @empid ES NULO)

Y convert(varchar, OrderDate, 101) = convert(varchar, @orderdate, 101)

La idea es que los usuarios aquí puedan buscar pedidos para una fecha determinada y, opcionalmente, también pueden
especificar otros parámetros de búsqueda. El programador aquí considera que OrderDate puede incluir una porción de
tiempo, y lo tiene en cuenta usando un código de formato para convertir() que produce una cadena sin tiempo, y para estar
realmente seguro de que realiza la misma operación en el parámetro de entrada. (En Northwind , OrderDate siempre no
tiene una porción de tiempo, pero por el bien del ejemplo, asumo que este no es el caso).

Para esta consulta, el índice en OrderDate sería la opción obvia, pero por la forma en que se escribe la consulta, SQL
Server no puede buscar ese índice, porque OrderDate está enredado en una expresión. Esto a veces se conoce como la
expresión no ser sargable , donde sarg es la abreviatura de argumento de búsqueda , es decir, algo que se puede usar como
un predicado de búsqueda en una consulta. (Personalmente, no me gusta el término "sargable", pero como es posible que
veas que la gente lo deja caer de vez en cuando, pensé que debería mencionar esta jerga).

Debido a que el índice en OrderDate ha sido descalificado, el optimizador puede conformarse con cualquiera de los otros
índices, según la entrada del primer parámetro, lo que provoca un bajo rendimiento para otros tipos de búsquedas. El
remedio es reescribir la consulta, por ejemplo como:
SELECCIONE ...

DESDE Pedidos

DONDE (IDCliente = @custid O @custid ES NULO)

Y (EmployeeID = @empid O @empid ES NULO)

Y FechaPedido >= @fechaPedido

AND OrderDate < dateadd(DÍA, 1, @orderdate)

(Parece razonable suponer que un parámetro de entrada que se supone que es una fecha no tiene ninguna porción de
tiempo, por lo que hay pocas razones para ensuciar el código con una conversión adicional).

SQL mal escrito a menudo se manifiesta en problemas generales de rendimiento, es decir, la consulta siempre se ejecuta
lentamente, y tal vez no tan a menudo en situaciones en las que la detección de parámetros es importante. Sin embargo,
cuando se enfrenta a su problema de rastreo de parámetros, hay motivos para investigar si la consulta podría escribirse de
una manera que evite la dependencia del parámetro de entrada para un buen plan. Hay muchas maneras de escribir mal
SQL, y este no es el lugar para enumerarlas todas. Ocultar columnas en una expresión es solo un ejemplo, aunque común.
A veces, la expresión está oculta porque se debe a una conversión implícita, por ejemplo, cuando una columna de cadena
que se supone que contiene números se compara con un valor entero. Examine el plan y, cuando no se utilice un índice
como cabría esperar, vuelva atrás y vuelva a examinar la consulta.

6. SQL dinámico
Hasta ahora, solo hemos analizado los procedimientos almacenados, y con los procedimientos almacenados, la razón del
rendimiento diferente en la aplicación y en SSMS se debe con mayor frecuencia a las diferentes configuraciones para SET
ARITHABORT . Si tiene una aplicación que no usa procedimientos almacenados, pero genera las consultas en el cliente o

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 42/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

en la capa intermedia, hay algunas razones más por las que puede obtener una nueva entrada de caché cuando ejecuta la
consultaInglés
en SSMS y talEspañol

vez también una plan diferente al de la aplicación. En este capítulo, veremos estas posibilidades.
También veremos algunas soluciones para abordar los problemas de detección de parámetros que se aplican
principalmente a SQL dinámico.

6.1 ¿Qué es SQL dinámico?


SQL dinámico es cualquier SQL que no forma parte de un procedimiento almacenado (o cualquier otro tipo de módulo).
Esto incluye:

Sentencias SQL ejecutadas con EXEC () y sp_executesql .


Sentencias SQL enviadas directamente desde el cliente.
Sentencias SQL enviadas desde módulos escritos en SQLCLR.

Dynamic SQL viene en dos sabores, sin parametrizar y parametrizado. En SQL no parametrizado, el programador
compone la cadena SQL concatenando los elementos del lenguaje con los valores de los parámetros. Por ejemplo, en
T‑SQL:

SELECCIONE @sql = 'SELECCIONE mycol DESDE tbl DONDE keycol = ' + convert(varchar, @value)

EJECUTIVO(@sql)

O en C#:
cmd.CommandText = "SELECT mycol FROM tbl WHERE keycol = " + value.ToString();

El SQL no parametrizado es muy malo por varias razones, consulte mi artículo The Curse and Blessings of Dynamic SQL
para obtener una discusión sobre las buenas y malas prácticas con SQL dinámico.

En SQL parametrizado, pasa parámetros como en un procedimiento almacenado. En T‑SQL:


EXEC sp_executesql N'SELECCIONE mycol DESDE dbo.tbl DONDE keycol = @value',

N'@valor int', @valor = @valor

O en C#:

cmd.CommandText = "SELECCIONE mycol DESDE dbo.tbl DONDE keycol = @value";

cmd.Parameters.Add("@valor", SqlDbType.Int);

cmd.Parámetros["@valor"].Valor = valor;

El código C# da como resultado una llamada a sp_executesql que se parece exactamente al ejemplo de T‑SQL anterior.

Para obtener más detalles sobre sp_executesql , consulte mi artículo The Curse and Blessings of Dynamic SQL .

6.2 El texto de consulta es la clave hash


Los planes de consulta para SQL dinámico se colocan en la memoria caché del plan, al igual que los planes para los
procedimientos almacenados. (Si escucha que alguien le dice algo más, esa persona simplemente está confundida o confía
en información muy antigua. Hasta SQL Server 6.5, SQL Server no almacenaba en caché los planes para SQL dinámico).
Al igual que con los procedimientos almacenados, los planes para SQL dinámico pueden borrarse de la memoria caché por
varias razones, y las sentencias individuales pueden volver a compilarse. Además, puede haber más de un plan para el
mismo texto de consulta debido a las diferencias en las opciones de SET .

Además de eso, hay algunos factores más que pueden introducir un comportamiento desconcertante con el rastreo de
parámetros que no se aplican a los procedimientos almacenados que veremos en las próximas secciones.

Cuando SQL Server busca un procedimiento almacenado en la memoria caché, utiliza el nombre del procedimiento. Pero
eso no es posible con un lote de SQL dinámico, ya que no hay nombre. En su lugar, SQL Server calcula un hash a partir
del texto de la consulta y utiliza este hash como clave en la memoria caché del plan. Y aquí hay algo muy importante: este
valor hash se calcula sin ningún tipo de normalización del texto del lote. Los comentarios no se eliminan. El espacio en
blanco no se recorta ni se contrae. No se fuerza el uso de mayúsculas o minúsculas, incluso si la base de datos tiene una
intercalación que no distingue entre mayúsculas y minúsculas. El hash se calcula a partir del texto exactamente como se

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 43/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

envió, y cualquier pequeña diferencia generará un hash diferente y una entrada de caché diferente. (Hay una excepción, a
la que volveré
Inglés cuando Español
hablemosde la parametrización automática).
Ejecute esto con Incluir plan de ejecución real habilitado:
EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'

EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

EXEC sp_executesql N'select * from Orders where OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

Encontrará que las dos primeras llamadas utilizan el mismo plan, Index Seek + Key Lookup , mientras que la tercera
consulta utiliza un Clustered Index Scan . Es decir, la segunda llamada reutiliza el plan creado para la primera llamada.
Pero en la tercera llamada, las palabras clave de SQL están en minúsculas. Por lo tanto, no hay coincidencia de caché y se
crea un nuevo plan. Solo para reforzar este hecho, aquí hay un segundo ejemplo con el mismo resultado.
DBCC FREEPROCCACHE

Vamos

EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'

EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

EXEC sp_executesql N'SELECT * FROM Pedidos WHERE OrderDate > @orderdate ',

N'@orderdate datetime', '19980101'

La diferencia es que hay un solo espacio final en la tercera instrucción.

Hay una cosa más que observar. Si ejecuta esta consulta:


SELECCIONE '|' + texto estimado + '|'

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

DONDE est.text LIKE '%Pedidos%'

Verá esta salida:

|(@orderdate datetime)SELECT * FROM Pedidos WHERE OrderDate > @orderdate|

|(@orderdate datetime)SELECT * FROM Pedidos WHERE OrderDate > @orderdate |

(Las barras verticales sirven para resaltar el espacio final). Observe aquí que la lista de parámetros también forma parte del
texto de la consulta, y también se incluye en el texto de la consulta que tiene hash. Ejecuta esto:
EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

Es decir, la misma consulta de nuevo, pero con un espacio más en la lista de parámetros. Si ahora vuelve a ejecutar la
consulta del DMV, verá que ahora devuelve tres filas. (Bueno, cuatro ya que también se encuentra).

Este detalle sobre la lista de parámetros puede parecer una cosa de naturaleza más trivial, pero tiene una implicación
importante si está escribiendo código de cliente. Considere este fragmento de C#:
cmd.CommandText = "SELECT * FROM dbo.Orders WHERE CustomerID = @c";

cmd.Parameters.Add("@c", SqlDbType.NVarChar).Value = TextBox.Value;

Tenga en cuenta que el parámetro se agrega sin una longitud específica para el valor del parámetro. Diga que el valor en el
cuadro de texto es ALFKI. Esto da como resultado la siguiente llamada a SQL Server:
exec sp_executesql N'SELECT * FROM Pedidos WHERE CustomerID = @c',

N'@c nvarchar(5)',@c=N'ALFKI'

Tenga en cuenta que el parámetro se declara como nvarchar(5) . Es decir, la longitud del parámetro se toma de la longitud
del valor real pasado. Esto significa que si ejecuta el mismo lote varias veces con diferentes valores de parámetros de
diferente longitud, obtendrá múltiples entradas de caché. No solo ensucias el caché, sino que debido a la detección de
parámetros, puedes obtener diferentes planes al azar para diferentes longitudes de parámetros. Esto puede resultar en un
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 44/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

comportamiento confuso donde la misma operación es lenta para un valor, pero rápida para otro, aunque esperaría el
mismo rendimiento
Inglés para

estos valores.
Español

Hay un remedio simple para este problema en particular: siempre especifique la longitud explícitamente:

cmd.Parameters.Add("@c", SqlDbType.NVarChar, 5).Valor = TextBox.Value;

Aquí usé 5, ya que la columna CustomerID es nchar(5) . Si no desea crear una dependencia con el modelo de datos,
puede especificar la longitud como algo mayor, por ejemplo, 4000 (que es el máximo para un nvarchar normal ).

6.3 La importancia del esquema predeterminado


Otra diferencia con los procedimientos almacenados es menos obvia y se muestra mejor con un ejemplo. Ejecuta esto y
mira los planes de ejecución:
DBCC FREEPROCCACHE

Vamos

CREAR ESQUEMA Esquema2

Vamos

CREAR USUARIO Usuario1 SIN INICIO DE SESIÓN CON DEFAULT_SCHEMA = dbo

CREAR USUARIO Usuario2 SIN LOGIN CON DEFAULT_SCHEMA = Schema2

OTORGAR SELECCIONAR EN Órdenes A Usuario1, Usuario2

CONCEDER SHOWPLAN A Usuario1, Usuario2

Vamos

EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'

Vamos

EJECUTAR COMO USUARIO = 'Usuario1'


EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

REVERTIR

Vamos

EJECUTAR COMO USUARIO = 'Usuario2'


EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '19980101'

REVERTIR

Vamos

ELIMINAR USUARIO Usuario1

ELIMINAR USUARIO Usuario2

DROP ESQUEMA Schema2

Vamos

El script primero borra el caché del plan y luego crea un esquema y dos usuarios de la base de datos y les otorga los
permisos necesarios para ejecutar las consultas. Luego ejecutamos la misma consulta tres veces, pero con diferentes
valores de parámetro. La primera vez que pasamos una fecha que está más allá del rango en Northwind , entonces el
optimizador se conforma con un plan con Index Seek + Key Lookup . La segunda y tercera vez que ejecutamos la
consulta, pasamos una fecha diferente, por lo que una Exploración de índice agrupado es una mejor opción. Pero como
hay un plan en caché, esperamos obtener ese plan, y eso es lo que sucede también en la segunda ejecución. Sin embargo,
en la tercera ejecución, encontramos para nuestra sorpresa que el plan es el CI Scan. ¿Qué está pasando? ¿Por qué no
conseguimos el plan en el caché?

La clave aquí es que ejecutamos la consulta como tres usuarios diferentes. La primera vez que ejecutamos la consulta
como nosotros mismos (presumiblemente somos dbo ), pero para las otras dos ejecuciones nos hacemos pasar por los dos
usuarios recién creados. (Si no está familiarizado con la suplantación, busque el tema EJECUTAR COMO en los Libros en
línea; también lo cubro en mi artículo Permisos de empaquetado en procedimientos almacenados ). necesita ningún inicio
de sesión para este ejemplo. Lo importante es que tienen diferentes esquemas predeterminados. User1 tiene dbo como
esquema predeterminado, pero para User2 el esquema predeterminado es Schema2 . ¿Por qué importa esto?

Tenga en cuenta que cuando SQL Server busca un objeto, primero busca en el esquema predeterminado del usuario y, si no
encuentra el objeto, busca en el esquema dbo . Para dbo y Usuario1, la consulta no es ambigua, ya que dbo es su esquema
predeterminado y este es el esquema para la tabla Pedidos . Pero para User2 esto es diferente. Actualmente solo hay
dbo.Orders , pero ¿qué pasa si Schema2.Orders se agrega más tarde? Según las reglas, User2 ahora debería obtener
datos de esa tabla y no de dbo.Orders . Pero si User2 usara la misma entrada de caché que dbo y User1, eso no sucedería.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 45/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Por lo tanto, User2 necesita una entrada de caché propia. SiSe agrega Schema2.Orders , esa entrada de caché se puede
invalidarInglés
sin afectar a otros

usuarios.
Español

Podemos ver que estos son los atributos del plan. Aquí hay una variación de la consulta que ejecutamos para los
procedimientos almacenados:
SELECCIONE qs.plan_handle, a.attrlist

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA (SELECCIONE epa.attribute + '=' + convert(nvarchar(127), epa.value) + ' '

DESDE sys.dm_exec_plan_attributes(qs.plan_handle) epa

DONDE epa.is_cache_key = 1

ORDEN POR epa.atributo

FOR XML PATH('')) COMO una(lista de atributos)

DONDE a.attrlist LIKE '%dbid=' + ltrim(str(db_id())) + ' %'

-- Y est.text LIKE '%WHERE OrderDate > @orderdate%'

Y est.text NO COMO '%sys.dm_exec_plan_attributes%'

Hay tres diferencias en la consulta de procedimientos almacenados:

1. El filtrado de la base de datos es diferente, ya que la columna dbid en sys.dm_exec_sql_text no se completa para SQL
dinámico, sino que debemos tomar este valor de sys.dm_exec_plan_attributes . Para evitar tener que llamar a esta
función dos veces en la consulta, el filtrado se realiza de esta manera algo torpe.
2. Como no hay un nombre de procedimiento con el que coincidir, tenemos que usar parte del texto de la consulta.
3. Necesitamos una condición adicional para filtrar la consulta contra sys.dm_exec_plan_attributes .

Cuando ejecuté esta consulta, vi esta lista (parcial) de atributos:


date_first=7 date_format=1 dbid=6 objectid=158662399 set_options=251 user_id=5
date_first=7 date_format=1 dbid=6 objectid=158662399 set_options=251 user_id=1

Primero mire objectid . Como puede ver, este valor es idéntico para las dos entradas. Esta identificación de objeto es el
valor hash que describí anteriormente. A continuación, mire el atributo que es distintivo: user_id . El nombre como tal es
un nombre inapropiado; el valor es el esquema predeterminado para los usuarios que utilizan este plan. El esquema dbo
siempre tiene schema_id  = 1. En mi base de datos Northwind , Schema2 obtuvo schema_id  = 5 cuando ejecuté la
consulta, pero es posible que vea un valor diferente.

Ahora ejecuta esta consulta:


EXEC sp_executesql N'SELECT * DE dbo. Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'

Y luego ejecute la consulta contra sys.dm_exec_plan_attributes nuevamente. Aparece una tercera fila en la salida:
date_first=7 date_format=1 dbid=6 objectid=549443125 set_options=251 user_id=-2

objectid es diferente al anterior, ya que el texto de la consulta es diferente. user_id ahora es -2. ¿Qué significa esto? Si
observa más de cerca la consulta, verá que ahora especificamos el esquema explícitamente cuando accedemos a Pedidos .
Es decir, la consulta ahora no es ambigua y todos los usuarios pueden usar esta entrada de caché. Y eso es exactamente lo
que significa user_id  = -2: no hay referencias de objetos ambiguas en la consulta. El corolario de esto es que es una buena
práctica utilizar siempre la notación de dos partes en SQL dinámico, sin importar si crea el SQL dinámico en un programa
cliente o en un procedimiento almacenado.

Esto se aplica no solo a tablas y vistas, sino también a cualquier función definida por el usuario a la que pueda llamar.
También se aplica a los tipos definidos por el usuario que puede usar (tipos de tabla, tipos CLR, tipos de alias sin formato,
etc.), incluida la lista de parámetros. (La lista de parámetros es parte de lo que se almacena en caché). Si se hace referencia
a un solo objeto que vive en sys.objects o sys.types en notación de una parte, esta referencia evita que los usuarios
compartan la entrada de caché con diferentes esquemas predeterminados. .

Puede pensar "no usamos esquemas en nuestra aplicación, por lo que esto no es un problema para nosotros", ¡pero no tan
rápido! Cuando usa CREATE USER , el esquema predeterminado para el nuevo usuario es dbo , a menos que especifique
algo más. Sin embargo, si el DBA es de la vieja escuela, puede crear usuarios con cualquiera de los procedimientos

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 46/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

heredados sp_adduser o sp_grantdbaccess, y funcionan de manera diferente. No solo crean un usuario, sino que también
crean unInglés
esquema con Español
el mismonombre que el usuario y establecen este esquema como el esquema predeterminado para
el usuario recién creado y hacen que este usuario sea el propietario del esquema. ¿Suena cursi? Sí, pero hasta SQL 2000, el
esquema y los usuarios estaban unificados en SQL Server. Dado que es posible que no tenga control sobre cómo se crean
los usuarios, no debe confiar en que los usuarios tengan dbo como su esquema predeterminado.

Finalmente, puede preguntarse por qué este problema no se aplica al almacenamiento en caché de planes para
procedimientos almacenados. La respuesta es que en un procedimiento almacenado, la resolución de nombres siempre la
realiza el propietario del procedimiento, no el usuario actual. Es decir, en un procedimiento propiedad de dbo , Orders
solo puede hacer referencia a dbo.Orders , nunca a ninguna tabla Orders en algún otro esquema. (Pero tenga en cuenta
que esto se aplica solo al texto de consulta directa del procedimiento almacenado. No se aplica al SQL dinámico invocado
con EXEC () o sp_executesql dentro del procedimiento).

6.4 Auto-parametrización
Dije que hay una excepción a la regla de que SQL Server no normaliza el texto de la consulta antes de calcular el hash.
Esa excepción es la parametrización automática, un mecanismo por el cual SQL Server reemplaza las constantes en una
consulta no parametrizada y la convierte en una consulta parametrizada. El propósito de la parametrización automática es
reducir la amenaza de aplicaciones mal escritas que insertan valores de parámetros en cadenas SQL en lugar de ajustarse a
las mejores prácticas mediante el uso de declaraciones parametrizadas.

Existen dos modelos, para auto-parametrización: simple y forzado. El modelo está controlado por una configuración de
base de datos y la parametrización simple es la predeterminada. Con este modelo, SQL Server auto-parametriza solo
sentencias SQL de muy baja complejidad. Además, SQL Server solo emplea una parametrización automática simple
cuando hay un único plan posible; es decir, la parametrización simple está diseñada para no causar problemas de detección
de parámetros. Con la parametrización forzada, SQL Server reemplaza todas las constantes en una consulta con
parámetros, incluso si son posibles múltiples planes. Es decir, cuando la parametrización forzada está en juego, puede
enfrentar problemas con la detección de parámetros.

Nota : la parametrización forzada solo se aplica a SQL dinámico, nunca a consultas en procedimientos almacenados. Además, la
parametrización forzada no se aplica a las consultas que hacen referencia a variables. Hay algunas situaciones más en las que SQL
Server no aplica la parametrización forzada, como se indica en este tema de los Libros en pantalla.

A continuación, veremos cómo puede ver si puede tener un problema de rastreo de parámetros debido a la parametrización
automática. La siguiente secuencia de comandos configura a Northwind en parametrización forzada y luego ejecuta dos
consultas en la tabla Pedidos . Uno que no devuelve filas y otro que devuelve la mayoría de las filas.

ALTER DATABASE Northwind ESTABLECER PARAMETRIZACIÓN FORZADA

DBCC FREEPROCCACHE

Vamos

SELECCIONE * DESDE Pedidos DONDE Fecha de pedido > '20000101'

Vamos

SELECCIONE * DESDE Pedidos DONDE Fecha de pedido > '19970101'

Vamos

; CON datos basados ​


COMO (

SELECCIONE est.text COMO sqltext, qp.query_plan,

charindex('<Lista de parámetros>', qp.query_plan) + len('<Lista de parámetros>')

como paramstart,

charindex('</ParameterList>', qp.query_plan) como parámetro

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA sys.dm_exec_text_query_plan(qs.plan_handle,

qs.statement_start_offset,

qs.statement_end_offset) qp

DONDE est.text NO COMO '%exec_query_stats%'

), siguiente_nivel AS (

SELECCIONE sqltext, query_plan,


CASO CUANDO paramend > paramstart

ENTONCES CAST (subcadena (query_plan, paramstart,

paramend - paramstart) AS xml)

TERMINAR COMO parámetros


DESDE datos basados

SELECCIONE texto SQL,

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 47/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
(SELECCIONE CR.c.valor('@Columna', 'nvarchar(128)') + ' = ' +

CR.c.value('@ParameterCompiledValue', 'nvarchar(512)') + ' '

Inglés Español

DESDE n.params.nodes('ColumnReference') COMO CR(c)

FOR XML PATH(''), TYPE).value('.', 'nvarchar(MAX)') AS Parámetros,

CAST (query_plan AS xml) AS [Query plan]

DESDE siguiente_nivel n

Vamos

Si observa el resultado del Plan de ejecución en SSMS, verá esto en la parte superior:

Es decir, la fecha ha sido reemplazada por un parámetro, @0 . Y si observa los planes de ejecución, verá que ambas
consultas se ejecutan con Index Seek + Key Lookup, a pesar de que la segunda consulta se implementa mejor como un
escaneo de índice agrupado, ya que accede a la mayor parte de la tabla. El resultado de la consulta de diagnóstico es así:

Es decir, a pesar de que ejecutamos dos declaraciones con texto de consulta diferente, solo hay una entrada de caché donde
el plan se determina a partir del valor de la fecha en la primera declaración SELECT .

Ahora cambie FORZADO en el script para leer SIMPLE y vuelva a ejecutar el script. En la parte superior del plan de
ejecución, verá lo mismo que arriba (excepto que @0 ahora es @1 por alguna razón). Sin embargo, esta vez las dos
consultas tienen diferentes planes de ejecución y el resultado de la última consulta ahora se ve así:

Lo que parece suceder con la parametrización simple es que SQL Server intenta parametrizar automáticamente la consulta,
pero cuando compila la consulta descubre que hay más de un plan posible y se retira de la parametrización automática. Sin
embargo, quedan algunos rastros del intento de parametrización automática, y esto podría atraerlo si solo observa el texto
de la consulta en la salida del plan de presentación. Debe obtener el texto de sys.dm_exec_sql_text como en la consulta
anterior, para ver el texto real en el caché del plan y asegurarse de que la parametrización automática esté realmente en
juego.

Nota : cuando ejecuta lo anterior para una parametrización simple, es posible que vea NULL en las columnas Params y Query plan
cuando ejecuta el script por primera vez. Si lo vuelve a ejecutar, sin incluir el primer lote, verá los valores en la segunda ejecución.
Esto se debe al parámetro de configuración optimizar para cargas de trabajo ad hoc. Cuando este parámetro es 1, SQL Server solo
almacena en caché el texto de la consulta en la primera ejecución de una cadena sin parámetros. Solo si la misma cadena se ejecuta por
segunda vez, el plan se almacena en caché. El valor predeterminado para esta configuración es 0, pero 1 es la configuración
recomendada, ya que mitiga el efecto de las aplicaciones mal escritas que no parametrizan las declaraciones. No hay inconvenientes
conocidos al establecer la opción en 1. A partir de SQL 2019, esta configuración también se puede habilitar por base de datos con
ALTER DATABASE SCOPED CONFIGURATION.

6.5 Ejecución de consultas de aplicaciones en SSMS


Como comprenderá de las secciones anteriores, hay algunas trampas más cuando desea solucionar una consulta de
aplicación de SSMS que puede hacer que obtenga una entrada de caché diferente y, potencialmente, un plan de consulta
diferente.

Al igual que con los procedimientos almacenados, debe tener en cuenta ARITHABORT y otras opciones de SET . Pero
también debe asegurarse de tener exactamente el mismo texto de consulta y de que su esquema predeterminado esté de
acuerdo con el usuario que ejecuta la aplicación.

Este último es el más fácil de tratar. En la mayoría de los casos, esto hará:

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 48/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
EJECUTAR COMO USUARIO = 'appuser'

Vamos

Inglés Español
-- Ejecute SQL aquí

Vamos

REVERTIR

appuser es aquí el usuario de la base de datos que usa la aplicación, ya sea un usuario privado para la aplicación o el
nombre de usuario de la persona real que usa la base de datos. Observe que esto falla si la consulta accede a recursos fuera
de la base de datos actual. En este caso, puede usar EJECUTAR COMO INICIAR SESIÓN en su lugar. Tenga en cuenta que
esto requiere un permiso de nivel de servidor.

Recuperar el texto SQL exacto puede ser más difícil. Lo mejor es utilizar un seguimiento para capturar el SQL; puede
ejecutar el seguimiento en Profiler o como un seguimiento del lado del servidor. Si la instrucción SQL no está
parametrizada, debe tener cuidado de copiar el texto completo y luego seleccionar exactamente ese texto en SSMS. Es
decir, no elimine ni agregue espacios en blanco iniciales o finales. No agregue saltos de línea para que la consulta sea más
legible y no elimine ningún comentario. Manténgalo exactamente como lo ejecuta la aplicación. Puede usar la consulta
contra sys.dm_exec_plan_attributes en este artículo para verificar que no agregó una segunda entrada al caché.

Otra alternativa es obtener el texto de sys.dm_exec_query_stats y sys.dm_exec_sql_text . Aquí hay una consulta que
puede usar:
SELECCIONE '<' + texto estimado + '>'

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

DONDE est.text LIKE '%algún texto SQL significativo aquí%'

Al copiar el texto de SSMS, es importante asegurarse de que se conserven los saltos de línea. Si ejecuta la consulta en
modo de texto, esto no es un problema, pero si está en modo de cuadrícula, debe tener cuidado ya que, de forma
predeterminada, SSMS reemplaza los saltos de línea con espacios (lo cual es bueno si desea copiar datos a Excel, pero
aqui no). Puede cambiar esto en ->Opciones de herramientas como se ve aquí:

Tenga en cuenta que esta opción no está disponible en versiones anteriores de SSMS. Si tiene SSMS 2012 o SSMS 2014,
no importa, ya que conservan CR/LF de todos modos. (Pero SSMS 2008 y SSMS 2005 no).

SQL parametrizado es más fácil de manejar, ya que la instrucción SQL está empaquetada en un carácter literal. Es decir, si
ves esto en Profiler

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 49/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
EXEC sp_executesql N'SELECT * FROM Pedidos DONDE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'


Inglés Español

No hay problema si lo cambia a, digamos:
EXEC sp_executesql

N'SELECT * FROM Pedidos WHERE OrderDate > @orderdate',

N'@orderdate datetime', '20000101'

Lo importante es que no debe cambiar nada de lo que está entre las comillas de los dos primeros parámetros a
sp_executesql , ya que a partir de ahí se calcula el hash. (Es decir, la lista de parámetros se incluye en el cálculo hash).

Si no tiene ningún permiso a nivel de servidor ( ALTER TRACE para ejecutar un seguimiento o VIEW SERVER STATE para
consultar sys.dm_exec_query_stats y sys.dm_exec_sql_text ), está empezando a ser difícil. (A menos que esté en SQL
2016 o posterior y haya habilitado Query Store para la base de datos, que trataré en el próximo capítulo). Si el SQL
dinámico se produce en un procedimiento almacenado que puede editar, puede agregar una instrucción PRINT para obtener
el texto. (En realidad, los procedimientos almacenados que producen SQL dinámico siempre deben tener un parámetro
@debug e incluir la línea:IF @debug = 1 PRINT @sql.) Aún debe tener cuidado de obtener el texto exacto y no agregar ni
quitar espacios. Si hay una lista de parámetros, debe asegurarse de copiar la lista de parámetros exactamente también. Si la
aplicación produce el SQL, es posible obtener la instrucción SQL si puede ejecutar la aplicación en un depurador, pero
puede ser difícil obtener el texto exacto de la lista de parámetros. La mejor opción puede ser probar la aplicación en una
base de datos en una instancia de SQL Server donde tenga todos los permisos necesarios, por ejemplo, en su estación de
trabajo local y obtener el texto de la consulta de esta manera.

6.6 Guías de planes y congelación de planes


A veces, es posible que desee modificar una consulta para resolver un problema de rendimiento agregando algún tipo de
sugerencia. Para un procedimiento almacenado, no es improbable que pueda editar el procedimiento e implementar el
cambio de manera bastante inmediata. Pero si la consulta se genera dentro de una aplicación, puede ser más difícil. El
ejecutable completo debe construirse y tal vez implementarse en todas las máquinas de los usuarios. También es probable
que haya un paso de control de calidad obligatorio involucrado. Y si se trata de una aplicación de terceros, cambiar el
código está fuera de cuestión.

Sin embargo, SQL Server proporciona soluciones para estas situaciones. En este capítulo, veremos las guías de planes .
Las guías de planes pueden ser una bendición si tiene una consulta lenta en una aplicación que no puede cambiar. Sin
embargo, como verá, no son tan fáciles de configurar y es probable que solo use las guías de planes como último recurso.
Hay un atajo conocido como congelación de planes que es más fácil de usar. Sin embargo, la congelación del plan es algo
que es de interés principalmente en SQL 2014 y versiones anteriores, ya que en SQL 2016 y versiones posteriores, usaría
Query Store para la misma tarea, y lo veremos en el próximo capítulo.

Nota : las guías de planes no están disponibles en las ediciones de gama baja de SQL Server: Express, Web y Workgroup Edition.

Este es un ejemplo de cómo configurar una guía de planes. Este ejemplo en particular se ejecuta en SQL 2008 y versiones
posteriores:
DBCC FREEPROCCACHE

Vamos

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19960101'

Vamos

EXEC sp_create_plan_guide

@nombre = N'MiGuía',

@stmt = N'SELECT * FROM dbo.Orders WHERE OrderDate > @orderdate',

@tipo = N'SQL',

@module_or_batch = NULO,

@params = N'@orderdate datetime',

@hints = N'OPTION (TABLE HINT (dbo.Orders , INDEX (OrderDate)))'

Vamos

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19980101'

Vamos

EXEC sp_control_plan_guide N'DROP', N'MyGuide'

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 50/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

En este ejemplo, creo un plan para garantizar que una consulta en OrderDate siempre use una búsqueda de índice (tal vez
porque espero
Inglés que todas

las consultas sean de los últimos días). Especifico un nombre para la guía. A continuación,
Español
especifico la afirmación exacta a la que se aplica la guía. Como cuando ejecuta una consulta en SSMS para investigarla,
debe asegurarse de no agregar o perder ningún espacio inicial o final, ni debe realizar ninguna otra modificación. El
parámetro @type especifica que la guía es para SQL dinámico y no para un procedimiento almacenado. Si la declaración
SELECT hubiera sido parte de un lote más grande, tendría que especificar el texto de ese lote en el parámetro
@module_or_batch, nuevamente exactamente como se envió desde la aplicación. Cuando especifico NULL para
@module_or_batch , se supone que @stmt es el texto completo del lote. @params es la lista de parámetros para el lote
y nuevamente debe haber una coincidencia exacta carácter por carácter con lo que envía la aplicación.

Finalmente, @hints es donde está la diversión. En este ejemplo, especifico que la consulta siempre debe usar el índice en
OrderDate , sin importar el valor rastreado para @orderdate . Esta sugerencia de consulta en particular, OPTION (
TABLE HINT ) no está disponible en SQL 2005, por lo que el ejemplo no se ejecuta en esa versión.

En el script, el DBCC FREEPROCCACHE inicial solo está ahí para darnos una pizarra limpia. Además, para el propósito de
la demostración, ejecuto la consulta con un valor de parámetro que da el plan "malo", el Análisis de índice agrupado . Una
vez ingresada la guía del plan, surte efecto inmediatamente. Es decir, cualquier entrada actual de la consulta se expulsa de
la memoria caché.

En SQL 2008 y versiones posteriores, puede especificar los parámetros para sp_create_plan_guide en cualquier orden
siempre que los nombre y puede omitir la N antes de los literales de cadena. Sin embargo, SQL 2005 es mucho menos
indulgente. Los parámetros deben ingresarse en el orden dado, incluso si los nombra, y debe especificar la N antes de
todos los literales de cadena.

En este ejemplo, utilicé una guía de plan para forzar un índice, pero también puede usar otras sugerencias, incluida la
sugerencia USE PLAN que le permite especificar el plan de consulta completo que se usará. ¡Ciertamente no es para los
débiles de corazón!

...aunque esa es exactamente la idea con la congelación del plan. Digamos que tiene una consulta que oscila entre dos
planes, uno bueno y otro malo, debido a la detección de parámetros, y en realidad no hay una forma civilizada de eliminar
el plan malo de la ecuación. En lugar de luchar con los parámetros complejos de sp_create_plan_guide , puede extraer un
controlador de plan de la memoria caché y enviarlo al procedimiento almacenado sp_create_plan_guide_from_handle
para forzar el plan que sabe que es bueno. Aquí hay una demostración y un ejemplo.
DBCC FREEPROCCACHE

ACTIVAR ARITHABORT

Vamos

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19990101'

Vamos

DECLARAR @plan_handle varbinary(64),

@rowc int

SELECCIONE @plan_handle = plan_handle

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

DONDE est.text LIKE '%Orders WHERE OrderDate%'

Y est.text NO COMO '%dm_exec_query_stats%'

SELECCIONE @rowc = @@rowcount

SI @filac = 1

EJECUTIVO sp_create_plan_guide_from_handle 'MyFrozenPlan', @plan_handle

MÁS

RAISERROR('%d planes encontrados en la caché del plan. No se puede crear la guía del plan', 16, 1, @rowc)

Vamos

-- ¡Pruébalo!

DESACTIVAR ARITHABORT

Vamos

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19960101'

Vamos

ACTIVAR ARITHABORT

EXEC sp_control_plan_guide 'DROP', 'MyFrozenPlan'

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 51/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

Para el propósito de la demostración, primero borro el caché del plan y configuro ARITHABORT en un estado conocido.
Luego ejecuto
Inglés mi consulta

con un parámetro que sé que me dará un buen plan. El siguiente lote demuestra cómo usar
Español
sp_create_plan_guide_from_handle . Primero ejecuto una consulta contra sys.dm_exec_query_stats y
sys.dm_exec_sql_text para encontrar la entrada de mi consulta. A continuación, capturo @@rowcount en una variable
local (dado que @@rowcount se establece después de cada declaración, prefiero copiarlo en una variable local en un
SELECTque se adjunta directamente a la consulta para evitar accidentes). Esta es una precaución de seguridad en caso de
que obtenga varias coincidencias o ninguna coincidencia en el caché. Si obtengo exactamente una coincidencia, llamo a
sp_create_plan_guide_from_handle , que toma dos parámetros: el nombre de la guía del plan y el identificador del plan.
¡Y eso es!

La siguiente parte prueba la guía. Para asegurarme de no usar la misma entrada de caché, uso una configuración diferente
de ARITHABORT . Si ejecuta la demostración con la visualización del plan de ejecución habilitada, verá que la segunda
ejecución de la consulta utiliza el mismo plan con Index Seek + Key Lookup que la primera, aunque el plan normal para el
parámetro dado sería un escaneo de índice agrupado . Es decir, la guía del plan no está vinculada a un determinado
conjunto de atributos del plan.

Cuando use esto de verdad, ejecutaría la consulta para la que desea la guía del plan, solo si el plan deseado aún no está en
el caché. La consulta contra el caché requerirá algo de destreza para que obtenga exactamente un resultado y el resultado
correcto. Una alternativa puede ser mirar las coincidencias en el resultado de la consulta en SSMS y copiar y pegar el
identificador del plan.

Lo bueno es que no tiene que configurar esto en el servidor de producción, pero puede experimentar en un servidor de
laboratorio. La guía se almacena en sys.plan_guides , por lo que una vez que tenga la guía correcta, puede usar el
contenido allí para crear una llamada a sp_create_plan_guide para que ejecute el servidor de producción. También puede
escribir la guía del plan a través del Explorador de objetos en SSMS.

Si tiene un lote de instrucciones múltiples o un procedimiento almacenado, es posible que no desee configurar una guía
para todo el lote, sino solo para una instrucción. Por esta razón , sp_create_plan_guide_from_handle acepta un tercer
parámetro @statement_start_offset , un valor que puede obtener de sys.dm_exec_query_stats .

Un problema potencial con las guías de planes es que, dado que no aparecen en el texto de la consulta, pueden pasarse por
alto y olvidarse fácilmente. Un riesgo es que a medida que evoluciona la base de datos, el plan exigido por la guía del plan
ya no es el mejor. El perfil de los datos podría haber cambiado, o podría haberse agregado un mejor índice. Un segundo
riesgo es que la consulta se cambie de una forma aparentemente inocente. Pero si el texto cambia, si solo agrega un
espacio, la guía del plan ya no coincidirá, y es posible que vuelva al bajo rendimiento original y, habiendo olvidado todo
sobre la guía del plan, tendrá que reinventar la rueda. .

Por esta razón, es una buena idea agregar a su lista de verificación de solución de problemas que debe consultar
sys.plan_guides para ver si hay guías de planes en la base de datos y, si las hay, puede investigar más a fondo si son
relevantes para el consultas que está luchando. Si desea ver las guías de planes actualmente activas, también puede usar
esta consulta para encontrar esta información en el caché del plan:
; CON constantes COMO (

SELECCIONE N' PlanGuideDB="' AS guide_db_attr, guide_name_attr = N'PlanGuideName="'

), datos basados ​
AS (

SELECCIONE est.text COMO sqltext, qp.query_plan,

charindex(c.guide_db_attr, qp.query_plan) + len(c.guide_db_attr)

como guía_db_inicio,

charindex(c.guide_name_attr, qp.query_plan) + len(c.guide_name_attr)

AS guía_nombre_inicio

DESDE sys.dm_exec_query_stats qs

APLICACIÓN CRUZADA sys.dm_exec_sql_text(qs.sql_handle) est

APLICACIÓN CRUZADA sys.dm_exec_text_query_plan(qs.plan_handle,

qs.statement_start_offset,

qs.statement_end_offset) qp

CROSS JOIN constantes c

DONDE est.text NO COMO '%exec_query_stats%'

), siguiente_nivel AS (

SELECCIONE sqltext, query_plan,


CASO CUANDO guide_db_start > 100

ENTONCES subcadena (query_plan, guide_db_start,

charindex('"', query_plan, guide_db_start + 1) -

guía_db_inicio)

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 52/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
FINALIZAR COMO PlanGuideDB,

Inglés CASO CUANDO guide_name_start > 100

Español

ENTONCES subcadena (query_plan, guide_name_start,

charindex('"', query_plan, guide_name_start + 1) -

guía_nombre_inicio)

TERMINAR COMO PlanGuideName

DESDE datos basados

SELECCIONE sqltext, PlanGuideName, PlanGuideDB,

TRY_CAST (query_plan AS xml) AS [Query plan]

DESDE siguiente_nivel n

DONDE PlanGuideDB NO ES NULO O PlanGuideName NO ES NULO

La consulta es de naturaleza similar a las consultas que hemos usado para extraer valores de parámetros del plan, pero aquí
buscamos los dos atributos que identifican la guía del plan. La forma normal de extraerlos sería usar XQuery, pero como
he comentado: existe el riesgo de que el plan no se pueda representar en el tipo de datos xml incorporado , de ahí las
acrobacias con charindex y substring .

Para obtener más información sobre las guías de planes, consulte las secciones sobre guías de planes en los Libros en línea
o el informe técnico que se incluye en la sección Lecturas adicionales a continuación.

7. Uso del almacén de consultas


7.1 Introducción al Almacén de consultas
SQL Server 2016 introdujo una nueva característica, conocida como Query Store. Cuando Query Store está habilitado para
una base de datos, SQL Server guarda los planes, la configuración y las estadísticas de ejecución para consultas en tablas
persistentes. Esto le permite realizar un seguimiento de la ejecución de consultas a lo largo del tiempo y cuando se produce
un cambio abrupto en el rendimiento debido a que un plan cambió de un día para otro, puede buscar ambos planes en las
tablas del Almacén de consultas. Y, si necesita abordar el problema rápidamente, puede forzar el plan antiguo y más
rápido.

Este capítulo no tiene como objetivo brindar una cobertura completa de Query Store, sino solo brindarle consultas para
obtener la misma información de Query Store que mostré anteriormente para el caché del plan. Si tiene la opción de
obtener planes del Almacén de consultas y la memoria caché del plan, el Almacén de consultas suele ser una mejor
alternativa, ya que los datos se conservan. Por ejemplo, digamos que los usuarios informaron un mal desempeño entre las
nueve y las diez de la mañana, pero en el momento en que investigaste el problema, el desempeño volvió a la normalidad,
porque la consulta con el plan incorrecto se volvió a compilar por algún motivo sin tu intervención. En este caso, no puede
encontrar el plan en el caché para que pueda investigar cómo se veía el plan y qué valores de parámetros se rastrearon. Por
otro lado, es muy probable que pueda encontrar el plan incorrecto en las tablas del Almacén de consultas.

Dado que Query Store está en el nivel de la base de datos, esto le brinda una ventaja si está buscando problemas en una
base de datos específica, sin necesidad de filtrar otras bases de datos. Pero esto funciona en ambos sentidos; si está
buscando problemas en todo el servidor, es posible que prefiera consultar el caché del plan. El hecho de que el Almacén de
consultas esté a nivel de base de datos ofrece una segunda ventaja si no tiene permisos a nivel de servidor: solo necesita el
permiso VER ESTADO DE LA BASE DE DATOS para consultar el Almacén de consultas. (Tiene este permiso, si es miembro
del rol db_owner ).

Query Store no está habilitado de forma predeterminada para una base de datos. Para habilitar Query Store para
Northwind , ejecute este comando:
ALTER DATABASE Northwind SET QUERY_STORE = ON (QUERY_CAPTURE_MODE = TODO)

Hay un par de opciones que le permiten controlar el comportamiento y el rendimiento de Query Store. No cubriré estas
opciones aquí, solo una, y esa es QUERY_CAPTURE_MODE que está presente en este comando. Con el modo ALL , Query
Store guarda datos sobre todas las consultas, y necesitamos esta configuración aquí para que funcionen las
demostraciones. ALL es la configuración predeterminada para SQL 2016 y SQL 2017. SQL 2019 introdujo el modo AUTO
, que también es el predeterminado en SQL 2019 y versiones posteriores. En este modo, el Almacén de consultas captura
las consultas relevantes en función del recuento de ejecuciones y el consumo de recursos para cotizar Libros en línea. Las
consultas en los procedimientos almacenados siempre se capturan, por lo que esto solo se aplica a SQL dinámico. creo que
AUTOes una buena opción, ya que reduce la sobrecarga y el almacenamiento necesario para Query Store en un sistema en
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 53/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

el que no se han respetado las mejores prácticas y las consultas generalmente no están parametrizadas. Pero como se
mencionó, necesitamosEspañol
Inglés

TODOS aquí para que las demostraciones funcionen.

Las tablas del Almacén de consultas no se exponen directamente. En su lugar, consulta vistas que también incluyen datos
que aún no se han conservado en el disco. (Para garantizar que el Almacén de consultas tenga una sobrecarga baja, los
datos se vacían en las tablas persistentes de forma asíncrona).

7.2 Encontrar claves de caché


La primera consulta contra el caché del plan que vimos es una que nos da los atributos del plan que son claves de caché .
La versión Query Store de esta consulta es mucho más sencilla, ya que se accede a los atributos del plan a través de
sys.query_context_settings, donde cada atributo es una columna. Sin embargo, las opciones SET siguen siendo una
máscara de bits. Aquí hay una consulta que devuelve los atributos del plan más importantes:
SELECCIONE q.query_id, convierta (bigint, cs.set_options) como set_options,

cs.language_id, cs.date_format, cs.date_first, cs.default_schema_id

DESDE sys.query_store_query q

ÚNASE a sys.query_context_settings cs

ON q.context_settings_id = cs.context_settings_id

DONDE q.object_id = object_id('dbo.List_orders_6')

El caso de prueba para esta consulta fue:


CREAR PROCEDIMIENTO List_orders_6 AS

SELECCIONE *

DESDE Pedidos

DONDE Fecha de pedido > '12/01/1998'

Vamos

ESTABLECER FORMATO DE FECHA dmy

Vamos

EXEC Lista_pedidos_6

Vamos

ESTABLECER FORMATO DE FECHA mdy

Vamos

EXEC Lista_pedidos_6

Vamos

Cuando ejecuté la consulta anterior, obtuve este resultado:


query_id set_options language_id date_format date_first default_schema_id
-------- ----------- ----------- ----------- --------- - -----------------
7 4347 0 2 7 -2
8 4347 0 1 7 -2

Tenga en cuenta que hay diferentes valores en la columna date_format . Cuando se trata de la columna query_id , esta es
solo una identificación interna para la consulta y es posible que vea diferentes valores cuando intente lo anterior. Sin
embargo, puede notar que si bien el texto de la consulta es el mismo, diferentes configuraciones dan como resultado
diferentes query_id s.

7.3 Búsqueda de valores de parámetros rastreados


Podemos usar Query Store para encontrar los valores de parámetros para los que se olió un plan. Esta consulta es un poco
más simple que la misma consulta contra el caché del plan, porque no tenemos que jugar con las compensaciones para
obtener el texto de la consulta. Pero la consulta todavía tiene que contar como compleja, porque necesitamos hacer el
mismo trabajo para extraer los valores de los parámetros del plan XML:
; CON datos basados ​
COMO (

SELECCIONE q.query_text_id, q.context_settings_id, p.query_plan, qt.query_sql_text,

last_compile_batch_offset_start / 2 COMO stmt_start,

charindex('<Lista de parámetros>', p.query_plan) + len('<Lista de parámetros>')

como paramstart,

charindex('</ParameterList>', p.query_plan) como parámetro

DESDE sys.query_store_query q

ÚNASE a sys.query_store_plan p EN q.query_id = p.query_id

ÚNASE a sys.query_store_query_text qt EN q.query_text_id = qt.query_text_id

DONDE q.object_id = object_id('dbo.List_orders_11')

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 54/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?
), siguiente_nivel AS (

SELECCIONE query_text_id, context_settings_id, query_plan, query_sql_text, stmt_start,

Inglés Español

CASO CUANDO paramend > paramstart

ENTONCES TRY_CAST(substring(query_plan, paramstart,

paramend - paramstart) AS xml)

TERMINAR COMO parámetros


DESDE datos basados

SELECCIONE convert(bigint, cs.set_options) COMO [SET], stmt_start COMO Pos,

n.query_sql_text AS Declaración,

CR.c.value('@Column', 'nvarchar(128)') como parámetro,

CR.c.value('@ParameterCompiledValue', 'nvarchar(128)') AS [Valor rastreado],

TRY_CAST (query_plan AS xml) AS [Query plan]

DESDE siguiente_nivel n

APLICACIÓN CRUZADA n.params.nodes('ColumnReference') COMO CR(c)

ÚNASE a sys.query_context_settings cs

ON n.context_settings_id = cs.context_settings_id

ORDENAR POR [SET], Pos, Parámetro

La consulta comienza con sys.query_store_query , que es la tabla principal del Almacén de consultas que define la
consulta y aquí podemos filtrar la identificación del objeto para el procedimiento que nos interesa. El texto de la consulta
se toma de sys.query_store_query_text , mientras que el plan de consulta está en sys.query_store_plan . Al igual que en
sys.dm_exec_text_query_plan, el XML del plan se almacena en una columna nvarchar(MAX) debido a la limitación en
el tipo de datos xml . Manejamos el XML exactamente de la misma manera que en la consulta original. En el SELECT
final , obtengo las opciones SET de sys.query_context_settings .

Este fue el caso de prueba que usamos anteriormente:


ACTIVAR ARITHABORT

EXEC List_orders_11 '19980101', 'ALFKI'

Vamos

DESACTIVAR ARITHABORT

EXEC List_orders_11 '19970101', 'BERGS'

El resultado de la consulta anterior es similar al resultado de la consulta original anterior en el artículo, pero encontrará
que el texto de la consulta también incluye la lista de parámetros.

Si desea encontrar los parámetros para un lote de SQL dinámico, debe reemplazar esta condición en los datos basados ​en
CTE
DONDE q.object_id = object_id('dbo.List_orders_11')

con una condición en alguna pieza adecuada en el texto de consulta. También es posible que desee filtrar la consulta contra
el propio Almacén de consultas. Por lo tanto, puede obtener algo como:
DONDE qt.query_sql_text COMO '%Orderdate >%'

Y qt.query_sql_text NO COMO '%query_store_query%'

Si tiene una consulta que, por alguna razón, se vuelve a compilar con frecuencia y, en ocasiones, obtiene un mal plan
debido a la detección, la consulta anterior puede devolver muchas filas. Para reducir el ruido, puede usar las vistas
sys.query_store_runtime_stats y sys.query_store_runtime_stats_interval para filtrar el resultado. Digamos que los
usuarios de la oficina de Nueva Delhi informaron que tuvieron un mal rendimiento entre las 09:00 y las 10:00, su hora
local. Puede agregar esta condición a los datos basados ​en CTE anteriores :
Y EXISTE (SELECCIONE *

DESDE sys.query_store_runtime_stats rs

ÚNASE a sys.query_store_runtime_stats_interval rsi

ENCENDIDO rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id

DONDE rs.plan_id = p.plan_id

Y rsi.end_time >= '20161125 09:00 +05:30'

Y rsi.start_time <= '20161125 10:00 +05:30')

La esencia aquí es que Query Store guarda estadísticas de tiempo de ejecución para un plan por intervalos. Una fila en
sys.query_store_runtime_stats contiene las estadísticas de un plan durante un intervalo y
sys.query_store_runtime_stats_interval contiene la duración de dicho intervalo. Debe observar que el tipo de datos de

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 55/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

las columnas start_time y end_time es datetimeoffset , razón por la cual he incluido el desplazamiento TZ en las
consultasInglés
anteriores. Tenga

en cuenta que si omite el desplazamiento TZ, sus valores se convertirán a UTC
Español
(desplazamiento +00:00) y es posible que no encuentre los datos que está buscando.

En lugar de filtrar por tiempo, puede filtrar en una de las muchas columnas en sys.query_store_runtime_stats para
seleccionar solo planes con un determinado consumo de recursos. Esto se deja como ejercicio para el lector.

7.4 Forzar planes con Query Store


Como mencioné, puede usar Query Store para forzar un plan. Esto es similar a las guías de planes, pero cuando fuerza un
plan a través de Query Store, esto no da como resultado una entrada en sys.plan_guides . También hay una diferencia en
cómo funciona esto con diferentes claves de caché. Este guión ilustra:
ALTER DATABASE Northwind SET QUERY_STORE CLEAR

ACTIVAR ARITHABORT

Vamos

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19990101'

Vamos

DECLARAR @query_id bigint,

@plan_id bigint,

@rowc int

SELECCIONE @query_id = q.query_id, @plan_id = p.plan_id

DESDE sys.query_store_query q

ÚNASE a sys.query_store_plan p EN q.query_id = p.query_id

ÚNASE a sys.query_store_query_text qt EN q.query_text_id = qt.query_text_id

DONDE qt.query_sql_text LIKE '%Orders WHERE OrderDate%'

Y qt.query_sql_text NO COMO '%query_store_query%'

SELECCIONE @rowc = @@rowcount

SI @filac = 1

EJECUTIVO sp_query_store_force_plan @query_id, @plan_id

MÁS

RAISERROR('%d filas encontradas en Query Store. No se puede forzar el plan', 16, 1, @rowc)

DESACTIVAR ARITHABORT

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19960101'

ACTIVAR ARITHABORT

EXEC sp_executesql N'SELECT * FROM dbo.Orders DONDE OrderDate > @orderdate',

N'@fechapedido fechahora', @fechapedido = '19960101'

SELECCIONE q.query_id, p.plan_id, p.is_forced_plan,

convertir (bigint, cs.set_options) como set_options,

qt.query_sql_text, try_cast(p.query_plan AS xml)

DESDE sys.query_store_query q

ÚNASE a sys.query_store_query_text qt EN q.query_text_id = qt.query_text_id

ÚNASE a sys.query_store_plan p EN q.query_id = p.query_id

ÚNASE a sys.query_context_settings cs

ON q.context_settings_id = cs.context_settings_id

DONDE qt.query_sql_text LIKE '%Orders WHERE OrderDate%'

Y qt.query_sql_text NO COMO '%query_store_query%'

EJECUTIVO sp_query_store_unforce_plan @query_id, @plan_id

A los efectos de la demostración, borro las tablas del Almacén de consultas. Luego ejecuto una consulta en la base de
datos de pedidos para la cual Index Seek + Key Lookup es el mejor plan con los parámetros dados. A continuación,
recupero los identificadores de consulta y plan para esta consulta. La verificación de @@rowcount es solo para
asegurarse de que la demostración se ejecute como se esperaba. Siguiendo esto, fuerzo este plan con
sp_query_store_force_plan . A continuación, vuelvo a ejecutar la consulta dos veces, una vez con ARITHABORT OFF y
otra vez con ARITHABORT ON . Tenga en cuenta que esta vez, especifico los valores de los parámetros para que se
devuelvan todos los pedidos y, por lo tanto, el mejor plan es un escaneo de índice agrupado .

Si observa los planes para las últimas dos consultas, encontrará que la primera ejecución con ARITHABORT OFF usa el CI
Scan , es decir, no se considera el plan forzado, mientras que la segunda ejecución sí usa el plan forzado. Por lo tanto,
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 56/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

cuando fuerza un plan con Query Store, solo lo fuerza para un conjunto particular de claves de caché. Esto es diferente a
cuando congelamos
Inglés unEspañol

plan con sp_create_plan_guide_from_handle ; en ese caso el plan forzado se aplicó también
cuando ARITHBORT estaba APAGADO .

Esto es seguido por una consulta de diagnóstico. Antes de mirar el resultado, podemos notar que es muy simple encontrar
si hay planes que se han forzado a través de Query Store: hay una columna is_forced_plan en sys.query_store_plan . Sin
embargo, si observamos el resultado de la consulta, tal vez no sea como cabría esperar. (Por razones de espacio, no incluyo
las dos últimas columnas):
query_id plan_id is_forced_plan set_options contar_ejecuciones
----------- --------- -------------- -------------- -- ------------------
4 4 0 251 1
1 1 1 4347 1
1 5 0 4347 1

Hay tres filas en la salida, no una, a pesar de que ejecutamos la misma consulta tres veces. Como aprendimos antes, la
configuración de contexto (o los atributos del plan) están vinculados a query_id . Por lo tanto, la ejecución con
set_options = 251 ( ARITHABORT OFF ) tiene un query_id diferente . Y dado que forzamos el plan para un query_id
específico , esto explica por qué Seek + Lookup no se aplicó para ARITHABORT OFF .

Queda por entender por qué hay dos entradas para query_id = 1. Podemos suponer que la que tiene plan_id = 5 proviene
de la segunda ejecución, ya que presumiblemente plan_id se asigna secuencialmente desde 1 después de borrar Query
Store. Pero, ¿por qué hay un nuevo plan, cuando forzamos plan_id = 1? Cuando forcé el plan, el plan se salió de la
memoria caché, ya que Query Store no verifica si estoy forzando el plan actual o un plan anterior. Por esta razón, hubo una
nueva compilación cuando luego se volvió a ejecutar la consulta con ARITHABORT ON. Cuando se fuerza un plan, SQL
Server no solo toma ese plan al pie de la letra, ya que es posible que no sea un plan utilizable. (Por ejemplo, podría
referirse a un índice que se ha descartado). En cambio, el optimizador genera planes hasta que genera el plan forzado, o un
plan que es muy similar al plan original.

Si guarda el XML de los planes en el disco, puede usar su herramienta de comparación favorita para compararlos. Usé
Beyond Compare , y este es un extracto de lo que vi para plan_id = 5:

He resaltado las diferencias pertinentes. Primero observe la apariencia del atributo UsePlan="1"que está ausente en plan_id
1. Esto indica que el plan es de hecho un plan forzado. (A través de Query Store, una guía de plan o el uso explícito de la
sugerencia USE PLAN ). Lo que también es interesante es la aparición del atributo WithUnorderedPrefetch que no está
presente en el plan realmente forzado. Es decir, este plan tiene una implementación diferente de Nested Loop Joinoperador
del plan original y también tiene una referencia de columna que no está en el primer plan. Es decir, los planes 1 y 5 no son
exactamente iguales. O, de manera más general, cuando fuerza un plan, es posible que no obtenga exactamente el mismo
plan que forzó, pero obtendrá un plan, como me lo describió un desarrollador de Microsoft, que es moralmente equivalente
al plan original.

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 57/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

En general, le recomiendo que use el plan forzado en Query Store en lugar de las guías de planes, ya que es más fácil de
usar queInglés
las guías de planes.
Español

Por otra parte, con Query Store solo puede forzar un plan específico, mientras que con las
guías de planes puede inyectar una pista, pero dejar cierto grado de libertad al optimizador.

7.5 Conclusión sobre el almacén de consultas


Antes de dejar el tema de Query Store, debo agregar que hay una GUI para Query Store en SQL Server Management
Studio donde puede investigar cuáles son las consultas costosas, etc., y también puede forzar planes desde esta UI. Dejo al
lector que explore estas opciones por su cuenta.

Si desea obtener más información sobre el Almacén de consultas, puede leer el tema Supervisión del rendimiento mediante
el Almacén de consultas en los Libros en pantalla. Para una discusión más profunda y prolongada, puede interesarle una
serie de artículos sobre Query Store de Data Platform Enrico van der Laar publicados en Simple Talk .

8. Observaciones finales
Ahora ha aprendido por qué una consulta puede funcionar de manera diferente en la aplicación y en SSMS. También ha
visto varias formas de abordar los problemas con la detección de parámetros.

¿Incluí todas las razones por las que podría haber una diferencia de rendimiento entre la aplicación y SSMS? No todos, sin
duda hay algunas razones más. Por ejemplo, si la aplicación se ejecuta en una máquina remota y ejecuta SSMS
directamente en el servidor, una red lenta puede marcar una gran diferencia. Pero creo que he capturado las razones más
probables que se deben a circunstancias dentro de SQL Server.

¿Incluí todas las razones posibles por las que el rastreo de parámetros puede darte un mal plan y cómo debes abordarlo?
Probablemente no. Hay espacio para más variaciones allí y, en particular, no puedo saber qué está haciendo su aplicación.
Pero espero que algunos de los métodos que he presentado aquí puedan ayudarlo a encontrar una solución.

Si cree que se ha topado con algo que no está cubierto en el artículo, pero cree que debería haberlo incluido, escríbame a
esquel@sommarskog.se . Y lo mismo se aplica si ve algún error en el artículo, ni los más mínimos errores ortográficos o
gramaticales. Por otro lado, si tiene una pregunta sobre cómo resolver un problema en particular, le recomiendo que
publique una pregunta en un foro de SQL Server, porque mucha más gente verá su pregunta. Si quiere que vea la pregunta,
lo mejor que puede hacer es publicar en el foro de preguntas y respuestas de Microsoft Docs con la etiqueta
sql-server-general .

8.1 Otras lecturas


Si desea obtener más información sobre la compilación de consultas, estadísticas, etc., a continuación hay algunos
artículos que me gustaría recomendar. Puede notar que todos se relacionan con SQL 2008 o SQL 2005. Esto se debe a que
armé la lista de artículos en ese período de tiempo. No he investigado si son versiones más nuevas de estos artículos. Pero
incluso si tiene una versión más nueva de SQL Server, los artículos siguen siendo aplicables en gran medida.

Estadísticas utilizadas por el optimizador de consultas en Microsoft SQL Server 2008 : un documento técnico escrito por
Eric Hanson y Yavor Angelov del equipo de SQL Server.

Planifique el almacenamiento en caché en SQL Server 2008 : un documento técnico escrito por el MVP de SQL Server,
Greg Low. El Apéndice A de este documento detalla las reglas para la parametrización simple.

Solución de problemas de rendimiento en SQL Server 2008 : un documento extenso que analiza el rendimiento desde
varios ángulos, no solo el ajuste de consultas. Escrito por varios desarrolladores y gente de CSS de Microsoft.

Forzar planes de consulta : un libro blanco sobre guías de planes. Esta versión es para SQL 2005; No he visto ninguna
versión para SQL 2008.

9. Revisiones
2021-04-12
Se agregó la sección El Efecto de las Transacciones , basada en una experiencia que tuvo Daniel López Atán.
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 58/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

2021-01-25
Una revisión menor
Inglés

del artículo para mantenerlo actualizado.
Español
Capturas de pantalla actualizadas para que sean de SSMS 18.8 e incluyan recuentos de filas reales y estimados.
Se agregó una breve discusión sobre la inserción de funciones escalares en la sección ¿Qué es un procedimiento
almacenado?
Se agregó un valor más al script setopions.sql .
Creó la sección Recompilación de declaraciones de variables de tabla y parámetros con valores de tabla debido
al nuevo comportamiento con variables de tabla en SQL 2019.
Reescribió la sección Vistas indexadas y similares para centrarse en lo que es relevante para las versiones
modernas de SQL Server.
En la sección Obtención de los planes y parámetros de consulta con Management Studio , ahora menciono que
puede ver los tiempos de ejecución por operador en el plan gráfico.
Se actualizó la sección Plan de consulta en vivo y se agregó la sección Obtener el plan de ejecución real más
reciente .
Se actualizó la consulta en Obtención de definiciones de tabla e índice para que ahora devuelva información sobre
todo tipo de índices.
Se agregó una mención del DMV sys.dm_db_stats_histogram en la sección Búsqueda de información sobre
estadísticas .
Se corrigió un error en la subsección Forzar un Índice Diferente . Sugerí que cuando fuerza dos índices, SQL
Server tenía la opción de qué índice usar. Ese no es el caso. SQL Server está obligado a usar ambos.
A esto vienen otras adiciones y modificaciones menores, así como supresiones de pasajes relacionados con
versiones y circunstancias antiguas.

2020-08-21
Se agregó un párrafo a la sección El significado del esquema predeterminado para señalar que se necesita una
notación de dos partes para todos los objetos que pertenecen a un esquema.

2019-10-26
Se agregó la sección Configuración de la base de datos para analizar una situación en la que puede obtener un
rendimiento diferente por razones no relacionadas con la detección de parámetros.

2018-12-29
Un par de correcciones / adiciones menores.
En un lugar, sugerí que ANSI_PADDING afecta las columnas nvarchar . Eso es incorrecto, se aplica solo a
varchar y varbinary .
En la sección Los efectos de la recompilación de sentencias, se agregó una nota relacionada con que a partir de
SQL 2019, SQL Server ahora difiere la compilación de sentencias con variables de tabla.
Se agregó una nueva sección Plan de consulta en vivo para discutir cómo puede obtener planes con valores reales
parciales de una consulta en ejecución.
Algunas modificaciones menores de la sección Obtención de planes de consulta y parámetros de un seguimiento
incluyen la eliminación de la nota anterior y la adición de una nueva sobre un nuevo evento extendido,
query_plan_profile .

 
2018-08-29
Actualizada la sección ¿Podría ser MARS? después de la entrada de Nick Smith. Parece que ahora tenemos una
respuesta de por qué MARS puede hacer que las cosas funcionen más lentamente: es la latencia de la red.

2017-12-05
Tres actualizaciones menores:
Se agregó texto en la sección Poner el plan de consulta en el caché que ACTUALIZAR ESTADÍSTICAS solo
desencadena una recompilación si los datos han cambiado.
Se agregó una nota a la sección Configuración predeterminada para comentar un pasaje misterioso en el tema de
SET ARITHABORT que Daniel Berber me informó.
Actualizada la sección ¿Podría ser MARS? después de que Allen Firth informara que él también había
experimentado esto.
https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 59/60
2/11/22, 16:46 ¿Lento en la aplicación, rápido en SSMS?

2017-09-20
SeInglés
agregó una sección
Español
 ser MARS? , inspirado en una observación de Lonny Niederstadt.
¿Podría

2017-07-09
Gordon Griffin señaló un error en la sección Búsqueda de información sobre estadísticas . Sugerí que siempre
podría usar el nombre de la columna como argumento para DBCC SHOW_STATISTICS , pero esto solo es cierto si la
columna tiene estadísticas creadas automáticamente. No funciona si hay estadísticas creadas por el usuario en la
columna.

2016-12-18
Una revisión general del artículo para reflejar que es 2016, lo que significa que parte del texto relacionado con SQL
2000 y SQL 2005 se ha reducido o eliminado. Las siguientes secciones se han modificado en mayor medida o se han
añadido:
Un problema con los servidores vinculados . Debido a la introducción de la seguridad de nivel de fila en SQL
2016, es posible que vuelva a enfrentarse a situaciones en las que obtenga malos planes, ya que los usuarios con
pocos privilegios no pueden ejecutar DBCC SHOW_STATISTICS en una tabla de la consulta.
El capítulo sobre SQL dinámico ha tenido una revisión mayor que el resto del texto con algunos cambios en el
material.
Siguiendo una sugerencia de Andrew Morton, ahora señalo la virtud de especificar la longitud del parámetro en
una llamada desde .NET.
La discusión sobre la parametrización automática se reescribe por completo. La versión anterior sugería
incorrectamente que podría tener problemas de rastreo de parámetros con una parametrización simple, pero
esto es muy poco probable.
En la sección sobre guías de planes y congelación de planes , agregué una consulta para encontrar guías de
planes activas en el caché de planes.
Hay un nuevo capítulo sobre cómo usar Query Store, una nueva característica introducida en SQL 2016, para
obtener información para solucionar problemas de rastreo de parámetros.

2013-08-30
Corregido un error en la configuración de funciones que Alex Valen tuvo la amabilidad de señalar.

2013-04-14
Se actualizó la sección Un problema con los servidores vinculados para reflejar que este problema se eliminó con
SQL 2012 SP1. También se agregó una nota sobre SET NO_BROWSETABLE ON en la sección La historia hasta
ahora .

2011-12-31
Se agregó texto sobre el Agente SQL Server que se ejecuta con QUOTED_IDENTIFIER OFF de forma
predeterminada. Se amplió la sección sobre Guías de planes para cubrir también la congelación de planes.

2011-11-27
Varios de los enlaces en la sección Lecturas adicionales estaban rotos. Esto ha sido arreglado.

2011-07-02
Ahora hay una traducción al ruso disponible. ¡Felicitaciones a Dima Piliugin por este trabajo!

2011-06-25
Se agregó una sección Un problema con los servidores vinculados para discutir un problema especial que puede
causar la situación Lento en la aplicación, rápido en SSMS.

2011-02-20
Primera versión.

Volver a mi página de inicio .

https://www-sommarskog-se.translate.goog/query-plan-mysteries.html?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es-419&_x_tr_pto=sc 60/60

También podría gustarte