Чому в даному випадку використання змінної таблиці більш ніж удвічі швидше таблиці #temp?


37

Я переглянув статтю « Тимчасові таблиці проти змінних таблиць та їх вплив на продуктивність SQL Server та на SQL Server 2008» зміг відтворити аналогічні результати, показані там у 2005 році.

Під час виконання збережених процедур (визначень нижче) лише з 10 рядків версія змінної таблиці виконує тимчасову версію таблиці більш ніж у два рази.

Я очистив кеш процедур і запустив обидві збережені процедури 10000 разів, потім повторив процес ще 4 запуски. Результати нижче (час у мс на партію)

T2_Time     V2_Time
----------- -----------
8578        2718      
6641        2781    
6469        2813   
6766        2797
6156        2719

Моє запитання: Що є причиною кращої роботи версії змінної таблиці?

Я провів деяке розслідування. наприклад, перегляд лічильників продуктивності за допомогою

SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';

підтверджує, що в обох випадках тимчасові об'єкти кешуються після першого запуску, як очікувалося, а не створюються з нуля знову для кожного виклику.

Точно так же відстеження Auto Stats, SP:Recompile, SQL:StmtRecompileподія в Profiler (скріншот нижче) показує , що ці події відбуваються один раз (на перший виклик #tempзбереженої процедури таблиці) і інші 9,999 кари не викликає яку - небудь з цих подій. (Версія змінної таблиці не отримує жодної з цих подій)

Сліди

Трохи більший накладний витрата першого запуску збереженої процедури жодним чином не може спричинити велику загальну різницю, однак для очищення кешу процедур та запуску обох процедур потрібно лише декілька мс один раз, тому я не вірю ні в статистику, ні причиною можуть бути перекомпіляції.

Створення необхідних об’єктів бази даних

CREATE DATABASE TESTDB_18Feb2012;

GO

USE TESTDB_18Feb2012;

CREATE TABLE NUM 
  ( 
     n INT PRIMARY KEY, 
     s VARCHAR(128) 
  ); 

WITH NUMS(N) 
     AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0) 
         FROM   master..spt_values v1, 
                master..spt_values v2) 
INSERT INTO NUM 
SELECT N, 
       'Value: ' + CONVERT(VARCHAR, N) 
FROM   NUMS 

GO

CREATE PROCEDURE [dbo].[T2] @total INT 
AS 
  CREATE TABLE #T 
    ( 
       n INT PRIMARY KEY, 
       s VARCHAR(128) 
    ) 

  INSERT INTO #T 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   #T 
                        WHERE  #T.n = NUM.n) 
GO

CREATE PROCEDURE [dbo].[V2] @total INT 
AS 
  DECLARE @V TABLE ( 
    n INT PRIMARY KEY, 
    s VARCHAR(128)) 

  INSERT INTO @V 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   @V V 
                        WHERE  V.n = NUM.n) 


GO

Тестовий сценарій

SET NOCOUNT ON;

DECLARE @T1 DATETIME2,
        @T2 DATETIME2,
        @T3 DATETIME2,  
        @Counter INT = 0

SET @T1 = SYSDATETIME()

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END

SET @T2 = SYSDATETIME()
SET @Counter = 0

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END

SET @T3 = SYSDATETIME()

SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
       DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time

Слід профайлера вказує на те, що статистика створюється в #tempтаблиці лише один раз, незважаючи на те, що вона очищається і повторно заповнюється ще 9 999 разів після цього.
Мартін Сміт

Відповіді:


31

Вихід 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%            |
+-----------------------------------+-------------------+

Профіль тимчасової таблиці

введіть тут опис зображення

Профіль змінної таблиці

введіть тут опис зображення


10

Disco Inferno

Оскільки це старіше питання, я вирішив переглянути проблему на нових версіях SQL Server, щоб побачити, чи існує ще одна і та ж версія продуктивності, чи характеристики взагалі змінилися.

Зокрема, додавання системних таблиць в пам'яті для SQL Server 2019 здається вагомим приводом для повторної перевірки.

Я використовую дещо інший тестовий джгут, оскільки я зіткнувся з цим питанням, працюючи над чимось іншим.

Тестування, тестування

Використовуючи версію Stack Overflow 2013 року , у мене є цей індекс і ці дві процедури:

Індекс:

CREATE INDEX ix_whatever 
    ON dbo.Posts(OwnerUserId) INCLUDE(Score);
GO

Темп-таблиця:

    CREATE OR ALTER PROCEDURE dbo.TempTableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        CREATE TABLE #t(i INT NOT NULL);
        DECLARE @i INT;

        INSERT #t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM #t AS t;

    END;
    GO 

Змінна таблиця:

    CREATE OR ALTER PROCEDURE dbo.TableVariableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        DECLARE @t TABLE (i INT NOT NULL);
        DECLARE @i INT;

        INSERT @t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM @t AS t;

    END;
    GO 

Щоб запобігти будь-якому потенційному очікуванню ASYNC_NETWORK_IO , я використовую обгорткові процедури.

