Cada lote causa una compilación

10

Tenemos una aplicación de terceros que envía sentencias T-SQL en lotes.

La base de datos está alojada en un SQL Server 2016 Enterprise SP1 CU7, 16 núcleos y 256 GB de memoria. Optimizar para Ad-Hoc está habilitado.

Este es un ejemplo ficticio de las consultas que se están ejecutando:

exec sp_executesql N'
IF @@TRANCOUNT = 0 SET TRANSACTION ISOLATION LEVEL SNAPSHOT

select field1, field2 from table1 where field1=@1
option(keep plan, keepfixed, loop join)

select field3, field4 from table2 where field3=@1
option(keep plan, keepfixed, loop join)', N'@1 nvarchar(6)',@1=N'test'

Cuando monitorizo ​​la base de datos y miro lotes / seg y compila / seg, noto que siempre son iguales. Bajo carga pesada, esto puede ser 1000 lotes / seg y 1000 compilaciones / seg. Bajo carga promedio, hay 150 lotes / seg.

Analizo el caché de consultas para planes compilados recientemente:

SELECT TOP (1000) qs.creation_time
    , DatabaseName = DB_NAME(st.dbid)
    , qs.execution_count
    , st.text
    , qs.plan_handle
    , qs.sql_handle
    , qs.query_hash 
FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.plan_handle) AS st
ORDER BY creation_time DESC;

Cuando ejecuto la consulta anterior, solo veo 10-20 nuevos planes de consulta / seg.

Es como si cada sp_executesqlllamada desencadenara una compilación, pero el plan de consulta no está en caché.

¿Cuál puede ser la causa de que los lotes / seg sean iguales a las compilaciones / seg?

Frederik Vanderhaegen
fuente

Respuestas:

12

Es como si cada sp_executesqlllamada desencadenara una compilación pero el plan de consulta no se almacena en caché.

SQL Server no almacena en caché un plan de consulta para lotes que solo contienen una sp_executesqlllamada. Sin un plan en caché, se produce una compilación cada vez. Esto es por diseño y esperado.

SQL Server evita el almacenamiento en caché de lotes con un bajo costo de compilación. Los detalles de lo que está y no está en caché ha cambiado muchas veces a lo largo de los años. Vea mi respuesta a Trace flag 2861 y lo que realmente significa un plan de "costo cero" para más detalles.

En resumen, la probabilidad de reutilización (incluidos los valores de parámetros específicos) es pequeña, y el costo de compilar el texto ad hoc que contiene la sp_executesqlllamada es muy pequeño. El lote parametrizado interno producido por, sp_executesqlpor supuesto, se almacena en caché y se reutiliza; este es su valor. El procedimiento almacenado extendido en sp_executesqlsí también se almacena en caché.

Para ser almacenado en caché y reutilizado, la sp_executesqldeclaración tendría que ser parte de un lote más grande que se considera que vale la pena almacenar en caché. Por ejemplo:

-- Show compilation counter
SELECT
    DOPC.[object_name],
    DOPC.cntr_value
FROM sys.dm_os_performance_counters AS DOPC
WHERE
    DOPC.counter_name = N'SQL Compilations/sec'
GO
-- This is only here to make the batch worth caching
DECLARE @TC integer =
(
    SELECT TOP (1) @@TRANCOUNT 
    FROM master.dbo.spt_values AS SV
);

