Как оптимизировать медленный запрос на вложенных циклах (внутреннее объединение)

39

TL; DR

Поскольку этот вопрос продолжает получать взгляды, я кратко изложу его здесь, чтобы новичкам не пришлось страдать от истории:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

Я понимаю, что это может быть проблемой не всех, но, подчеркнув чувствительность предложений ON, это может помочь вам смотреть в правильном направлении. В любом случае оригинальный текст здесь для будущих антропологов:

Оригинальный текст

Рассмотрим следующий простой запрос (задействованы только 3 таблицы)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

Это довольно простой запрос, единственная запутанная часть - последнее объединение категории, именно так, потому что категория 5 уровня может существовать или не существовать. В конце запроса я ищу информацию о категории для каждого идентификатора продукта (SKU ID), и именно здесь появляется очень большая таблица category_link. Наконец, таблица #Ids - это просто временная таблица, содержащая 10 000 идентификаторов.

При выполнении я получаю следующий фактический план выполнения:

Фактический план выполнения

Как вы можете видеть, почти 90% времени проводится во вложенных циклах (Inner Join). Вот дополнительная информация об этих вложенных циклах:

Вложенные циклы (внутреннее соединение)

Обратите внимание, что имена таблиц не совпадают точно, потому что я отредактировал имена таблиц запросов для удобства чтения, но их довольно легко сопоставить (ads_alt_category = category). Есть ли способ оптимизировать этот запрос? Также обратите внимание, что в производственной среде временная таблица #Ids не существует, это параметр с табличным значением тех же 10 000 идентификаторов, которые передаются в хранимую процедуру.

Дополнительная информация:

  • индексы категорий по category_id и parent_category_id
  • указатель категории по номеру для идентификатора категории, код_языка
  • указатель category_link по sku_id, category_id

Редактировать (решено)

Как указано в принятом ответе, проблема заключалась в предложении OR в соединении category_link JOIN. Однако код, предложенный в принятом ответе, очень медленный, даже медленнее, чем исходный код. Более быстрое, а также более чистое решение - просто заменить текущее условие JOIN следующим:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

Эта минутная настройка является самым быстрым решением, проверенным на соответствие двойному соединению из принятого ответа, а также проверенным на CROSS APPLY, как предложено valverij.

Луис Феррао
источник
Нам нужно увидеть остальную часть плана запроса.
RBarryYoung
Просто замечание: при таком количестве зависимых объединений ошибки оценки мощности становятся вероятными. Чаще всего производительность запросов снижается из-за недооценки мощности.
USR
Есть ли в плане выполнения предложения для индексов? Кроме того, не забывайте, что вы можете установить первичные ключи и индексы для ваших временных таблиц (более подробная информация здесь )
@rbarry Если, попробовав текущие решения, я ничего не получу, я улучшу этот вопрос
1
Как насчет дублирования запроса с помощью UNION и избавления от ИЛИ

Ответы:

17

Проблема, кажется, находится в этой части кода:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

orв условиях соединения всегда подозрительно. Одно предложение состоит в том, чтобы разделить это на два соединения:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

Затем вам нужно изменить остальную часть запроса, чтобы справиться с этим. , , coalesce(l1.sku_id, l2.sku_id)например в selectпункте.

Гордон Линофф
источник
С количеством фильтрации делается на том , что особенно присоединиться, я бы также проверить изменяя JOINк CROSS APPLYс INпереключаясь на EXISTSв APPLY«s WHEREп.
Спасибо Гордон, я проверю это первым делом утром. @Valverij, я не знаком с кросс-аппликацией, не могли бы вы подробнее описать свое решение, может быть, в правильном ответе, чтобы я мог голосовать, если это окажется самый быстрый сценарий?
3
Я принимаю этот ответ, потому что это был первый ответ, который указал мне на проблему. Однако предлагаемое решение является чрезвычайно медленным, даже медленнее, чем исходный код. Однако, зная, что предложение OR было проблемой, просто заменив его, ON l.category_id = ISNULL(c5.category_id, c4.category_idсделал свое дело.
Луис Феррао
1
@LuisFerrao. , , Спасибо за дополнительную информацию. Полезно знать, что coalesce()толкает оптимизатор в правильном направлении.
Гордон Линофф
9

Как упомянул другой пользователь, это соединение, вероятно, является причиной:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

Помимо разделения их на несколько соединений, вы также можете попробовать CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

Из ссылки MSDN выше:

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

По сути, APPLYэто как подзапрос, который сначала отфильтровывает записи справа, а затем применяет их к остальной части вашего запроса.

Эта статья очень хорошо объясняет, что это такое и когда его использовать: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Важно отметить, однако, что CROSS APPLYне всегда работает быстрее, чем INNER JOIN. Во многих ситуациях это, вероятно, будет примерно таким же. В редких случаях, тем не менее, я на самом деле видел это медленнее (опять же, все это зависит от структуры вашей таблицы и самого запроса).

Как правило, если я присоединяюсь к столу со слишком большим количеством условных выражений, то склоняюсь к APPLY

Также забавная заметка: OUTER APPLYбудет действовать какLEFT JOIN

Кроме того, пожалуйста, обратите внимание на мой выбор, чтобы использовать, EXISTSа не IN. При выполнении INподзапроса помните, что он вернет весь набор результатов, даже после того, как найдет ваше значение. Однако EXISTSпри этом он остановит подзапрос, как только найдет совпадение.

valverij
источник
Я тщательно протестировал это решение. Пока вы это писали, это довольно медленно, но вы забыли применить совет, с которого начали свое сообщение. Замена AND x.cat = c4.cat OR x.cat = c5.catпо x.cat = ISNULL(c5.cat, c4.cat)и избавлении от пункта IN сделал это второе самое быстрое решение, и достойным upvote, потому что это довольно информативны.
Луис Феррао
Спасибо. Строка IN на самом деле не должна была быть там (я не могла выбрать IN или придерживаться OR), я ее уберу.
valverij