Die Ausgabe von SET STATISTICS IO ONfü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.NUMdie #tempTabelle 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_SHWartezeiten in der #tempTabelle 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.sysschobjsNamen 'nc1'und 'nc2'.
Das Abfragen tempdb.sys.fn_dblogwä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 #tempVersion erforderlichen .
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
Wenn Sie die Transaktionsprotokolleinträge für die #tempTabellenversion 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 TABLETransaktion erscheint nur in der #TempVersion 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 FCheckAndCleanupCachedTempTableTransaktion erscheint in beiden, hat aber 6 zusätzliche Einträge in der #tempVersion. Dies sind die 6 Zeilen, auf die Bezug genommen wird, sys.sysschobjsund 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_CLUSTEREDist eine Aktualisierung der modify_dateSpalte in sys.objects. Die verbleibenden fünf Zeilen befassen sich alle mit dem Umbenennen von Objekten. Da namees sich um eine Schlüsselspalte der beiden betroffenen NCIs ( nc1und 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 #tempTabellenversion, wenn die gespeicherte Prozedur einen Teil der von der FCheckAndCleanupCachedTempTableTransaktion ausgeführten Bereinigung beendet , die temporäre Tabelle von so etwas wie #T__________________________________________________________________________________________________________________00000000E316einem anderen internen Namen umbenannt wird, #2F4A0079und wenn sie eingegeben wird CREATE TABLE, benennt die Transaktion sie zurück. Dieser Flip-Flop-Name kann von einer Verbindung gesehen werden, die dbo.T2in 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::SetNamemit 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

#tempTabelle nur einmal erstellt werden, obwohl sie anschließend 9.999 Mal gelöscht und neu aufgefüllt wurden.