CREATE PROCEDURE #TT AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TempTableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

CREATE PROCEDURE #TV AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TableVariableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

SQL Server 2017

Починаючи з 2014 і 2016 років, це в основному РЕЛІКС, я починаю тестування з 2017 року. Крім того, для стислості я стрибаю правильно до профілювання коду за допомогою Perfview . У реальному житті я дивився на очікування, засувки, розгортки, шалені сліди та інші речі.

Профілювання коду - це єдине, що виявило щось цікаве.

Різниця у часі:

  • Таблиця темп: 17891 мс
  • Змінна таблиця: 5891 мс

Ще дуже чітка різниця, так? Але що зараз вражає SQL Server?

Горіхи

Дивлячись на два найбільші збільшення у різних вибірках, ми бачимо sqlminі sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketє двома найбільшими правопорушниками.

Горіхи

Судячи з імен у стеках викликів, очищення та внутрішнє перейменування тимчасових таблиць, здається, є найбільшим часом виклику тимчасового виклику та викликом змінної таблиці.

Незважаючи на те, що змінні таблиці внутрішньо підтримуються тимчасовими таблицями, це, здається, не є проблемою.

SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;

Таблиця '# B98CE339'. Кількість сканувань 1

Переглядаючи стеки викликів для тесту змінної таблиці, взагалі не відображається жоден з основних порушників:

Горіхи

SQL Server 2019 (ванільний)

Добре, значить, це все ще проблема в SQL Server 2017, чи щось в 2019 році не вийшло?

По-перше, щоб показати, що в моєму рукаві немає нічого:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

Горіхи

Різниця у часі:

  • Таблиця темп: 15765 мс
  • Змінна таблиця: 7250 мс

Обидві процедури були різними. Виклик таблиці темп був на пару секунд швидшим, а виклик змінної таблиці - на 1,5 секунди повільніше. Уповільнення змін у таблиці може бути частково пояснено відкладеною компіляцією змінної таблиці , новим вибором оптимізатора в 2019 році.

Дивлячись на відмінності в Perfview, він трохи змінився - sqlmin вже не існує - але sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketє.

Горіхи

SQL Server 2019 (системні таблиці Tempdb в пам'яті)

Що з цією новою системою таблиці пам'яті? Гм? Суп з цим?

Давайте включимо!

EXEC sys.sp_configure @configname = 'advanced', 
                      @configvalue = 1  
RECONFIGURE;

EXEC sys.sp_configure @configname = 'tempdb metadata memory-optimized', 
                      @configvalue = 1 
RECONFIGURE;

Зауважте, що для цього потрібно перезапустити SQL Server, тож пробачте мене, коли я перезавантажую SQL цього прекрасного дня в п'ятницю.

Зараз все виглядає інакше:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

SELECT *, 
       OBJECT_NAME(object_id) AS object_name, 
       @@VERSION AS sql_server_version
FROM tempdb.sys.memory_optimized_tables_internal_attributes;

Горіхи

Різниця у часі:

  • Таблиця темп: 11638 мс
  • Змінна таблиця: 7403 мс

Темп-таблиці зробили приблизно на 4 секунди краще! Це щось.

Мені щось подобається.

Цього разу відмінність Perfview не дуже цікава. Пліч-о-пліч, цікаво відзначити, наскільки близькі часи по всій раді:

Горіхи

Одним із цікавих моментів розбіжності є заклики до hkengine!, які можуть здатися очевидними, оскільки зараз використовуються функції hekaton-ish.

Горіхи

Що стосується перших двох елементів у розл., Я не можу багато чого скласти ntoskrnl!?:

Горіхи

Або sqltses!CSqlSortManager_80::GetSortKey, але вони є тут, щоб Smrtr Ppl ™ подивився на:

Горіхи

Зауважте, що у документах немає документації та, безумовно, не є безпечною для виробництва, тому, будь ласка, не використовуйте її прапор сліду запуску, який ви можете використовувати, щоб додаткові об'єкти системних таблиць (sysrowsets, sysallocunits і sysseobjvalues) включені до функції пам'яті, але це не помітні зміни в термінах виконання в цьому випадку.

Раунд

Навіть у новіших версіях SQL-сервера дзвінки високої частоти до змінних таблиць набагато швидші, ніж високочастотні дзвінки до тимчасових таблиць.

Незважаючи на те, що звинувачувати компіляції, перекомпіляції, автоматичну статистику, засувки, зворотні блоки, кешування чи інші проблеми, це проблема все ще існує навколо управління очищенням таблиці темп.

Це більш тісний дзвінок у SQL Server 2019 із увімкненими системними таблицями пам'яті, але змінні таблиці все ж краще, коли частота викликів висока.

Звичайно, колись мудрий мудрець колись замислювався: "використовувати змінні таблиці, коли вибір плану не є проблемою".


Приємно - вибачте, що я пропустив, що ви додали відповідь на це, поки просто не переходите за посиланням у своєму дописі в блозі "налагодження"
Мартін Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.