Почему эта оценка кардинальности соединения настолько велика?


18

Я испытываю невероятно высокую оценку количества элементов для следующего запроса:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

Ориентировочный план здесь . Я работаю над статистической копией таблиц, поэтому не могу включить фактический план. Однако я не думаю, что это очень актуально для этой проблемы.

По оценкам SQL Server, из производной таблицы «dm» будет возвращено 481577 строк. Затем он оценивает, что 4528030000 строк будет возвращено после выполнения соединения с X_LAST_TABLE, но JOIN_ID является первичным ключом X_LAST_TIME. Я ожидаю, что оценка кардинальности соединения будет между 0 и 481577 строками. Вместо этого оценка строки составляет 10% от числа строк, которые я получу при перекрестном соединении внешней и внутренней таблиц. Математика для этого работает с округлением: 481577 * 94025 * 0,1 = 45280277425, округленное до 4528030000.

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

Я тестирую на Microsoft SQL Server 2014 с включенной устаревшей оценкой мощности. TF 4199 и другие включены. Я могу дать полный список флагов трассировки, если это окажется актуальным.

Вот наиболее подходящее определение таблицы:

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

Я также написал все сценарии создания таблиц и их статистику, если кто-то захочет воспроизвести проблему на одном из своих серверов.

Чтобы добавить несколько моих наблюдений, использование TF 2312 фиксирует оценку, но это не вариант для меня. TF 2301 не фиксирует оценку. Удаление одной из таблиц фиксирует оценку. Как ни странно, изменение порядка соединения X_DETAIL_LINK также исправляет оценку. Под изменением порядка соединения я подразумеваю переписывание запроса, а не форсирование порядка соединения с подсказкой. Вот примерный план запроса при изменении порядка соединений.


PS Если вы можете каким-либо образом переключиться на bigintвместо того, decimal(18, 0)чтобы получить преимущества: 1) используйте 8 байтов вместо 9 для каждого значения и 2) используйте сопоставимый с байтами тип данных вместо упакованного типа данных, что может иметь последствия для процессорного времени при сравнении значений.
ErikE

@ErikE Спасибо за совет, но я это уже знал. К сожалению, мы застряли с NUMERIC (18,0) над BIGINT по устаревшим причинам.
Джо Оббиш

Это стоило того!
ErikE

Нужны ли вам таблицы X_DETAIL2and, X_DETAIL3если они JOIN_IDне равны null X_DETAIL1?
ErikE

@ErikE Это MCVE, поэтому на данный момент запрос не имеет смысла.
Джо Оббиш

Ответы:


14

Я знаю, что делать COALESCEнесколько столбцов и объединять их - не очень хорошая практика.

Генерировать хорошие оценки мощности и распределения достаточно сложно, когда схема имеет формат 3NF + (с ключами и ограничениями), а запрос реляционный и в основном SPJG (selection-projection-join-group by). Модель CE построена на этих принципах. Чем больше необычных или нереляционных функций в запросе, тем ближе он подходит к границам возможностей кардинальности и селективности. Зайди слишком далеко, и CE сдастся и угадай .

Большая часть примера MCVE - это простое SPJ (без G), хотя и с преимущественно внешними эквивалентами (смоделированными как внутреннее соединение плюс анти-полусоединение), а не с более простым внутренним эквивалентным соединением (или полусоединением). Все отношения имеют ключи, хотя никаких внешних ключей или других ограничений. Все соединения, кроме одного, относятся ко многим, и это хорошо.

Исключением является внешнее соединение « многие ко многим» между X_DETAIL_1и X_DETAIL_LINK. Единственная функция этого объединения в MCVE - потенциально дублировать строки в X_DETAIL_1. Это необычная вещь.

Простые предикаты равенства (выборки) и скалярные операторы также лучше. Например, атрибут сравнения-равный атрибут / константа обычно хорошо работает в модели. Относительно «легко» модифицировать гистограммы и статистику частоты, чтобы отразить применение таких предикатов.

COALESCEосновывается на том CASE, что в свою очередь реализовано как IIF(и это было верно задолго до того, как IIFпоявилось в языке Transact-SQL). СЕ моделирует IIFкак UNIONс двумя взаимоисключающими детьми, каждый из которых состоит из проекта по выбору входного отношения. Каждый из перечисленных компонентов имеет поддержку модели, поэтому объединить их относительно просто. Тем не менее, чем больше однослойных абстракций, тем менее точным будет конечный результат - причина того, почему большие планы выполнения, как правило, менее стабильны и надежны.

