Постоянный вычисляемый столбец, вызывающий сканирование

9

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

Протестировано на нескольких версиях SQL Server, включая 2016 SP1 CU1.

Repros

Беда в том , с table1, col7.

Таблицы и запросы являются частичной (и упрощенной) версией оригиналов. Я знаю, что запрос может быть переписан по-другому, и по какой-то причине избежать этой проблемы, но мы должны избегать прикосновения к коду, и вопрос о том, почему table1его нельзя найти, все еще стоит.

Как показал Пол Уайт (спасибо!), Поиск возможен, если принудительно, поэтому возникает вопрос: почему поиск не выбран оптимизатором, и можем ли мы сделать что-то по-другому, чтобы поиск происходил так, как должен, без изменения запроса? код?

Чтобы выяснить проблемную часть, вот соответствующее сканирование в плане плохого выполнения:

план

Алекс Фридман
источник

Ответы:

12

Почему поиск не выбран оптимизатором


TL: DR Расширенное определение вычисляемого столбца влияет на способность оптимизатора изначально изменять порядок соединений. С другой отправной точкой оптимизация на основе затрат проходит другой путь через оптимизатор и заканчивается другим выбором окончательного плана.


подробности

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

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

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

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

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

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

Используемые деревья довольно большие, но для иллюстрации начальное дерево запросов для невычисляемых столбцов начинается с: (обратите внимание на два внешних соединения в верхней части)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NAryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NAryJoin
                        LogOp_Get TBL: dbo.table1 (псевдоним TBL: a4)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table6 (псевдоним TBL: a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a3] .col18
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table1 (псевдоним TBL: a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table5 (псевдоним TBL: a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2] .col2
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4] .col2
                            ScaOp_Identifier QCOL: [a3] .col19
                    LogOp_Select
                        LogOp_Get TBL: dbo.table7 (псевдоним TBL: a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a7] .col22
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a7] .col23
                LogOp_Select
                    LogOp_Get TBL: таблица1 (псевдоним TBL: cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc] .col6
                        ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, не принадлежит, Value = 4)
                LogOp_Get TBL: dbo.table5 (псевдоним TBL: a5) 
                LogOp_Get TBL: таблица2 (псевдоним TBL: cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a5] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt] .col1
                        ScaOp_Identifier QCOL: [cdc] .col1
            LogOp_Get TBL: таблица3 (псевдоним TBL: ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr] .col9
                ScaOp_Identifier QCOL: [cdt] .col1

Тот же фрагмент запроса вычисляемого столбца : (обратите внимание, что внешнее соединение гораздо ниже, расширенное определение вычисляемого столбца и некоторые другие тонкие различия в (внутреннем) порядке объединения)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NAryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NAryJoin
                    LogOp_Get TBL: dbo.table1 (псевдоним TBL: a4)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table6 (псевдоним TBL: a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a3] .col18
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table1 (псевдоним TBL: a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table5 (псевдоним TBL: a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2] .col2
                            ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a3] .col19
                LogOp_Select
                    LogOp_Get TBL: dbo.table7 (псевдоним TBL: a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a7] .col22
                        ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [a7] .col23
            LogOp_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        LogOp_Select
                            LogOp_Get TBL: таблица1 (псевдоним TBL: cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc] .col6
                                ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, не принадлежит, Value = 4)
                        LogOp_Get TBL: таблица2 (псевдоним TBL: cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc] .col1
                            ScaOp_Identifier QCOL: [cdt] .col1
                    LogOp_Get TBL: таблица3 (псевдоним TBL: ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr] .col9
                        ScaOp_Identifier QCOL: [cdt] .col1
                AncOp_PrjList 
                    AncOp_PrjEl QCOL: [cdc] .col7
                        ScaOp_Convert char collate 53256, Null, Trim, ML = 6
                            ScaOp_IIF varchar collate 53256, Null, Var, Trim, ML = 6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_Intrinsic right
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, не принадлежит, Value = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, не принадлежит, значение = 0)
                                ScaOp_Const TI (varchar collate 53256, Var, Trim, ML = 1) XVAR (varchar, Owned, Value = Len, Data = (0,))
                                ScaOp_Intrinsic подстрока
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, не принадлежит, Value = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, не принадлежит, Value = 1)
                                    ScaOp_Identifier QCOL: [cdc] .col4
            LogOp_Get TBL: dbo.table5 (псевдоним TBL: a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a5] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2

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

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


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

Можем ли мы сделать что-то по-другому, чтобы добиться успеха

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

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

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

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


Дополнительные вопросы из комментариев

Я согласен, что лучше всего упростить запрос и т. Д., Но есть ли способ (флаг трассировки), чтобы оптимизатор продолжил оптимизацию и достиг того же результата?

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

Кроме того, зачем нужно сложное расширение, поскольку столбец сохраняется? Почему оптимизатор не может избежать его расширения, рассматривать его как обычный столбец и достигать той же начальной точки?

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

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