Выход 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 раз после этого.