Jeder Stapel bewirkt eine Kompilierung

10

Wir haben eine Drittanbieteranwendung, die T-SQL-Anweisungen in Stapeln sendet.

Die Datenbank wird auf einer SQL Server 2016 Enterprise SP1 CU7, 16 Kernen und 256 GB Speicher gehostet. Für Ad-hoc optimieren ist aktiviert.

Dies ist ein Dummy-Beispiel für die Abfragen, die ausgeführt werden:

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'

Wenn ich die Datenbank überwache und mir Stapel / Sek. Und Kompilierungen / Sek. Ansehe, stelle ich fest, dass sie immer gleich sind. Unter starker Last können dies 1000 Stapel / Sek. Und 1000 Kompilierungen / Sek. Sein. Unter durchschnittlicher Last gibt es 150 Chargen / Sek.

Ich analysiere den Abfrage-Cache auf kürzlich kompilierte Pläne:

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;

Wenn ich über der Abfrage ausgeführt werde, werden nur 10 bis 20 neue Abfragepläne pro Sekunde angezeigt.

Es ist, als würde jeder sp_executesqlAufruf eine Kompilierung auslösen, aber der Abfrageplan wird nicht zwischengespeichert.

Was kann die Ursache dafür sein, dass Stapel / Sek. Gleich Kompilierungen / Sek. Sind?

Frederik Vanderhaegen
quelle

Antworten:

12

Es ist, als würde jeder sp_executesqlAufruf eine Kompilierung auslösen, aber der Abfrageplan wird nicht zwischengespeichert.

SQL Server speichert keinen Abfrageplan für Stapel zwischen, die nur einen sp_executesqlAufruf enthalten. Ohne einen zwischengespeicherten Plan wird jedes Mal eine Kompilierung durchgeführt. Dies ist beabsichtigt und wird erwartet.

SQL Server vermeidet das Zwischenspeichern von Stapeln mit geringen Kosten für die Kompilierung. Die Details dessen, was zwischengespeichert wird und was nicht, haben sich im Laufe der Jahre vielfach geändert. Siehe meine Antwort auf Trace-Flag 2861 und was ein "Null-Kosten" -Plan tatsächlich bedeutet, für Details.

Kurz gesagt, die Wahrscheinlichkeit einer Wiederverwendung (einschließlich bestimmter Parameterwerte) ist gering, und die Kosten für das Kompilieren des Ad-hoc-Textes, der den sp_executesqlAufruf enthält, sind sehr gering. Die innere parametrisierte Charge von sp_executesqlwird natürlich zwischengespeichert und wiederverwendet - das ist der Wert davon. Die erweiterte gespeicherte Prozedur sp_executesqlselbst wird ebenfalls zwischengespeichert.

Um zwischengespeichert und wiederverwendet zu werden, sp_executesqlmüsste die Anweisung Teil eines größeren Stapels sein, der als zwischengespeichert angesehen wird. Beispielsweise:

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

Führen Sie diesen Code mehrmals aus. Beim ersten Mal werden jedoch viele Zusammenstellungen wie erwartet gemeldet. Beim zweiten Mal werden keine Kompilierungen gemeldet, sofern sie nicht optimize for ad hoc workloadsaktiviert sind (daher wird nur ein kompilierter Plan-Stub zwischengespeichert). Beim dritten Mal werden auf keinen Fall Zusammenstellungen gemeldet, da jeder Stub zu einem vollständig zwischengespeicherten Ad-hoc-Plan befördert wird.

Entfernen Sie die DECLARE @TCAnweisung, um sicherzustellen , dass die sys.sp_executesqlAnweisung ohne sie nie zwischengespeichert wird, unabhängig davon, wie oft sie ausgeführt wird.

Zeigen Sie die zugehörigen Plan-Cache-Einträge an mit:

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

Verwandte Fragen und Antworten: Kompilieren Trigger jedes Mal?

Paul White 9
quelle
11

Sie können annähern, was Sie in Performance Monitor und Activity Monitor für SQL Compilations/secund sehen Batch Requests/sec, während Sie einige Stapel in einem separaten Abfragefenster als Test ausführen, wie unten beschrieben.

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

Führen Sie in Abfragefenster 2 Folgendes aus, während der obige Code ausgeführt wird. Der Code führt einfach 100 T-SQL-Stapel aus:

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

Wenn Sie zurück zu Abfragefenster 1 wechseln, sehen Sie ungefähr Folgendes:

╔═══════════════╦══════════════════════╦══════════ ══════════════╦════════════════════╗
║ ElapsedTimeMS ║ SQL-Kompilierungen / Sek. ║ SQL-Neukompilierungen / Sek. ║ Stapelanforderungen / Sek. ║
╠═══════════════╬══════════════════════╬══════════ ══════════════╬════════════════════╣
║ 10020.00 ║ 10.07984031000 ║ 0.00000000000 ║ 10.07984031000 ║
╚═══════════════╩══════════════════════╩══════════ ══════════════╩════════════════════╝

Wenn wir uns diese Abfrage ansehen:

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

Wir können bestätigen, dass die Testabfrage 100 Mal ausgeführt wurde.

In den obigen Ergebnissen sehen Sie, dass wir jedes Mal Kompilierungen erhalten, wenn die sp_executesqlAnweisung ausgeführt wird. Der Plan dafür wird sicherlich zwischengespeichert, aber wir sehen eine Zusammenstellung dafür; was gibt?

Die Microsoft Docs sagen dies über sp_executesql:

sp_executesql verhält sich in Bezug auf Stapel, Namensumfang und Datenbankkontext genauso wie EXECUTE. Die Transact-SQL-Anweisung oder der Stapel im Parameter sp_executesql @stmt wird erst kompiliert, wenn die Anweisung sp_executesql ausgeführt wird. Der Inhalt von @stmt wird dann kompiliert und als Ausführungsplan ausgeführt, der vom Ausführungsplan des Stapels mit dem Namen sp_executesql getrennt ist.

Daher wird es bei jeder Ausführung sp_executesql selbst kompiliert, auch wenn sich der Plan für den Befehlstext bereits im Plan-Cache befindet. @PaulWhite zeigt in seiner Antwort, dass die meisten Aufrufe von sp_executesql tatsächlich nicht zwischengespeichert werden.

Max Vernon
quelle