ISNULLс другой стороны, это присуще двигателю. Он не построен с использованием каких-либо более основных компонентов. ISNULLНапример, применить эффект к гистограмме так же просто, как заменить шаг для NULLзначений (и при необходимости сжать). Это все еще относительно непрозрачно, поскольку скалярные операторы идут, и поэтому лучше избегать, где это возможно. Тем не менее, как правило, он более дружественный по отношению к оптимизатору (менее дружественный по отношению к оптимизатору), чем CASEальтернативный вариант на основе.

CE (70 и 120+) очень сложен, даже по стандартам SQL Server. Это не тот случай применения простой логики (с секретной формулой) к каждому оператору. CE знает о ключах и функциональных зависимостях; он умеет оценивать, используя частоты, многомерную статистику и гистограммы; и есть абсолютная тонна особых случаев, уточнений, сдержек и противовесов и вспомогательных структур. Он часто оценивает, например, соединения несколькими способами (частота, гистограмма) и принимает решение о результате или корректировке на основе различий между ними.

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

Эта единственная первоначальная комплексная оценка обеспечивает отправную точку и происходит задолго до того, как какое-либо внимание будет уделено окончательному плану выполнения (это происходит задолго до стадии составления тривиального плана). На этом этапе дерево запросов имеет тенденцию достаточно точно отражать письменную форму запроса (хотя с удаленными подзапросами, примененными упрощениями и т. Д.)

Сразу после первоначальной оценки SQL Server выполняет эвристическое переупорядочение соединений, которое, собственно говоря, пытается переупорядочить дерево для размещения меньших таблиц и соединений с высокой селективностью. Он также пытается позиционировать внутренние соединения перед тем, как внешние соединения и перекрестные продукты. Его возможности не обширны; его усилия не являются исчерпывающими; и он не учитывает физические затраты (поскольку они еще не существуют - присутствуют только статистическая информация и информация метаданных). Эвристическое переупорядочение наиболее успешно на простых деревьях внутреннего равновесия. Он существует, чтобы обеспечить «лучшую» отправную точку для оптимизации на основе затрат.

Почему эта оценка кардинальности соединения настолько велика?

MCVE имеет «необычное» в основном избыточное соединение « многие ко многим» и равное соединение с COALESCEпредикатом. Дерево операторов также имеет последнее внутреннее объединение , при котором порядок эвристического объединения не смог переместить дерево вверх в более предпочтительную позицию. Оставляя в стороне все скаляры и проекции, дерево соединений:

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

Обратите внимание, что ошибочная окончательная оценка уже установлена. Он печатается как Card=4.52803e+009и сохраняется внутри как значение с плавающей запятой двойной точности 4.5280277425e + 9 (4528027742.5 в десятичном виде).

Производная таблица в исходном запросе была удалена, а проекции нормализованы. SQL-представление дерева, на котором была выполнена начальная оценка мощности и кардинальности:

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(Кроме того, повторение COALESCEтакже присутствует в окончательном плане - один раз в последнем вычислительном скаляре и один раз на внутренней стороне внутреннего соединения).

Обратите внимание на финальное соединение. Это внутреннее соединение является (по определению) декартовым произведением X_LAST_TABLEи предыдущим выходным соединением с примененным выбором (предикатом соединения) lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID). Кардинальность декартового произведения - это просто 481577 * 94025 = 45280277425.

Для этого нам нужно определить и применить селективность предиката. Комбинация непрозрачного расширенного COALESCEдерева (с точки зрения UNIONи IIF, помните) вместе с воздействием на ключевую информацию, производными гистограммами и частотами ранее «необычного» комбинированного внешнего объединения «многие ко многим» означает, что CE не способен получить приемлемую оценку любым из нормальных способов.

В результате он входит в Guess Logic. Логика догадок умеренно сложна, со слоями «образованных» догадок и «не очень образованных» алгоритмов угадывания. Если лучшего основания для предположения не найдено, модель использует предположение последней инстанции, которое для сравнения на равенство: sqllang!x_Selectivity_Equal= фиксированная селективность 0,1 (10% предположения):

Стек вызовов

-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

Результатом является селективность 0,1 по декартовому продукту: 481577 * 94025 * 0,1 = 4528027742,5 (~ 4,52803e + 009), как указано выше.

Перезапись

Когда проблемное объединение закомментировано , получается лучшая оценка, поскольку избегается «предположение о последней инстанции» с фиксированной избирательностью (ключевая информация сохраняется объединениями 1-M). Качество оценки по-прежнему не совсем достоверно, поскольку COALESCEпредикат объединения вовсе не соответствует CE. Я полагаю, что пересмотренная оценка, по крайней мере, выглядит более разумной для людей.

