Выход SET STATISTICS IO ONдля обоих выглядит одинаково
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
дает
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
И, как указывает Аарон в комментариях, план для версии табличной переменной на самом деле менее эффективен, поскольку оба имеют план вложенных циклов, управляемый поиском по индексу dbo.NUMв #tempтабличной версии, выполняет поиск по индексу [#T].n = [dbo].[NUM].[n]с остаточным предикатом, [#T].[n]<=[@total]тогда как табличная переменная version выполняет поиск по индексу @V.n <= [@total]с остаточным предикатом @V.[n]=[dbo].[NUM].[n]и, таким образом, обрабатывает больше строк (именно поэтому этот план работает так плохо для большего числа строк)
Использование расширенных событий для просмотра типов ожидания для конкретного spid дает эти результаты для 10 000 выполненийEXEC 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 |
+---------------------+------------+----------------+----------------+----------------+
и эти результаты для 10000 казней 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 |
+---------------------+------------+----------------+----------------+----------------+
Таким образом, ясно, что количество PAGELATCH_SHожиданий намного выше в #tempслучае таблицы. Я не знаю ни одного способа добавления ресурса ожидания в расширенную трассировку событий, поэтому для дальнейшего изучения я запустил
WHILE 1=1
EXEC dbo.T2 10
Пока в другой связи опрос 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'
После того, как он был запущен в течение 15 секунд, он получил следующие результаты
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
Обе эти блокируемые страницы принадлежат (разным) некластеризованным индексам в tempdb.sys.sysschobjsбазовой таблице с именами 'nc1'и 'nc2'.
Запросы tempdb.sys.fn_dblogво время выполнения указывают, что количество записей журнала, добавленных при первом выполнении каждой хранимой процедуры, было несколько переменным, но для последующих выполнений число, добавленное каждой итерацией, было очень последовательным и предсказуемым. После того, как планы процедур кэшированы, количество записей в журнале становится примерно вдвое меньше, чем необходимо для #tempверсии.
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
Рассматривая записи журнала транзакций более подробно для #tempтабличной версии SP, каждый последующий вызов хранимой процедуры создает три транзакции, а переменная таблицы - только две.
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
В INSERT/ TVQUERYоперации идентичны , за исключением имени. Он содержит записи журнала для каждой из 10 строк, вставленных во временную таблицу или переменную таблицы, плюс записи LOP_BEGIN_XACT/ LOP_COMMIT_XACT.
CREATE TABLEТранзакция появляется только в #Tempверсии и выглядит следующим образом .
+-----------------+-------------------+---------------------+
| 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 | |
+-----------------+-------------------+---------------------+
FCheckAndCleanupCachedTempTableТранзакция появляется в обоих , но имеет 6 дополнительных записей в #tempверсии. Это 6 строк, которые относятся к sys.sysschobjsтому же шаблону, что и выше.
+-----------------+-------------------+----------------------------------------------+
| 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 | |
+-----------------+-------------------+----------------------------------------------+
Глядя на эти 6 строк в обеих транзакциях, они соответствуют одним и тем же операциям. Первый LOP_MODIFY_ROW, LCX_CLUSTERED- это обновление modify_dateстолбца в sys.objects. Все остальные пять строк связаны с переименованием объекта. Поскольку nameэто ключевой столбец обоих затронутых NCI ( nc1и nc2), он выполняется как удаление / вставка для тех, кто возвращается к кластерному индексу и обновляет его.
Похоже, что для #tempверсии таблицы, когда хранимая процедура завершается, часть очистки, выполняемой FCheckAndCleanupCachedTempTableтранзакцией, состоит в том, чтобы переименовать временную таблицу из чего-то похожего #T__________________________________________________________________________________________________________________00000000E316на другое внутреннее имя, например, #2F4A0079когда она введена, CREATE TABLEтранзакция переименовывает ее обратно. Это триггерное имя можно увидеть в одном соединении, которое выполняется dbo.T2в цикле, а в другом
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
Пример результатов

Таким образом, одно потенциальное объяснение наблюдаемой разницы в производительности, на которое ссылается Алекс, заключается в том, что именно эта дополнительная работа по сопровождению системных таблиц tempdbявляется ответственной.
При выполнении обеих процедур в цикле профилировщик кода Visual Studio обнаруживает следующее
+-------------------------------+--------------------+-------+-----------+
| 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 |
+-------------------------------+--------------------+-------+-----------+
Версия табличной переменной тратит около 60% времени на выполнение оператора вставки и последующего выбора, тогда как временная таблица меньше, чем половина. Это согласуется с временными интервалами, показанными в OP, и с заключением выше, что разница в производительности обусловлена временем, затрачиваемым на выполнение вспомогательной работы, а не временем, затрачиваемым на само выполнение запроса.
Наиболее важные функции, способствующие «пропущенным» 75% в версии временного стола:
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
В обеих функциях создания и выпуска функция CMEDProxyObject::SetNameотображается с включенным значением выборки 19.6%. Из чего я делаю вывод, что 39,2% времени в случае временной таблицы занято переименованием, описанным ранее.
А самые большие в версии табличных переменных, которые вносят вклад в остальные 40%,
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
Профиль временного стола

Профиль переменной таблицы

#tempтаблице только один раз, несмотря на то, что она очищается и повторно заполняется еще 9 999 раз после этого.