Die Ausgabe von SET STATISTICS IO ON
für beide sieht ähnlich aus
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
Gibt
V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Und als Aaron ist der Plan für die Tabellenvariable Version in den Kommentaren weist darauf hin , tatsächlich weniger effizient als während beide hat eine verschachtelte Schleife durch einen Index sucht angetrieben plant dbo.NUM
die #temp
Tabelle Version führt ein in den Index auf suchen [#T].n = [dbo].[NUM].[n]
mit Rest Prädikat [#T].[n]<=[@total]
während des Tabellenvariable Die Version führt eine Indexsuche @V.n <= [@total]
mit einem verbleibenden Prädikat durch @V.[n]=[dbo].[NUM].[n]
und verarbeitet so mehr Zeilen (weshalb dieser Plan für eine größere Anzahl von Zeilen so schlecht abschneidet).
Wenn Sie Extended Events verwenden , um die Wartetypen für die spezifische Spid zu untersuchen, erhalten Sie diese Ergebnisse für 10.000 Ausführungen vonEXEC dbo.T2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16 | 19 | 19 | 0 |
| PAGELATCH_SH | 39998 | 14 | 0 | 14 |
| PAGELATCH_EX | 1 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
und diese Ergebnisse für 10.000 Hinrichtungen von EXEC dbo.V2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX | 2 | 0 | 0 | 0 |
| PAGELATCH_SH | 1 | 0 | 0 | 0 |
| SOS_SCHEDULER_YIELD | 676 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
Es ist also klar, dass die Anzahl der PAGELATCH_SH
Wartezeiten in der #temp
Tabelle viel höher ist. Mir ist keine Möglichkeit bekannt, die Wait-Ressource zum erweiterten Ereignistrace hinzuzufügen, um dies weiter zu untersuchen
WHILE 1=1
EXEC dbo.T2 10
Während eines anderen Verbindungsabrufs sys.dm_os_waiting_tasks
CREATE TABLE #T(resource_description NVARCHAR(2048))
WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'
Nachdem es etwa 15 Sekunden lang laufen gelassen worden war, hatte es die folgenden Ergebnisse gesammelt
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
Beide Seiten, die zwischengespeichert werden, gehören zu (verschiedenen) nicht gruppierten Indizes für die Basistabelle mit den tempdb.sys.sysschobjs
Namen 'nc1'
und 'nc2'
.
Das Abfragen tempdb.sys.fn_dblog
während der Ausführung zeigt an, dass die Anzahl der Protokolldatensätze, die bei der ersten Ausführung jeder gespeicherten Prozedur hinzugefügt wurden, etwas variabel war. Für nachfolgende Ausführungen war die Anzahl, die bei jeder Iteration hinzugefügt wurde, jedoch sehr konsistent und vorhersehbar. Sobald die Prozedurpläne zwischengespeichert sind, beträgt die Anzahl der Protokolleinträge ungefähr die Hälfte der für die #temp
Version erforderlichen .
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
Wenn Sie die Transaktionsprotokolleinträge für die #temp
Tabellenversion des SP genauer betrachten, werden bei jedem nachfolgenden Aufruf der gespeicherten Prozedur drei Transaktionen und bei der Tabellenvariablen nur zwei Transaktionen erstellt.
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
Die INSERT
/ TVQUERY
-Transaktionen sind bis auf den Namen identisch. Dieser enthält die Protokollsätze für jede der 10 Zeilen, die in die temporäre Tabelle oder Tabellenvariable eingefügt wurden, sowie die Einträge LOP_BEGIN_XACT
/ LOP_COMMIT_XACT
.
Die CREATE TABLE
Transaktion erscheint nur in der #Temp
Version und sieht wie folgt aus.
+-----------------+-------------------+---------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_SHRINK_NOOP | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+---------------------+
Die FCheckAndCleanupCachedTempTable
Transaktion erscheint in beiden, hat aber 6 zusätzliche Einträge in der #temp
Version. Dies sind die 6 Zeilen, auf die Bezug genommen wird, sys.sysschobjs
und sie haben genau das gleiche Muster wie oben.
+-----------------+-------------------+----------------------------------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+----------------------------------------------+
Betrachtet man diese 6 Zeilen in beiden Transaktionen, so entsprechen sie denselben Operationen. Das erste LOP_MODIFY_ROW, LCX_CLUSTERED
ist eine Aktualisierung der modify_date
Spalte in sys.objects
. Die verbleibenden fünf Zeilen befassen sich alle mit dem Umbenennen von Objekten. Da name
es sich um eine Schlüsselspalte der beiden betroffenen NCIs ( nc1
und nc2
) handelt, wird dies als Löschen / Einfügen für diese ausgeführt. Dann wird zum Clustered-Index zurückgekehrt und auch dieser aktualisiert.
Es scheint, dass für die #temp
Tabellenversion, wenn die gespeicherte Prozedur einen Teil der von der FCheckAndCleanupCachedTempTable
Transaktion ausgeführten Bereinigung beendet , die temporäre Tabelle von so etwas wie #T__________________________________________________________________________________________________________________00000000E316
einem anderen internen Namen umbenannt wird, #2F4A0079
und wenn sie eingegeben wird CREATE TABLE
, benennt die Transaktion sie zurück. Dieser Flip-Flop-Name kann von einer Verbindung gesehen werden, die dbo.T2
in einer Schleife ausgeführt wird, während sie in einer anderen ausgeführt wird
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
Beispiel Ergebnisse
Eine mögliche Erklärung für das beobachtete Leistungsgefälle, auf das Alex anspielt, ist, dass diese zusätzliche Arbeit, die die Systemtabellen verwaltet tempdb
, dafür verantwortlich ist.
Wenn Sie beide Prozeduren in einer Schleife ausführen, wird im Visual Studio Code-Profiler Folgendes angezeigt
+-------------------------------+--------------------+-------+-----------+
| Function | Explanation | Temp | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute | Insert ... Select | 16.93 | 37.31 |
| CXStmtQuery::ErsqExecuteQuery | Select Max | 8.77 | 23.19 |
+-------------------------------+--------------------+-------+-----------+
| Total | | 25.7 | 60.5 |
+-------------------------------+--------------------+-------+-----------+
Die Version der Tabellenvariablen verbringt etwa 60% der Zeit mit der Ausführung der Einfügeanweisung und der anschließenden Auswahl, während die temporäre Tabelle weniger als die Hälfte davon beträgt. Dies steht im Einklang mit den im OP angegebenen Zeitpunkten und der Schlussfolgerung, dass der Leistungsunterschied auf die Zeit zurückzuführen ist, die für die Ausführung von Nebenarbeiten aufgewendet wurde, und nicht auf die Zeit, die für die Ausführung der Abfrage selbst aufgewendet wurde.
Die wichtigsten Funktionen, die zu den "fehlenden" 75% in der temporären Tabellenversion beitragen, sind
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
Sowohl unter der Funktion create als auch unter der Funktion release wird die Funktion CMEDProxyObject::SetName
mit dem Beispielwert inclusive von angezeigt 19.6%
. Daraus schließe ich, dass 39,2% der Zeit in der temporären Tabelle mit der zuvor beschriebenen Umbenennung belegt sind.
Und die größten in der Tabellenvariablenversion, die zu den anderen 40% beitragen, sind
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
Temporäres Tabellenprofil
Tabellenvariablenprofil
#temp
Tabelle nur einmal erstellt werden, obwohl sie anschließend 9.999 Mal gelöscht und neu aufgefüllt wurden.