-- Example call we are testing
-- (use anything for the inner query, this example uses the Stack Overflow database
EXECUTE sys.sp_executesql 
    N'SELECT LT.Type FROM dbo.LinkTypes AS LT WHERE LT.Id = @id;', 
    N'@id int', 
    @id = 1;
GO
-- Show compilation counter again
SELECT
    DOPC.[object_name],
    DOPC.cntr_value
FROM sys.dm_os_performance_counters AS DOPC
WHERE
    DOPC.counter_name = N'SQL Compilations/sec'

Ejecute ese código varias veces. Sin embargo, la primera vez, muchas recopilaciones se informan como se esperaba. La segunda vez, no se informan compilaciones, a menos que optimize for ad hoc workloadsesté habilitado (por lo que solo se almacena en caché un trozo de plan compilado ). En la tercera vez, no se informan compilaciones en ningún caso, ya que cualquier trozo se promociona a un plan ad hoc totalmente en caché.

Elimine la DECLARE @TCinstrucción para ver que la sys.sp_executesqlinstrucción nunca se almacena en caché sin ella, independientemente de la cantidad de veces que se ejecute.

Ver las entradas de caché del plan asociado con:

-- Show cached plans
SELECT
    DECP.refcounts,
    DECP.usecounts,
    DECP.size_in_bytes,
    DECP.cacheobjtype,
    DECP.objtype,
    DECP.plan_handle,
    DECP.parent_plan_handle,
    DEST.[text]
FROM sys.dm_exec_cached_plans AS DECP
CROSS APPLY sys.dm_exec_sql_text(DECP.plan_handle) AS DEST
WHERE 
    DEST.[text] LIKE N'%sp_executesql%'
    AND DEST.[text] NOT LIKE N'%dm_exec_cached_plans%';

Preguntas y respuestas relacionadas: ¿Los disparadores se compilan cada vez?

Paul White 9
fuente
11

Puede aproximar lo que ve en Monitor de rendimiento y Monitor de actividad SQL Compilations/secy Batch Requests/sec, mientras ejecuta algunos lotes en una ventana de consulta separada como prueba, como se detalla a continuación.

Ventana de consulta 1:

DECLARE @t1 datetime;
DECLARE @t2 datetime;
DECLARE @CompVal1 int;
DECLARE @CompVal2 int;
DECLARE @ReCompVal1 int;
DECLARE @ReCompVal2 int;
DECLARE @BatchVal1 int;
DECLARE @BatchVal2 int;
DECLARE @ElapsedMS decimal(10,2);

SELECT @t1 = GETDATE()
    , @CompVal1 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'SQL Compilations/sec                                                                                                            '
        )
    , @ReCompVal1 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'SQL Re-Compilations/sec                                                                                                         '
        )
    , @BatchVal1 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'Batch Requests/sec                                                                                                              '
        );

WAITFOR DELAY '00:00:10.000';

SELECT @t2 = GETDATE()
    , @CompVal2 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'SQL Compilations/sec                                                                                                            '
        )
    , @ReCompVal2 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'SQL Re-Compilations/sec                                                                                                         '
        )
    , @BatchVal2 = (
        SELECT spi.cntr_value
        FROM sys.sysperfinfo spi
        WHERE spi.counter_name = 'Batch Requests/sec                                                                                                              '
        );

SET @ElapsedMS = DATEDIFF(MILLISECOND, @t1, @t2);
SELECT  ElapsedTimeMS = @ElapsedMS
    , [SQL Compilations/sec] = (@CompVal2 - @CompVal1) / @ElapsedMS * 1000 
    , [SQL Recompilations/sec] = (@ReCompVal2 - @ReCompVal1) / @ElapsedMS * 1000
    , [Batch Requests/sec] = (@BatchVal2 - @BatchVal1) / @ElapsedMS * 1000;

En la Ventana de consulta 2, ejecute lo siguiente mientras se ejecuta el código anterior. El código simplemente ejecuta 100 lotes T-SQL:

EXEC sys.sp_executesql N'SELECT TOP(1) o.name FROM sys.objects o;';
GO 100

Si vuelve a la Ventana de consulta 1, verá algo como esto:

╔═══════════════╦══════════════════════╦══════════ ══════════════╦════════════════════╗
║ ElapsedTimeMS ║ Compilaciones SQL / seg ║ Recompilaciones SQL / seg ║ Solicitudes por lotes / seg ║
╠═══════════════╬══════════════════════╬══════════ ══════════════╬════════════════════╣
║ 10020.00 ║ 10.07984031000 ║ 0.00000000000 ║ 10.07984031000 ║
╚═══════════════╩══════════════════════╩══════════ ══════════════╩════════════════════╝

Si miramos esta consulta:

SELECT dest.text
    , deqs.execution_count
FROM sys.dm_exec_query_stats deqs
    CROSS APPLY sys.dm_exec_sql_text(deqs.plan_handle) dest
WHERE dest.text LIKE 'SELECT TOP(1)%'

Podemos confirmar que hubo 100 ejecuciones de la consulta de prueba.

En los resultados anteriores, puede ver que estamos obteniendo compilaciones cada vezsp_executesql que se ejecuta la declaración. El plan para eso ciertamente se está almacenando en caché, pero vemos una compilación para ello; ¿lo que da?

Los documentos de Microsoft dicen esto sobre sp_executesql:

sp_executesql tiene el mismo comportamiento que EXECUTE con respecto a los lotes, el alcance de los nombres y el contexto de la base de datos. La instrucción o lote de Transact-SQL en el parámetro sp_executesql @stmt no se compila hasta que se ejecuta la instrucción sp_executesql. Los contenidos de @stmt se compilan y ejecutan como un plan de ejecución separado del plan de ejecución del lote que se llama sp_executesql.

Por lo tanto, sp_executesql se está recopilando cada vez que se ejecuta, incluso si el plan para el texto de comando ya está en la caché del plan. @PaulWhite muestra en su respuesta que la mayoría de las llamadas a sp_executesql no se almacenan en caché.

Max Vernon
fuente