Cada lote causa uma compilação

10

Temos um aplicativo de terceiros que envia instruções T-SQL em lotes.

O banco de dados está hospedado em um SQL Server 2016 Enterprise SP1 CU7, 16 núcleos e 256 GB de memória. Otimizar para Ad-Hoc está ativado.

Este é um exemplo fictício das consultas que estão sendo executadas:

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'

Quando monitoro o banco de dados e vejo lotes / s e compilações / s, percebo que eles são sempre os mesmos. Sob carga pesada, isso pode ser de 1000 lotes / s e 1000 compilações / s. Sob carga média, existem 150 lotes / s.

Analiso o cache de consultas para planos compilados recentemente:

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;

Quando executo a consulta acima, vejo apenas 10 a 20 novos planos de consulta / s.

É como se todas as sp_executesqlchamadas disparassem uma compilação, mas o plano de consulta não fosse armazenado em cache.

Qual pode ser a causa de lotes / s serem iguais a compilações / s?

Frederik Vanderhaegen
fonte

Respostas:

12

É como se todas as sp_executesqlchamadas acionassem uma compilação, mas o plano de consulta não fosse armazenado em cache.

O SQL Server não armazena em cache um plano de consulta para lotes que contêm apenas uma sp_executesqlchamada. Sem um plano em cache, uma compilação ocorre sempre. Isso ocorre por design e é esperado.

O SQL Server evita o armazenamento em cache de lotes com baixo custo de compilação. Os detalhes do que é e não é armazenado em cache mudaram muitas vezes ao longo dos anos. Veja minha resposta ao sinalizador de rastreamento 2861 e o que realmente significa um plano de 'custo zero' para obter detalhes.

Em resumo, a probabilidade de reutilização (incluindo valores de parâmetros específicos) é pequena e o custo de compilar o texto ad hoc que contém a sp_executesqlchamada é muito pequeno. O lote parametrizado interno produzido por sp_executesqlé, obviamente, armazenado em cache e reutilizado - esse é o valor dele. O sp_executesqlpróprio procedimento armazenado estendido também é armazenado em cache.

Para ser armazenada em cache e reutilizada, a sp_executesqlinstrução precisaria fazer parte de um lote maior que é considerado digno de armazenamento em cache. Por exemplo:

-- 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'

Execute esse código várias vezes. Na primeira vez, porém, muitas compilações são relatadas como esperado. Na segunda vez, nenhuma compilação é relatada, a menos que optimize for ad hoc workloadsesteja ativada (portanto, apenas um Stub de Plano Compilado é armazenado em cache). Na terceira vez, nenhuma compilação é relatada em nenhum caso, pois qualquer stub é promovido para um plano ad hoc totalmente em cache.

Remova a DECLARE @TCinstrução para ver que sys.sp_executesqlela nunca é armazenada em cache sem ela, independentemente do número de vezes que é executada.

Visualize as entradas de cache do plano associado com:

-- 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%';

Perguntas e respostas relacionadas: Os gatilhos são compilados toda vez?

Paul White 9
fonte
11

Você pode aproximar o que vê no Monitor de desempenho e no Monitor de atividades SQL Compilations/sece Batch Requests/sec, enquanto executa alguns lotes na janela de consulta separada como teste, conforme detalhado abaixo.

Janela 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;

Na janela de consulta 2, execute o seguinte enquanto o código acima estiver em execução. O código simplesmente executa 100 lotes T-SQL:

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

Se você voltar para a Janela de consulta 1, verá algo assim:

╔═══════════════╦══════════════════════╦══════════ ══════════════╦════════════════════╗
║ ElapsedTimeMS Comp Compilações SQL / s ║ Recompilações SQL / s ║ Solicitações em lote / s ║
╠═══════════════╬══════════════════════╬══════════ ══════════════╬════════════════════╣
10020,00 ║ 10,07984031000 ║ 0,00000000000 ║ 10,07984031000 ║
╚═══════════════╩══════════════════════╩══════════ ══════════════╩════════════════════╝

Se olharmos para 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 houve 100 execuções da consulta de teste.

Nos resultados acima, você pode ver que estamos recebendo compilações cada vez que a sp_executesqlinstrução é executada. O plano para isso certamente está sendo armazenado em cache, mas vemos uma compilação para ele; o que da?

O Microsoft Docs diz o seguinte sobre sp_executesql:

sp_executesql tem o mesmo comportamento que EXECUTE em relação a lotes, o escopo de nomes e o contexto do banco de dados. A instrução Transact-SQL ou lote no parâmetro sp_executesql @stmt não é compilada até que a instrução sp_executesql seja executada. O conteúdo do @stmt é então compilado e executado como um plano de execução separado do plano de execução do lote chamado sp_executesql.

Portanto, sp_executesql ele próprio está sendo compilado toda vez que é executado, mesmo que o plano para o texto do comando já esteja no cache do plano. @PaulWhite mostra em sua resposta que a maioria das chamadas para sp_executesql não é, de fato, armazenada em cache.

Max Vernon
fonte