План был скомпилирован на экземпляре окончательной первоначальной версии SQL Server 2008 R2 (сборка 10.50.1600). Вам следует установить Service Pack 3 (сборка 10.50.6000), а затем установить последние исправления, чтобы привести его к (текущей) последней сборке 10.50.6542. Это важно по ряду причин, включая безопасность, исправление ошибок и новые функции.
Оптимизация встраивания параметров
Относящийся к настоящему вопросу, SQL Server 2008 R2 RTM не поддерживает оптимизацию встраивания параметров (PEO) для OPTION (RECOMPILE). Прямо сейчас вы оплачиваете стоимость перекомпиляции без реализации одного из основных преимуществ.
Когда PEO доступен, SQL Server может использовать литеральные значения, хранящиеся в локальных переменных и параметрах, непосредственно в плане запроса. Это может привести к значительным упрощениям и повышению производительности. Более подробная информация об этом содержится в моей статье « Параметры сниффинга», «Встраивание» и «Параметры RECOMPILE» .
Хэш, сортировка и разливы
Они отображаются только в планах выполнения, когда запрос был скомпилирован в SQL Server 2012 или более поздней версии. В более ранних версиях нам приходилось отслеживать разливы во время выполнения запроса с использованием Profiler или Extended Events. Разливы всегда приводят к физическому вводу-выводу в (и из) постоянной базы данных хранилища данных , что может иметь важные последствия для производительности, особенно если разлив велик или путь ввода-вывода находится под давлением.
В вашем плане выполнения есть два оператора Hash Match (Aggregate). Память, зарезервированная для хеш-таблицы, основана на оценке выходных строк (другими словами, она пропорциональна количеству групп, найденных во время выполнения). Предоставленная память фиксируется непосредственно перед началом выполнения и не может увеличиваться во время выполнения, независимо от того, сколько свободной памяти имеет экземпляр. В предоставленном плане оба оператора Hash Match (Aggregate) производят больше строк, чем ожидал оптимизатор, и поэтому могут возникать разливы в базу данных tempdb во время выполнения.
В плане также есть оператор Hash Match (Inner Join). Память, зарезервированная для хеш-таблицы, основана на оценке входных строк на стороне зонда . Входные данные датчика оценивают 847 399 строк, но во время выполнения встречаются 1223 636 строк. Этот избыток также может вызывать разлив хеша.
Избыточный агрегат
Hash Match (Aggregate) на узле 8 выполняет операцию группировки (Assortment_Id, CustomAttrID), но входные строки равны выходным строкам:

