Chaque lot provoque une compilation

10

Nous avons une application tierce qui envoie des instructions T-SQL par lots.

La base de données est hébergée sur un serveur SQL Server 2016 Enterprise SP1 CU7, 16 cœurs et 256 Go de mémoire. Optimiser pour ad hoc est activé.

Voici un exemple factice des requêtes en cours d'exécution:

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'

Lorsque je surveille la base de données et que je regarde les lots / s et les compilations / s, je remarque qu'ils sont toujours les mêmes. Sous forte charge, cela peut être 1000 lots / sec et 1000 compilations / sec. Sous charge moyenne, il y a 150 lots / sec.

J'analyse le cache de requêtes pour les plans récemment compilés:

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;

Lorsque je lance la requête ci-dessus, je ne vois que 10 à 20 nouveaux plans de requête / s.

C'est comme si chaque sp_executesqlappel déclenche une compilation, mais le plan de requête n'est pas mis en cache.

Quelle peut être la cause des lots / s égaux aux compilations / s?

Frederik Vanderhaegen
la source

Réponses:

12

C'est comme si chaque sp_executesqlappel déclenche une compilation, mais le plan de requête n'est pas mis en cache.

SQL Server ne met pas en cache un plan de requête pour les lots contenant uniquement un sp_executesqlappel. Sans plan mis en cache, une compilation se produit à chaque fois. C'est par conception, et attendu.

SQL Server évite la mise en cache des lots avec un faible coût de compilation. Les détails de ce qui est et n'est pas mis en cache ont changé de nombreuses fois au fil des ans. Voir ma réponse à l' indicateur Trace 2861 et ce que signifie réellement un plan «zéro coût» pour plus de détails.

En bref, la probabilité de réutilisation (y compris les valeurs de paramètres spécifiques) est faible et le coût de compilation du texte ad hoc contenant l' sp_executesqlappel est très faible. Le lot paramétré interne produit par sp_executesqlest bien sûr mis en cache et réutilisé - c'est sa valeur. La procédure stockée étendue sp_executesqlelle-même est également mise en cache.

Pour être mise en cache et réutilisée, l' sp_executesqlinstruction doit faire partie d'un lot plus important qui est considéré comme méritant d'être mis en cache. Par exemple:

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

Exécutez ce code plusieurs fois. Cependant, la première fois, de nombreuses compilations sont signalées comme prévu. La deuxième fois, aucune compilation n'est signalée, sauf si elle optimize for ad hoc workloadsest activée (donc seul un talon de plan compilé est mis en cache). La troisième fois, aucune compilation n'est signalée dans tous les cas, car tout stub est promu dans un plan ad hoc entièrement mis en cache.

Supprimez l' DECLARE @TCinstruction pour voir qu'elle sys.sp_executesqln'est jamais mise en cache sans elle, quel que soit le nombre d'exécutions.

Affichez les entrées de cache de plan associées avec:

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

Questions et réponses connexes: les déclencheurs se compilent-ils à chaque fois?

Paul White 9
la source
11

Vous pouvez approximer ce que vous voyez dans l'Analyseur de performances et l'Analyseur d'activité pour SQL Compilations/secet Batch Requests/sec, tout en exécutant certains lots dans une fenêtre de requête distincte comme test, comme détaillé ci-dessous.

Fenêtre de requête 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;

Dans la fenêtre de requête 2, exécutez ce qui suit pendant l'exécution du code ci-dessus. Le code exécute simplement 100 lots T-SQL:

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

Si vous revenez à la fenêtre de requête 1, vous verrez quelque chose comme ceci:

╔═══════════════╦══════════════════════╦══════════ ══════════════╦════════════════════╗
║ ElapsedTimeMS ║ Compilations SQL / s ║ Recompilations SQL / s ║ Demandes de lot / s ║
╠═══════════════╬══════════════════════╬══════════ ══════════════╬════════════════════╣
20 10020,00 ║ 10,07984031000 ║ 0,0000000000000 ║ 10,07984031000 ║
╚═══════════════╩══════════════════════╩══════════ ══════════════╩════════════════════╝

Si nous regardons cette requête:

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

Nous pouvons confirmer qu'il y a eu 100 exécutions de la requête de test.

Dans les résultats ci-dessus, vous pouvez voir que nous obtenons des compilations à chaquesp_executesql exécution de l' instruction. Le plan pour cela est certainement mis en cache, mais nous voyons une compilation pour cela; ce qui donne?

Les Microsoft Docs disent ceci sur sp_executesql:

sp_executesql a le même comportement que EXECUTE en ce qui concerne les lots, la portée des noms et le contexte de la base de données. L'instruction Transact-SQL ou le lot dans le paramètre sp_executesql @stmt n'est pas compilé tant que l'instruction sp_executesql n'est pas exécutée. Le contenu de @stmt est ensuite compilé et exécuté en tant que plan d'exécution distinct du plan d'exécution du lot appelé sp_executesql.

Ainsi, sp_executesql lui - même est compilé à chaque exécution, même si le plan du texte de commande est déjà dans le cache du plan. @PaulWhite montre dans sa réponse que la plupart des appels à sp_executesql ne sont en fait pas mis en cache.

Max Vernon
la source