sql >> Base de Datos >  >> RDS >> Database

Un caso de uso para sp_prepare / sp_prepexec

Hay características que muchos de nosotros evitamos, como cursores, disparadores y SQL dinámico. No hay duda de que cada uno tiene sus casos de uso, pero cuando vemos un activador con un cursor dentro de SQL dinámico, puede hacernos temblar (triple golpe).

Las guías de planes y sp_prepare están en un bote similar:si me vieras usando uno de ellos, levantarías una ceja; si me vieras usándolos juntos, probablemente revisarías mi temperatura. Pero, al igual que los cursores, los disparadores y el SQL dinámico, tienen sus casos de uso. Y recientemente me encontré con un escenario en el que usarlos juntos era beneficioso.

Antecedentes

Tenemos muchos datos. Y muchas aplicaciones que se ejecutan contra esos datos. Algunas de esas aplicaciones son difíciles o imposibles de cambiar, en particular las aplicaciones estándar de un tercero. Entonces, cuando su aplicación compilada envía consultas ad hoc a SQL Server, particularmente como una declaración preparada, y cuando no tenemos la libertad de agregar o cambiar índices, varias oportunidades de ajuste quedan descartadas de inmediato.

En este caso, teníamos una tabla con un par de millones de filas. Una versión simplificada y desinfectada:

CREATE TABLE dbo.TheThings
(
  ThingID    bigint NOT NULL,
  TypeID     uniqueidentifier NOT NULL,
  dt1        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt2        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt3        datetime NOT NULL DEFAULT sysutcdatetime(),
  CONSTRAINT PK_TheThings PRIMARY KEY (ThingID)
);
 
CREATE INDEX ix_type ON dbo.TheThings(TypeID);
 
SET NOCOUNT ON;
GO
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1) 2500, @guid2
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;

La declaración preparada de la aplicación se veía así (como se ve en el caché del plan):

(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0

El problema es que, para algunos valores de TypeID , habría muchos miles de filas. Para otros valores, habría menos de 10. Si se elige (y reutiliza) el plan incorrecto en función de un tipo de parámetro, esto puede ser un problema para los demás. Para la consulta que recupera un puñado de filas, queremos una búsqueda de índice con búsquedas para recuperar las columnas adicionales no cubiertas, pero para la consulta que devuelve 700 000 filas, solo queremos una exploración de índice agrupado. (Idealmente, el índice cubriría, pero esta vez esta opción no estaba en las tarjetas).

En la práctica, la aplicación siempre obtenía la variación de escaneo, aunque esa era la que se necesitaba aproximadamente el 1% del tiempo. El 99% de las consultas usaban un escaneo de 2 millones de filas cuando podrían haber usado una búsqueda + 4 o 5 búsquedas.

Podríamos reproducir esto fácilmente en Management Studio ejecutando esta consulta:

DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO
 
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO

Los planes volvieron así:

La estimación en ambos casos fue de 1000 filas; las advertencias de la derecha se deben a E/S residuales.

¿Cómo podríamos asegurarnos de que la consulta tomó la decisión correcta según el parámetro? Tendríamos que volver a compilarlo, sin agregar sugerencias a la consulta, activar indicadores de rastreo ni cambiar la configuración de la base de datos.

Si ejecuté las consultas de forma independiente usando OPTION (RECOMPILE) , obtendría la búsqueda cuando corresponda:

DBCC FREEPROCCACHE;
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE);
SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);

Con RECOMPILE, obtenemos estimaciones más precisas y una búsqueda cuando la necesitamos.

Pero, nuevamente, no pudimos agregar la sugerencia a la consulta directamente.

Probemos una guía de planes

Mucha gente advierte contra las guías de planes, pero aquí estábamos como en un rincón. Definitivamente preferiríamos cambiar la consulta, o los índices, si pudiéramos. Pero esta podría ser la siguiente mejor opción.

EXEC sys.sp_create_plan_guide   
  @name   = N'TheThingGuide',
  @stmt   = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0',
  @type   = N'SQL',
  @params = N'@P0 varchar(8000)',
  @hints  = N'OPTION (RECOMPILE)';

Parece sencillo; probarlo es el problema. ¿Cómo simulamos una declaración preparada en Management Studio? ¿Cómo podemos estar seguros de que la aplicación obtiene el plan guiado y que se debe explícitamente a la guía del plan?

Si tratamos de simular esta consulta en SSMS, esto se trata como una declaración ad hoc, no como una declaración preparada, y no pude obtener esto para recoger la guía del plan:

DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier
SELECT * FROM dbo.TheThings WHERE TypeID = @P0

SQL dinámico tampoco funcionó (esto también se trató como una declaración ad hoc):

DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', 
        @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier
        @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
EXEC sys.sp_executesql @sql, @params, @P0;

Y no pude hacer esto, porque tampoco tomaría la guía del plan (la parametrización se hace cargo aquí, y no tenía la libertad de cambiar la configuración de la base de datos, incluso si esto se tratara como una declaración preparada) :

SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';

No puedo verificar el caché del plan para las consultas que se ejecutan desde la aplicación, ya que el plan almacenado en caché no indica nada sobre el uso de la guía del plan (SSMS inyecta esa información en el XML cuando genera un plan real). Y si la consulta realmente observa la sugerencia RECOMPILE que estoy pasando a la guía del plan, ¿cómo podría ver alguna evidencia en el caché del plan de todos modos?

Probemos sp_prepare

He usado sp_prepare menos en mi carrera que las guías de planes y no recomendaría usarlo para el código de la aplicación. (Como señala Erik Darling, la estimación se puede extraer del vector de densidad, no de olfatear el parámetro).

En mi caso, no quiero usarlo por motivos de rendimiento, quiero usarlo (junto con sp_execute) para simular la instrucción preparada que proviene de la aplicación.

DECLARE @o int;
EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)',
     N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0';
 
EXEC sys.sp_execute @o,  'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan
EXEC sys.sp_execute @o,  'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup

SSMS nos muestra que la guía del plan se usó en ambos casos.

No podrá comprobar la memoria caché del plan para obtener estos resultados debido a la recompilación. Pero en un escenario como el mío, debería poder ver los efectos en el monitoreo, verificar explícitamente a través de eventos extendidos u observar el alivio del síntoma que lo hizo investigar esta consulta en primer lugar (solo tenga en cuenta que el tiempo de ejecución promedio, la consulta las estadísticas, etc. pueden verse afectadas por una compilación adicional).

Conclusión

Este fue un caso en el que una guía de plan fue beneficiosa y sp_prepare fue útil para validar que funcionaría para la aplicación. Estos no suelen ser útiles, y menos a menudo juntos, pero para mí fue una combinación interesante. Incluso sin la guía del plan, si desea usar SSMS para simular una aplicación que envía declaraciones preparadas, sp_prepare es su amigo. (Consulte también sp_prepexec, que puede ser un atajo si no está tratando de validar dos planes diferentes para la misma consulta).

Tenga en cuenta que este ejercicio no fue necesariamente para obtener un mejor rendimiento todo el tiempo, sino para aplanar la variación del rendimiento. Obviamente, las recompilaciones no son gratuitas, pero pagaré una pequeña penalización para que el 99 % de mis consultas se ejecuten en 250 ms y el 1 % se ejecute en 5 segundos, en lugar de quedarme con un plan que es absolutamente terrible para el 99 % de las consultas. o el 1% de las consultas.