Это говорит о том, что комбинация столбцов является ключом (так что группировка семантически не нужна). Стоимость выполнения избыточного агрегата увеличивается из-за необходимости дважды передавать 1,4 миллиона строк через обмены хэш-разделами (операторы параллелизма с обеих сторон).
С учетом того, что соответствующие столбцы взяты из разных таблиц, передать эту уникальную информацию уникальности сложнее, чем обычно, поэтому можно избежать операции избыточной группировки и ненужных обменов.
Неэффективное распределение потоков
Как отмечено в ответе Джо Оббиша , обмен в узле 14 использует хеш-разбиение для распределения строк по потокам. К сожалению, небольшое количество строк и доступных планировщиков означает, что все три строки оказываются в одном потоке. По-видимому, параллельный план выполняется последовательно (с параллельными издержками) вплоть до обмена в узле 9.
Вы можете решить эту проблему (для получения циклического перебора или широковещательного разбиения), исключив Различную сортировку на узле 13. Самый простой способ сделать это - создать кластеризованный первичный ключ в #tempтаблице и выполнить отдельную операцию при загрузке таблицы:
CREATE TABLE #Temp
(
id integer NOT NULL PRIMARY KEY CLUSTERED
);
INSERT #Temp
(
id
)
SELECT DISTINCT
CAV.id
FROM @customAttrValIds AS CAV
WHERE
CAV.id IS NOT NULL;
Кэширование статистики временной таблицы
Несмотря на использование OPTION (RECOMPILE), SQL Server все еще может кэшировать объект временной таблицы и связанную с ней статистику между вызовами процедур. Как правило, это приветствуется оптимизация производительности, но если временная таблица заполняется аналогичным объемом данных о вызовах смежных процедур, перекомпилированный план может основываться на неверной статистике (кэшируется из предыдущего выполнения). Это подробно описано в моих статьях, « Временные таблицы в хранимых процедурах» и « Кэширование временных таблиц» .
Чтобы избежать этого, используйте OPTION (RECOMPILE)вместе с явным UPDATE STATISTICS #TempTableпосле заполнения временной таблицы и перед тем, как на нее будет ссылаться запрос.
Переписать запрос
В этой части предполагается, что изменения в создании #Tempтаблицы уже сделаны.
Учитывая стоимость возможных разливов хеша и избыточного агрегата (и окружающих обменов), он может заплатить за материализацию набора в узле 10:
CREATE TABLE #Temp2
(
CustomAttrID integer NOT NULL,
Assortment_Id integer NOT NULL,
);
INSERT #Temp2
(
Assortment_Id,
CustomAttrID
)
SELECT
ACAV.Assortment_Id,
CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
ON T.id = ACAV.CustomAttributeValue_Id;
ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);
PRIMARY KEYДобавляются в отдельном шаге , чтобы обеспечить построение индекса имеет точную информацию кардинальной и избежать временных статистических таблиц кэширования вопроса.
Эта материализация, скорее всего, произойдет в памяти (избегая ввода-вывода tempdb ), если у экземпляра достаточно памяти. Это еще более вероятно после обновления до SQL Server 2012 (SP1 CU10 / SP2 CU1 или более поздней версии), который улучшил поведение Eager Write .
Это действие дает оптимизатору точную информацию о количестве элементов в промежуточном наборе, позволяет ему создавать статистику и позволяет объявить его (Assortment_Id, CustomAttrID)в качестве ключа.
План для заполнения #Temp2должен выглядеть следующим образом (обратите внимание, что сканирование кластеризованного индекса #Tempне имеет четкой сортировки, и теперь обмен использует циклическое разбиение строк):

При наличии этого набора окончательный запрос становится:
SELECT
A.Id,
A.AssortmentId
FROM
(
SELECT
T.Assortment_Id
FROM #Temp2 AS T
GROUP BY
T.Assortment_Id
HAVING
COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
ON A.Id = DT.Assortment_Id
WHERE
A.AssortmentType = @asType
OPTION (RECOMPILE);
Мы могли бы вручную переписать их COUNT_BIG(DISTINCT...как простую COUNT_BIG(*), но с новой ключевой информацией оптимизатор сделает это за нас:

Окончательный план может использовать соединение цикла / хеша / слияния в зависимости от статистической информации о данных, к которым у меня нет доступа. Еще одно небольшое замечание: я предположил, что такой индекс CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);существует.
В любом случае, важная вещь в окончательных планах состоит в том, что оценки должны быть намного лучше, а сложная последовательность операций группировки была сведена к одному агрегату потока (который не требует памяти и, следовательно, не может пролиться на диск).
Трудно сказать, что производительность в этом случае на самом деле будет лучше с использованием дополнительной временной таблицы, но оценки и выбор планов будут гораздо более устойчивыми к изменениям объема и распределения данных с течением времени. Это может быть более ценным в долгосрочной перспективе, чем небольшое повышение производительности сегодня. В любом случае, теперь у вас есть гораздо больше информации, на которой можно основывать свое окончательное решение.
#tempсоздание и использование будет проблемой для производительности, а не выигрышем. Вы сохраняете в неиндексированную таблицу только один раз. Попробуйте удалить его полностью (и, возможно,in (select id from #temp)exists