У меня есть запрос, который выполняется в приемлемое время, но я хочу извлечь из него максимально возможную производительность.
Операция, которую я пытаюсь улучшить, - это «Поиск индекса» справа от плана с узла 17.
Я добавил соответствующие индексы, но оценки, которые я получаю для этой операции, составляют половину того, что они должны быть.
Я пытался изменить свои индексы, добавить временную таблицу и переписать запрос, но не смог упростить его больше, чем это, чтобы получить правильные оценки.
У кого-нибудь есть предложения, что еще я могу попробовать?
Полный план и его детали можно найти здесь .
Не анонимизированный план можно найти здесь.
Обновить:
У меня есть ощущение, что первоначальная версия вопроса вызвала большую путаницу, поэтому я собираюсь добавить оригинальный код с некоторыми пояснениями.
create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
set nocount on;
declare @dist_ca_id int;
select *
into #temp
from @customAttrValIds
where id is not null;
select @dist_ca_id = count(distinct CustomAttrID)
from CustomAttributeValues c
inner join #temp a on c.Id = a.id;
select a.Id
, a.AssortmentId
from Assortments a
inner join AssortmentCustomAttributeValues acav
on a.Id = acav.Assortment_Id
inner join CustomAttributeValues cav
on cav.Id = acav.CustomAttributeValue_Id
where a.AssortmentType = @asType
and acav.CustomAttributeValue_Id in (select id from #temp)
group by a.AssortmentId
, a.Id
having count(distinct cav.CustomAttrID) = @dist_ca_id
option(recompile);
end
ответы:
Почему странное начальное наименование в ссылке pasteThePlan?
Ответ : потому что я использовал анонимный план из SQL Sentry Plan Explorer.
Почему
OPTION RECOMPILE
?Ответ : Потому что я могу позволить себе перекомпиляцию, чтобы избежать перехвата параметров (данные могут / могут быть искажены). Я проверил и доволен планом, который оптимизатор создает во время использования
OPTION RECOMPILE
.WITH SCHEMABINDING
?Ответ : Я бы очень хотел избежать этого и использовал бы его только тогда, когда у меня есть индексированное представление. В любом случае, это системная функция (
COUNT()
), поэтому здесь нет смыслаSCHEMABINDING
.
Ответы на более возможные вопросы:
Почему я использую
INSERT INTO #temp FROM @customAttrributeValues
?Ответ : Поскольку я заметил и теперь знаю, что при использовании переменных, включенных в запрос, любые оценки, которые получаются при работе с переменной, всегда равны 1. И я проверил помещение данных во временную таблицу, и тогда Оценочное значение равно Фактическим строкам. ,
Почему я использовал
and acav.CustomAttributeValue_Id in (select id from #temp)
?Ответ : Я мог бы заменить его JOIN на #temp, но разработчики были очень смущены и предложили этот
IN
вариант. Я действительно не думаю, что будет разница даже при замене, и в любом случае, с этим нет проблем.
источник
#temp
создание и использование будет проблемой для производительности, а не выигрышем. Вы сохраняете в неиндексированную таблицу только один раз. Попробуйте удалить его полностью (и, возможно,in (select id from #temp)
exists
select id from @customAttrValIds
вместо,select id from #temp
а предполагаемое количество строк было1
для переменной и3
для #temp (что соответствовало фактическому количеству строк). Вот почему я заменил@
на#
. И я ДЕЛАТЬ помню разговор (от Brent O или Aaron Bertrand) , где они сказали , что при использовании переменной TBL оценка для этого всегда будет 1. А как улучшение , чтобы получить лучшие оценки они будут использовать временную таблицу.Ответы:
План был скомпилирован на экземпляре окончательной первоначальной версии 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
таблице и выполнить отдельную операцию при загрузке таблицы:Кэширование статистики временной таблицы
Несмотря на использование
OPTION (RECOMPILE)
, SQL Server все еще может кэшировать объект временной таблицы и связанную с ней статистику между вызовами процедур. Как правило, это приветствуется оптимизация производительности, но если временная таблица заполняется аналогичным объемом данных о вызовах смежных процедур, перекомпилированный план может основываться на неверной статистике (кэшируется из предыдущего выполнения). Это подробно описано в моих статьях, « Временные таблицы в хранимых процедурах» и « Кэширование временных таблиц» .Чтобы избежать этого, используйте
OPTION (RECOMPILE)
вместе с явнымUPDATE STATISTICS #TempTable
после заполнения временной таблицы и перед тем, как на нее будет ссылаться запрос.Переписать запрос
В этой части предполагается, что изменения в создании
#Temp
таблицы уже сделаны.Учитывая стоимость возможных разливов хеша и избыточного агрегата (и окружающих обменов), он может заплатить за материализацию набора в узле 10:
PRIMARY KEY
Добавляются в отдельном шаге , чтобы обеспечить построение индекса имеет точную информацию кардинальной и избежать временных статистических таблиц кэширования вопроса.Эта материализация, скорее всего, произойдет в памяти (избегая ввода-вывода tempdb ), если у экземпляра достаточно памяти. Это еще более вероятно после обновления до SQL Server 2012 (SP1 CU10 / SP2 CU1 или более поздней версии), который улучшил поведение Eager Write .
Это действие дает оптимизатору точную информацию о количестве элементов в промежуточном наборе, позволяет ему создавать статистику и позволяет объявить его
(Assortment_Id, CustomAttrID)
в качестве ключа.План для заполнения
#Temp2
должен выглядеть следующим образом (обратите внимание, что сканирование кластеризованного индекса#Temp
не имеет четкой сортировки, и теперь обмен использует циклическое разбиение строк):При наличии этого набора окончательный запрос становится:
Мы могли бы вручную переписать их
COUNT_BIG(DISTINCT...
как простуюCOUNT_BIG(*)
, но с новой ключевой информацией оптимизатор сделает это за нас:Окончательный план может использовать соединение цикла / хеша / слияния в зависимости от статистической информации о данных, к которым у меня нет доступа. Еще одно небольшое замечание: я предположил, что такой индекс
CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
существует.В любом случае, важная вещь в окончательных планах состоит в том, что оценки должны быть намного лучше, а сложная последовательность операций группировки была сведена к одному агрегату потока (который не требует памяти и, следовательно, не может пролиться на диск).
Трудно сказать, что производительность в этом случае на самом деле будет лучше с использованием дополнительной временной таблицы, но оценки и выбор планов будут гораздо более устойчивыми к изменениям объема и распределения данных с течением времени. Это может быть более ценным в долгосрочной перспективе, чем небольшое повышение производительности сегодня. В любом случае, теперь у вас есть гораздо больше информации, на которой можно основывать свое окончательное решение.
источник
Оценки мощности по вашему запросу на самом деле очень хорошие. Редко получается, чтобы количество предполагаемых строк точно соответствовало количеству фактических строк, особенно если у вас есть столько объединений. Оценка кардинальности соединения - сложная задача для оптимизатора. Следует отметить одну важную вещь: количество оценочных строк для внутренней части вложенного цикла зависит от выполнения этого цикла. Поэтому, когда SQL Server сообщает, что 463869 строк будет извлечено с помощью поиска по индексу, реальной оценкой в этом случае будет число выполнений (2) * 463869 = 927738, которое не так уж далеко от фактического числа строк, 1391608. Удивительно, но количество предполагаемых строк почти идеально сразу после соединения с вложенным циклом в узле с идентификатором 10.
Плохие оценки количества элементов чаще всего являются проблемой, когда оптимизатор запросов выбирает неверный план или не предоставляет достаточно памяти для плана. Я не вижу каких-либо разливов для tempdb для этого плана, поэтому память выглядит хорошо. Для вызова вложенного цикла, который вы вызываете, у вас есть небольшая внешняя таблица и проиндексированная внутренняя таблица. Что в этом плохого? Чтобы быть точным, что бы вы ожидали, что оптимизатор запросов будет делать по-другому здесь?
С точки зрения повышения производительности, что выделяется мне, так это то, что SQL Server использует алгоритм хеширования для распределения параллельных строк, в результате чего все они находятся в одном потоке:
В результате один поток выполняет всю работу с поиском по индексу:
Это означает, что ваш запрос фактически не выполняется параллельно, пока оператор потоков перераспределения на узле с идентификатором 9. Вероятно, вам понадобится циклическое разбиение, чтобы каждая строка заканчивалась в своем собственном потоке. Это позволит двум потокам выполнять поиск индекса для идентификатора узла 17. Добавление лишнего
TOP
оператора может привести к циклическому разбиению. Я могу добавить детали здесь, если хотите.Если вы действительно хотите сосредоточиться на оценках мощности, вы можете поместить строки после первого соединения во временную таблицу. Если вы собираете статистику по временной таблице, которая дает оптимизатору больше информации о внешней таблице для соединения с вложенным циклом, которое вы вызвали. Это также может привести к круговому разбиению.
Если вы не используете флаги трассировки 4199 или 2301, вы можете рассмотреть их. Флаг трассировки 4199 предлагает множество различных исправлений оптимизатора, но они могут ухудшить некоторые рабочие нагрузки. Флаг трассировки 2301 изменяет некоторые предположения о количестве элементов в соединителе оптимизатора запросов и делает его более напряженным. В обоих случаях проверьте их перед включением.
источник
Я считаю, что получение более точной оценки этого объединения не изменит план, если только 1.4 миль не будет достаточной частью таблицы, чтобы оптимизатор выбрал сканирование индекса (а не кластера) с хеш-соединением или объединением слиянием. Я подозреваю, что это было бы не так, и на самом деле не полезно, но вы можете протестировать эффекты, заменив внутреннее соединение на CustomAttributeValues внутренним хеш-соединением и внутренним объединением слиянием .
Я также рассмотрел код более широко и не вижу способа улучшить его - мне было бы интересно, конечно, оказаться неправым. И если вам захочется опубликовать полную логику того, чего вы пытаетесь достичь, меня заинтересует другой взгляд.
источник
OPTION(FORCE ORDER)
, которое не позволяет оптимизатору переупорядочивать объединения из текстовой последовательности, а также многие другие оптимизации.Вы не собираетесь улучшаться из [некластеризованного] поиска индекса. Единственное, что лучше, чем поиск по некластерному индексу, - это поиск по кластерному индексу.
Кроме того, я был администратором SQL в течение последних десяти лет и разработчиком SQL в течение пяти лет до этого, и, по моему опыту, крайне редко можно найти улучшения для SQL-запросов, изучая план выполнения, который вы не могли ' найти другими способами. Основная причина создания плана выполнения заключается в том, что он часто предлагает отсутствующие индексы, которые можно добавить для повышения производительности.
Основной выигрыш в производительности будет заключаться в корректировке самого SQL-запроса, если там есть какие-то недостатки. Например, пару месяцев назад я получил функцию SQL, которая работает в 160 раз быстрее, переписав
SELECT UNION SELECT
сводную таблицу стилей для использования стандартногоPIVOT
оператора SQL .Итак, давайте посмотрим,
SELECT * INTO
как правило, менее эффективно, чем стандартINSERT Object1 (column list) SELECT column list
. Так что я бы переписал это. Затем, если Function1 была определена безWITH SCHEMABINDING
, добавлениеWITH SCHEMABINDING
предложения должно позволить ему работать быстрее.Вы выбрали много псевдонимов, которые не имеют смысла, например, псевдоним Object2 как Object3. Вы должны выбрать лучшие псевдонимы, которые не запутывают код. У вас есть «Object7.Column5 in (выберите Column1 из Object1)».
IN
пункты такого рода всегда более эффективно записываются какEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)
. Возможно, я должен был написать это по-другому.EXISTS
всегда будет по крайней мере так же хорошо, какIN
. Это не всегда лучше, но обычно так и есть.Кроме того, я сомневаюсь, что
option(recompile)
это улучшает производительность запросов здесь. Я бы протестировал его удаление.источник