Когда запрос записывается с внешним соединением, X_DETAIL_LINK помещенным последним , эвристическое переупорядочение может поменять его с последним внутренним соединением X_LAST_TABLE. Помещение внутреннего объединения прямо рядом с проблемой « Внешнее объединение» дает ограниченным возможностям раннего переупорядочения возможность улучшить окончательную оценку, поскольку эффекты в основном избыточного «необычного» внешнего объединения «многие ко многим» наступают после сложной оценки избирательности. для COALESCE. Опять же, оценки немного лучше, чем фиксированные догадки, и, вероятно, не выдержит решительного перекрестного допроса в суде.

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

Вложенный ISNULLпредложенный в ответе Макса Вернона удается избежать фиксированного предположения о спасении, но окончательной оценкой является невероятный ноль строк (для приличия поднятый на одну строку). Это также может быть фиксированное предположение о 1 строке для всей статистической основы, которую имеет вычисление.

Я ожидаю, что оценка кардинальности соединения будет между 0 и 481577 строками.

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

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

Когда я попробовал MCVE, 120+ CE вывел нулевую (= 1) итоговую оценку строки (как вложенную ISNULL) для исходного запроса, что также неприемлемо для моего мышления.

Реальное решение, вероятно, включает в себя изменение дизайна, чтобы позволить простые экви-соединения без COALESCEили ISNULL, и в идеале внешние ключи и другие ограничения, полезные для компиляции запроса.


10

Я полагаю, что Compute Scalarоператор, COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)являющийся результатом присоединения, X_LAST_TABLE.JOIN_IDявляется основной причиной проблемы. Исторически сложилось так, что вычислить скаляры было трудно точно 1 , 2 .

Поскольку вы предоставили минимально полный проверяемый пример (спасибо!) С точной статистикой, я могу переписать запрос так, чтобы объединение больше не требовало расширенных CASEфункциональных возможностей, что COALESCEпривело к гораздо более точным оценкам строк и, очевидно, к большей Точная общая стоимость См. приложение в конце. :

SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);

Хотя xID IS NOT NULLтехнически это не требуется, поскольку ID = JOIN_IDони не будут объединяться с нулевыми значениями, я включил их, поскольку они более четко отображают намерение.

План 1 и План 2

План 1:

введите описание изображения здесь

План 2:

введите описание изображения здесь

Новый запрос выигрывает (?) От распараллеливания. Также следует отметить, что у нового запроса есть выходное число строк, равное 1, что на самом деле может оказаться хуже в конце дня, чем оценка 4528030000 для исходного запроса. Стоимость поддерева для оператора выбора в новом запросе составляет 243210, а первоначальная - 536,535, что явно меньше. Сказав это, я не верю, что первая оценка приблизилась к реальности.


Приложение 1.

После дальнейших консультаций с разными людьми по поводу The Heap ™, подстегнутых обсуждением с @Lamak, кажется, что мой вышеописанный запрос работает ужасно, даже с параллелизмом. Решение , которое позволяет как производительность хорошей и хорошие оценки мощности состоит в замене COALESCE(x,y,z)с ISNULL(ISNULL(x, y), z), как в:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

COALESCECASEоптимизатор запросов превращает в оператор "под прикрытием". Таким образом, оценщику кардинальности сложнее найти надежную статистику для столбцов, скрытых внутри COALESCE. ISNULLвнутренняя функция гораздо более «открыта» для оценки мощности. Также ничего ISNULLне стоит оптимизировать, если известно, что цель не обнуляется.

План для ISNULLварианта выглядит так:

введите описание изображения здесь

(Вставьте версию плана здесь ).

К вашему сведению, подпишитесь на Sentry One за их замечательный Plan Explorer, который я использовал для создания графических планов выше.


-1

В соответствии с вашим условием соединения, таблица может быть организована очень многими способами, это «изменение в один конкретный способ» исправит результат.

Предположим, что объединение только одной таблицы даст вам правильный результат.

SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID

Здесь вместо X_DETAIL_1, вы можете использовать либоX_DETAIL_2 или X_DETAIL_3.

Поэтому цель отдыха 2 таблицы не ясна.

Это как у вас сломать стол X_DETAIL_1 на 2 части.

Скорее всего , « есть ошибка , где вы заполнения этих таблиц. » В идеале X_DETAIL_1, X_DETAIL_2иX_DETAIL_3 должны содержать одинаковое количество строк.

Но одна или несколько таблиц содержат нежелательное количество строк.

Извините, если я ошибаюсь.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.