Вихід 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]тоді як табличні змінний версія виконує пошук індексу @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 |
+---------------------+------------+----------------+----------------+----------------+
і це результати за 10 000 страт 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 Code виявляє наступне
+-------------------------------+--------------------+-------+-----------+
| 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% часу на виконання оператора вставлення та подальшого вибору, тоді як тимчасова таблиця менше половини, ніж ця. Це узгоджується з термінами, показаними в ОП, і з висновком вище, що різниця в продуктивності зменшується до часу, витраченого на виконання допоміжних робіт, не за рахунок часу, витраченого на виконання запиту.
Найважливіші функції, що сприяють «відсутнім» 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 разів після цього.