Я просматривал здесь статью « Временные таблицы и переменные таблиц и их влияние на производительность SQL Server», а на SQL Server 2008 удалось воспроизвести результаты, аналогичные показанным в 2005 году.
При выполнении хранимых процедур (определения ниже) только с 10 строками версия табличной переменной out выполняет временную версию таблицы более чем в два раза.
Я очистил кэш процедур и запустил обе хранимые процедуры 10 000 раз, затем повторил процесс еще для 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 раз после этого.Ответы:
Выход
SET STATISTICS IO ON
для обоих выглядит одинаководает
И, как указывает Аарон в комментариях, план для версии табличной переменной на самом деле менее эффективен, поскольку оба имеют план вложенных циклов, управляемый поиском по индексу
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
и эти результаты для 10000 казней
EXEC dbo.V2 10
Таким образом, ясно, что количество
PAGELATCH_SH
ожиданий намного выше в#temp
случае таблицы. Я не знаю ни одного способа добавления ресурса ожидания в расширенную трассировку событий, поэтому для дальнейшего изучения я запустилПока в другой связи опрос
sys.dm_os_waiting_tasks
После того, как он был запущен в течение 15 секунд, он получил следующие результаты
Обе эти блокируемые страницы принадлежат (разным) некластеризованным индексам в
tempdb.sys.sysschobjs
базовой таблице с именами'nc1'
и'nc2'
.Запросы
tempdb.sys.fn_dblog
во время выполнения указывают, что количество записей журнала, добавленных при первом выполнении каждой хранимой процедуры, было несколько переменным, но для последующих выполнений число, добавленное каждой итерацией, было очень последовательным и предсказуемым. После того, как планы процедур кэшированы, количество записей в журнале становится примерно вдвое меньше, чем необходимо для#temp
версии.Рассматривая записи журнала транзакций более подробно для
#temp
табличной версии SP, каждый последующий вызов хранимой процедуры создает три транзакции, а переменная таблицы - только две.В
INSERT
/TVQUERY
операции идентичны , за исключением имени. Он содержит записи журнала для каждой из 10 строк, вставленных во временную таблицу или переменную таблицы, плюс записиLOP_BEGIN_XACT
/LOP_COMMIT_XACT
.CREATE TABLE
Транзакция появляется только в#Temp
версии и выглядит следующим образом .FCheckAndCleanupCachedTempTable
Транзакция появляется в обоих , но имеет 6 дополнительных записей в#temp
версии. Это 6 строк, которые относятся кsys.sysschobjs
тому же шаблону, что и выше.Глядя на эти 6 строк в обеих транзакциях, они соответствуют одним и тем же операциям. Первый
LOP_MODIFY_ROW, LCX_CLUSTERED
- это обновлениеmodify_date
столбца вsys.objects
. Все остальные пять строк связаны с переименованием объекта. Посколькуname
это ключевой столбец обоих затронутых NCI (nc1
иnc2
), он выполняется как удаление / вставка для тех, кто возвращается к кластерному индексу и обновляет его.Похоже, что для
#temp
версии таблицы, когда хранимая процедура завершается, часть очистки, выполняемойFCheckAndCleanupCachedTempTable
транзакцией, состоит в том, чтобы переименовать временную таблицу из чего-то похожего#T__________________________________________________________________________________________________________________00000000E316
на другое внутреннее имя, например,#2F4A0079
когда она введена,CREATE TABLE
транзакция переименовывает ее обратно. Это триггерное имя можно увидеть в одном соединении, которое выполняетсяdbo.T2
в цикле, а в другомПример результатов
Таким образом, одно потенциальное объяснение наблюдаемой разницы в производительности, на которое ссылается Алекс, заключается в том, что именно эта дополнительная работа по сопровождению системных таблиц
tempdb
является ответственной.При выполнении обеих процедур в цикле профилировщик кода Visual Studio обнаруживает следующее
Версия табличной переменной тратит около 60% времени на выполнение оператора вставки и последующего выбора, тогда как временная таблица меньше, чем половина. Это согласуется с временными интервалами, показанными в OP, и с заключением выше, что разница в производительности обусловлена временем, затрачиваемым на выполнение вспомогательной работы, а не временем, затрачиваемым на само выполнение запроса.
Наиболее важные функции, способствующие «пропущенным» 75% в версии временного стола:
В обеих функциях создания и выпуска функция
CMEDProxyObject::SetName
отображается с включенным значением выборки19.6%
. Из чего я делаю вывод, что 39,2% времени в случае временной таблицы занято переименованием, описанным ранее.А самые большие в версии табличных переменных, которые вносят вклад в остальные 40%,
Профиль временного стола
Профиль переменной таблицы
источник
Дискотека Inferno
Поскольку это более старый вопрос, я решил вернуться к этой проблеме в более новых версиях SQL Server, чтобы посмотреть, существует ли тот же профиль производительности или же характеристики изменились вообще.
В частности, добавление системных таблиц в памяти для SQL Server 2019 представляется достойным поводом для повторного тестирования.
Я использую немного другой тестовый комплект, поскольку столкнулся с этой проблемой, работая над чем-то другим.
Тестирование, тестирование
Используя версию переполнения стека 2013 года , у меня есть этот индекс и эти две процедуры:
Показатель:
Временная таблица:
Переменная таблицы:
Чтобы предотвратить потенциальное ожидание ASYNC_NETWORK_IO , я использую процедуры-оболочки.
SQL Server 2017
Так как 2014 и 2016 годы в основном являются RELICS, я начинаю свое тестирование с 2017 года. Также, для краткости, я прыгаю прямо к профилированию кода с помощью Perfview . В реальной жизни я смотрел на ожидания, защелки, спин-блокировки, сумасшедшие трассировочные флаги и другие вещи.
Профилирование кода - единственное, что выявило что-либо интересное.
Разница во времени:
Все еще очень четкая разница, а? Но что сейчас поражает SQL Server?
Глядя на два верхних прироста разбросанных образцов, мы видим
sqlmin
иsqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
являемся двумя крупнейшими преступниками.Судя по именам в стеках вызовов, очистка и внутреннее переименование временных таблиц, по-видимому, являются самыми большими временными отсосами при вызове временной таблицы по сравнению с вызовом табличной переменной.
Несмотря на то, что табличные переменные внутренне поддерживаются временными таблицами, это не кажется проблемой.
Просмотр стеков вызовов для теста табличных переменных вообще не показывает ни одного из основных нарушителей:
SQL Server 2019 (Vanilla)
Хорошо, так что это все еще проблема в SQL Server 2017, что-то другое в 2019 году из коробки?
Во-первых, чтобы показать, что в моем рукаве ничего нет:
Разница во времени:
Обе процедуры были разные. Вызов временной таблицы был на пару секунд быстрее, а вызов табличной переменной - примерно на 1,5 секунды медленнее. Замедление табличных переменных может быть частично объяснено отложенной компиляцией табличных переменных , новым выбором оптимизатора в 2019 году.
Глядя на diff в Perfview, он немного изменился - sqlmin больше нет - но
sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
есть.SQL Server 2019 (системные таблицы Tempdb в памяти)
Как насчет этого нового в памяти системной таблицы? Хм? С чем это?
Давайте включим это!
Обратите внимание, что для этого требуется перезапуск SQL Server, поэтому простите меня, пока я перезагружаю SQL в этот прекрасный пятничный день.
Теперь все выглядит иначе:
Разница во времени:
Временные таблицы работали на 4 секунды лучше! Это что-то.
Мне что то нравится
На этот раз разница в Perfview не очень интересна. Наряду с этим интересно отметить, насколько близки времена по всем направлениям:
Один интересный момент в diff - это вызовы
hkengine!
, которые могут показаться очевидными, поскольку в настоящее время используются функции hekaton-ish.Что касается двух верхних элементов в diff, я не могу сделать большую часть из
ntoskrnl!?
:Или
sqltses!CSqlSortManager_80::GetSortKey
, но они здесь для Smrtr Ppl ™, чтобы посмотреть:Обратите внимание, что документ недокументирован и определенно небезопасен для производства, поэтому, пожалуйста, не используйте его флаг трассировки запуска, который можно использовать для включения дополнительных системных объектов временных таблиц (sysrowsets, sysallocunits и sysseobjvalues), включенных в функцию в памяти, но это не сделал заметной разницы во времени выполнения в этом случае.
Округлять
Даже в более новых версиях SQL-сервера высокочастотные вызовы табличных переменных намного быстрее, чем высокочастотные вызовы временных таблиц.
Хотя соблазнительно винить компиляцию, перекомпиляцию, автоматическую статистику, защелки, спин-блокировки, кэширование или другие проблемы, проблема все еще остается в управлении очисткой временной таблицы.
Это более близкий вызов в SQL Server 2019 с включенными системными таблицами в памяти, но табличные переменные все еще работают лучше, когда частота вызовов высока.
Конечно, как сказал вейп-мудрец, «используйте переменные таблицы, когда выбор плана не является проблемой».
источник