Изменить запрос, чтобы улучшить оценки операторов

14

У меня есть запрос, который выполняется в приемлемое время, но я хочу извлечь из него максимально возможную производительность.

Операция, которую я пытаюсь улучшить, - это «Поиск индекса» справа от плана с узла 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

ответы:

  1. Почему странное начальное наименование в ссылке pasteThePlan?

    Ответ : потому что я использовал анонимный план из SQL Sentry Plan Explorer.

  2. Почему OPTION RECOMPILE?

    Ответ : Потому что я могу позволить себе перекомпиляцию, чтобы избежать перехвата параметров (данные могут / могут быть искажены). Я проверил и доволен планом, который оптимизатор создает во время использования OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Ответ : Я бы очень хотел избежать этого и использовал бы его только тогда, когда у меня есть индексированное представление. В любом случае, это системная функция ( COUNT()), поэтому здесь нет смысла SCHEMABINDING.

Ответы на более возможные вопросы:

  1. Почему я использую INSERT INTO #temp FROM @customAttrributeValues?

    Ответ : Поскольку я заметил и теперь знаю, что при использовании переменных, включенных в запрос, любые оценки, которые получаются при работе с переменной, всегда равны 1. И я проверил помещение данных во временную таблицу, и тогда Оценочное значение равно Фактическим строкам. ,

  2. Почему я использовал and acav.CustomAttributeValue_Id in (select id from #temp)?

    Ответ : Я мог бы заменить его JOIN на #temp, но разработчики были очень смущены и предложили этот INвариант. Я действительно не думаю, что будет разница даже при замене, и в любом случае, с этим нет проблем.

Раду Георгиу
источник
Я предполагаю, что #tempсоздание и использование будет проблемой для производительности, а не выигрышем. Вы сохраняете в неиндексированную таблицу только один раз. Попробуйте удалить его полностью (и, возможно, in (select id from #temp)exists
измените
@ ypercubeᵀᴹ Правда, примерно на несколько страниц читается с использованием переменной вместо временной таблицы.
Раду Георгиу
Кстати, табличная переменная будет предоставлять правильную оценку количества строк при использовании с Option (Перекомпилировать) - но все равно не будет иметь детализированной статистики, количества
TH
@TH Ну, я смотрел в фактическом плане выполнения оценки, когда select id from @customAttrValIdsвместо, select id from #tempа предполагаемое количество строк было 1для переменной и 3для #temp (что соответствовало фактическому количеству строк). Вот почему я заменил @на #. И я ДЕЛАТЬ помню разговор (от Brent O или Aaron Bertrand) , где они сказали , что при использовании переменной TBL оценка для этого всегда будет 1. А как улучшение , чтобы получить лучшие оценки они будут использовать временную таблицу.
Раду Георгиу
@RaduGheorghiu Да, но в мире этих парней опция (перекомпиляция) редко встречается, и они также предпочитают временные таблицы по другим уважительным причинам. Возможно, оценка просто всегда неверно отображается как 1, так как она меняет план, как показано здесь: theboreddba.com/Categories/FunWithFlags/…
TH

Ответы:

12

План был скомпилирован на экземпляре окончательной первоначальной версии 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), но входные строки равны выходным строкам:

Узел 8 Hash Match (Совокупный)

Это говорит о том, что комбинация столбцов является ключом (так что группировка семантически не нужна). Стоимость выполнения избыточного агрегата увеличивается из-за необходимости дважды передавать 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не имеет четкой сортировки, и теперь обмен использует циклическое разбиение строк):

# Temp2 население

При наличии этого набора окончательный запрос становится:

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);существует.

В любом случае, важная вещь в окончательных планах состоит в том, что оценки должны быть намного лучше, а сложная последовательность операций группировки была сведена к одному агрегату потока (который не требует памяти и, следовательно, не может пролиться на диск).

Трудно сказать, что производительность в этом случае на самом деле будет лучше с использованием дополнительной временной таблицы, но оценки и выбор планов будут гораздо более устойчивыми к изменениям объема и распределения данных с течением времени. Это может быть более ценным в долгосрочной перспективе, чем небольшое повышение производительности сегодня. В любом случае, теперь у вас есть гораздо больше информации, на которой можно основывать свое окончательное решение.

Пол Уайт 9
источник
9

Оценки мощности по вашему запросу на самом деле очень хорошие. Редко получается, чтобы количество предполагаемых строк точно соответствовало количеству фактических строк, особенно если у вас есть столько объединений. Оценка кардинальности соединения - сложная задача для оптимизатора. Следует отметить одну важную вещь: количество оценочных строк для внутренней части вложенного цикла зависит от выполнения этого цикла. Поэтому, когда SQL Server сообщает, что 463869 строк будет извлечено с помощью поиска по индексу, реальной оценкой в ​​этом случае будет число выполнений (2) * 463869 = 927738, которое не так уж далеко от фактического числа строк, 1391608. Удивительно, но количество предполагаемых строк почти идеально сразу после соединения с вложенным циклом в узле с идентификатором 10.

