SQL Server 2014: есть ли какое-либо объяснение непоследовательной оценки количества участников самостоятельного объединения?

27

Рассмотрим следующий план запросов в SQL Server 2014:

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

В плане запроса самосоединение ar.fId = ar.fIdдает оценку в 1 строку. Однако это логически противоречивая оценка: arсодержит 20,608строки и только одно отдельное значение fId(точно отраженное в статистике). Следовательно, это соединение создает полный перекрестный продукт строк ( ~424MMстрок), в результате чего запрос выполняется в течение нескольких часов.

Мне трудно понять, почему SQL Server предложил оценку, которая может быть легко доказана как несовместимая со статистикой. Любые идеи?

Начальное расследование и дополнительные детали

Основываясь на ответе Пола , кажется, что эвристика SQL 2012 и SQL 2014 для оценки мощности объединения должна легко справляться с ситуацией, когда необходимо сравнивать две идентичные гистограммы.

Я начал с вывода с флага трассировки 2363, но не смог понять это легко. Означает ли следующий фрагмент, что SQL Server сравнивает гистограммы для fIdи bIdдля оценки избирательности только используемого объединения fId? Если так, то это, очевидно, было бы неправильно. Или я неправильно читаю вывод флага трассировки?

Plan for computation:
  CSelCalcExpressionComparedToExpression( QCOL: [ar].fId x_cmpEq QCOL: [ar].fId )
Loaded histogram for column QCOL: [ar].bId from stats with id 3
Loaded histogram for column QCOL: [ar].fId from stats with id 1
Selectivity: 0

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

Вот полный сценарий repro , вот полный вывод из флага трассировки 2363 , а вот определения запросов и таблиц на случай, если вы хотите быстро просмотреть их, не открывая полный сценарий:

WITH cte AS (
    SELECT ar.fId, 
        ar.bId,
        MIN(CONVERT(INT, ar.isT)) AS isT,
        MAX(CONVERT(INT, tcr.isS)) AS isS
    FROM  #SQL2014MinMaxAggregateCardinalityBug_ar ar 
    LEFT OUTER JOIN #SQL2014MinMaxAggregateCardinalityBug_tcr tcr
        ON tcr.rId = 508
        AND tcr.fId = ar.fId
        AND tcr.bId = ar.bId
    GROUP BY ar.fId, ar.bId
)
SELECT s.fId, s.bId, s.isS, t.isS
FROM cte s 
JOIN cte t 
    ON t.fId = s.fId 
    AND t.isT = 1

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_ar (
    fId INT NOT NULL,
    bId INT NOT NULL,
    isT BIT NOT NULL
    PRIMARY KEY (fId, bId)
)

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_tcr (
    rId INT NOT NULL,
    fId INT NOT NULL,
    bId INT NOT NULL,
    isS BIT NOT NULL
    PRIMARY KEY (rId, fId, bId, isS)
)
Джефф Паттерсон
источник

Ответы:

23

Мне трудно понять, почему SQL Server предложил оценку, которая может быть легко доказана как несовместимая со статистикой.

консистенция

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

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

Начальная оценка

В вашем конкретном случае начальная оценка мощности объединения не выполняется для двух идентичных поддеревьев. . Форма дерева в это время:

  LogOp_Join
     LogOp_GbAgg
        LogOp_LeftOuterJoin
           LogOp_Get TBL: ar
           LogOp_Select
              LogOp_Get TBL: tcr
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [tcr] .rId
                 Значение ScaOp_Const = 508
           ScaOp_Logical x_lopAnd
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .fId
                 ScaOp_Identifier [tcr] .fId
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .bId
                 ScaOp_Identifier [tcr] .bId
        AncOp_PrjList 
           AncOp_PrjEl Expr1003 
              ScaOp_AggFunc stopMax
                 ScaOp_Convert int
                    ScaOp_Identifier [tcr] .isS
     LogOp_Select
        LogOp_GbAgg
           LogOp_LeftOuterJoin
              LogOp_Get TBL: ar
              LogOp_Select
                 LogOp_Get TBL: tcr
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [tcr] .rId
                    Значение ScaOp_Const = 508
              ScaOp_Logical x_lopAnd
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .fId
                    ScaOp_Identifier [tcr] .fId
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .bId
                    ScaOp_Identifier [tcr] .bId
           AncOp_PrjList 
              AncOp_PrjEl Expr1006 
                 ScaOp_AggFunc stopMin
                    ScaOp_Convert int
                       ScaOp_Identifier [ar] .isT
              AncOp_PrjEl Expr1007 
                 ScaOp_AggFunc stopMax
                    ScaOp_Convert int
                       ScaOp_Identifier [tcr] .isS
        ScaOp_Comp x_cmpEq
           ScaOp_Identifier Expr1006 
           Значение ScaOp_Const = 1
     ScaOp_Comp x_cmpEq
        ScaOp_Identifier QCOL: [ar] .fId
        ScaOp_Identifier QCOL: [ar] .fId

