Вихід 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 разів після цього.