Плохие оценки количества элементов чаще всего являются проблемой, когда оптимизатор запросов выбирает неверный план или не предоставляет достаточно памяти для плана. Я не вижу каких-либо разливов для tempdb для этого плана, поэтому память выглядит хорошо. Для вызова вложенного цикла, который вы вызываете, у вас есть небольшая внешняя таблица и проиндексированная внутренняя таблица. Что в этом плохого? Чтобы быть точным, что бы вы ожидали, что оптимизатор запросов будет делать по-другому здесь?

С точки зрения повышения производительности, что выделяется мне, так это то, что SQL Server использует алгоритм хеширования для распределения параллельных строк, в результате чего все они находятся в одном потоке:

дисбаланс нитей

В результате один поток выполняет всю работу с поиском по индексу:

поиск дисбаланса нити

Это означает, что ваш запрос фактически не выполняется параллельно, пока оператор потоков перераспределения на узле с идентификатором 9. Вероятно, вам понадобится циклическое разбиение, чтобы каждая строка заканчивалась в своем собственном потоке. Это позволит двум потокам выполнять поиск индекса для идентификатора узла 17. Добавление лишнего TOPоператора может привести к циклическому разбиению. Я могу добавить детали здесь, если хотите.

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

Если вы не используете флаги трассировки 4199 или 2301, вы можете рассмотреть их. Флаг трассировки 4199 предлагает множество различных исправлений оптимизатора, но они могут ухудшить некоторые рабочие нагрузки. Флаг трассировки 2301 изменяет некоторые предположения о количестве элементов в соединителе оптимизатора запросов и делает его более напряженным. В обоих случаях проверьте их перед включением.

Джо Оббиш
источник
-2

Я считаю, что получение более точной оценки этого объединения не изменит план, если только 1.4 миль не будет достаточной частью таблицы, чтобы оптимизатор выбрал сканирование индекса (а не кластера) с хеш-соединением или объединением слиянием. Я подозреваю, что это было бы не так, и на самом деле не полезно, но вы можете протестировать эффекты, заменив внутреннее соединение на CustomAttributeValues внутренним хеш-соединением и внутренним объединением слиянием .

Я также рассмотрел код более широко и не вижу способа улучшить его - мне было бы интересно, конечно, оказаться неправым. И если вам захочется опубликовать полную логику того, чего вы пытаетесь достичь, меня заинтересует другой взгляд.

TH
источник
3
Существует очень большое количество планов для этого запроса со множеством вариантов порядка и вложенности соединений, параллелизма, локальной / глобальной агрегации и т. Д. И т. Д., На большинство из которых будут влиять изменения в производной статистике (распределение, а также необработанное количество элементов) на узле плана 10. Обратите внимание также, что в общем случае следует избегать подсказок о соединении, поскольку они содержат молчание OPTION(FORCE ORDER), которое не позволяет оптимизатору переупорядочивать объединения из текстовой последовательности, а также многие другие оптимизации.
Пол Уайт 9
-12

Вы не собираетесь улучшаться из [некластеризованного] поиска индекса. Единственное, что лучше, чем поиск по некластерному индексу, - это поиск по кластерному индексу.

Кроме того, я был администратором SQL в течение последних десяти лет и разработчиком SQL в течение пяти лет до этого, и, по моему опыту, крайне редко можно найти улучшения для SQL-запросов, изучая план выполнения, который вы не могли ' найти другими способами. Основная причина создания плана выполнения заключается в том, что он часто предлагает отсутствующие индексы, которые можно добавить для повышения производительности.

Основной выигрыш в производительности будет заключаться в корректировке самого SQL-запроса, если там есть какие-то недостатки. Например, пару месяцев назад я получил функцию SQL, которая работает в 160 раз быстрее, переписав SELECT UNION SELECTсводную таблицу стилей для использования стандартного PIVOTоператора SQL .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Итак, давайте посмотрим, 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)это улучшает производительность запросов здесь. Я бы протестировал его удаление.

Мэтью Сонтум
источник
6
Если поиск по некластерному индексу покрывает запрос, он почти всегда будет лучше, чем поиск по кластерному индексу, потому что по определению в кластеризованном индексе есть все столбцы, а в некластеризованном индексе меньше столбцов, поэтому потребуется меньше просмотров страниц (и меньше уровней шагов в b-дерево), чтобы получить данные. Поэтому не совсем верно утверждать, что поиск по кластерному индексу всегда будет лучше.
ErikE