При первом входе соединения был упрощен непроецированный агрегат, а при втором входе соединения предикат t.isT = 1помещен под ним, где t.isTесть MIN(CONVERT(INT, ar.isT)). Несмотря на это, вычисление селективности для isTпредиката можно использовать CSelCalcColumnInIntervalна гистограмме:

  CSelCalcColumnInInterval
      Колонка: COL: Expr1006 

Загруженная гистограмма для столбца QCOL: [ar] .isT из статистики с идентификатором 3

Селективность: 4.85248e-005

Создана коллекция статистики: 
  CStCollFilter (ID = 11, CARD = 1)
      CStCollGroupBy (ID = 10, CARD = 20608)
          CStCollOuterJoin (ID = 9, CARD = 20608 x_jtLeftOuter)
              CStCollBaseTable (ID = 3, CARD = 20608 TBL: ar)
              CStCollFilter (ID = 8, CARD = 1)
                  CStCollBaseTable (ID = 4, CARD = 28 TBL: tcr)

(Правильное) ожидание - это 20 608 строк, которые будут уменьшены до 1 строки этим предикатом.

Присоединиться к оценке

Теперь возникает вопрос, как 20 608 строк из другого входного соединения будут совпадать с этой одной строкой:

  LogOp_Join
      CStCollGroupBy (ID = 7, CARD = 20608)
          CStCollOuterJoin (ID = 6, CARD = 20608 x_jtLeftOuter)
              ...

      CStCollFilter (ID = 11, CARD = 1)
          CStCollGroupBy (ID = 10, CARD = 20608)
              ...

      ScaOp_Comp x_cmpEq
          ScaOp_Identifier QCOL: [ar] .fId
          ScaOp_Identifier QCOL: [ar] .fId

Существует несколько разных способов оценки объединения в целом. Мы могли бы, например:

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

В зависимости от используемой оценки кардинальности и некоторых эвристик может быть использован любой из них (или вариация). Дополнительную информацию см. В официальном документе Microsoft « Оптимизация планов запросов с помощью оценщика мощности SQL Server 2014» .

Ошибка?

Теперь, как отмечено в вопросе, в этом случае «простое» одностолбцовое соединение (on fId) использует CSelCalcExpressionComparedToExpressionкалькулятор:

План для расчета:

  CSelCalcExpressionComparedToExpression [ar] .fId x_cmpEq [ar] .fId

Загруженная гистограмма для столбца QCOL: [ar] .bId из статистики с идентификатором 2
Загруженная гистограмма для столбца QCOL: [ar] .fId из статистики с идентификатором 1

Селективность: 0

Этот расчет оценивает, что объединение 20 608 строк с 1 отфильтрованной строкой будет иметь нулевую селективность: ни одна строка не будет соответствовать (сообщается как одна строка в окончательных планах). Это неправильно? Да, возможно, здесь есть ошибка в новом CE. Можно утверждать, что 1 строка будет соответствовать всем строкам или ни одной, поэтому результат может быть разумным, но есть основания полагать иначе.

Детали на самом деле довольно хитры, но ожидание, что оценка будет основана на нефильтрованных fIdгистограммах, измененных селективностью фильтра, дающего 20608 * 20608 * 4.85248e-005 = 20608строки, очень разумно.

Следование этим расчетам будет означать использование калькулятора CSelCalcSimpleJoinWithDistinctCountsвместо CSelCalcExpressionComparedToExpression. Нет задокументированного способа сделать это, но если вам интересно, вы можете включить недокументированный флаг трассировки 9479:

9479 план

Обратите внимание, что при окончательном объединении получается 20 608 строк из двух однорядных входных данных, но это не должно быть сюрпризом. Это тот же план, который был разработан оригинальным CE под TF 9481.

Я упоминал, что детали сложны (и требуют много времени для исследования), но, насколько я могу судить, основная причина проблемы связана с предикатом rId = 508с нулевой избирательностью. Эта нулевая оценка повышается до одной строки обычным способом, что, по-видимому, способствует оценке нулевой избирательности в рассматриваемом соединении, когда она учитывает более низкие предикаты во входном дереве (отсюда загрузка статистики для bId).

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

9473 план

Поведение оценки мощности соединения с CSelCalcExpressionComparedToExpressionпомощью также может быть изменено, чтобы не учитывать `` bId` с другим недокументированным флагом вариации (9494). Я упоминаю все это, потому что я знаю, что вы заинтересованы в таких вещах; не потому что они предлагают решение. До тех пор, пока вы не сообщите Microsoft о проблеме, и они не решат ее (или нет), возможно, наилучшим способом продвижения вперед будет выражение запроса. Независимо от того, является ли поведение преднамеренным или нет, им должно быть интересно услышать о регрессии.

Наконец, чтобы привести в порядок еще одну вещь, упомянутую в сценарии воспроизведения: конечная позиция фильтра в плане вопроса - это результат исследования на основе затрат, в результате которого GbAggAfterJoinSelагрегат и фильтр перемещаются над объединением, поскольку выходные данные объединения имеют такой маленький размер. количество рядов Фильтр изначально был ниже объединения, как вы и ожидали.

Пол Уайт говорит, что GoFundMonica
источник