Чому пошук не вибирається оптимізатором
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: table1 (псевдонім TBL: cdc)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col6
ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, не належить, значення = 4)
LogOp_Get TBL: dbo.table5 (псевдонім TBL: a5)
LogOp_Get TBL: table2 (псевдонім 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: table3 (псевдонім 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: table1 (псевдонім TBL: cdc)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col6
ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, не належить, значення = 4)
LogOp_Get TBL: table2 (псевдонім TBL: cdt)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [cdc] .col1
ScaOp_Identifier QCOL: [cdt] .col1
LogOp_Get TBL: table3 (псевдонім 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, не належить, значення = 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 substring
ScaOp_Const TI (int, ML = 4) XVAR (int, не належить, значення = 6)
ScaOp_Const TI (int, ML = 4) XVAR (int, не належить, значення = 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
план пошуку як для обчислених, так і для не обчислених схем стовпців, коли використовується керівництво плану або підказка.
Чи можемо ми зробити щось інакше, щоб здійснити пошуки
Це те, про що потрібно хвилюватися, лише якщо оптимізатор не знайде план із прийнятними характеристиками продуктивності самостійно.
Усі звичайні інструменти налаштування потенційно застосовні. Наприклад, ви можете розбити запит на більш прості частини, переглянути та вдосконалити доступну індексацію, оновити чи створити нову статистику ... тощо.
Всі ці речі можуть впливати на оцінки кардинальності, шлях коду, що проходить через оптимізатор, і впливати на рішення, що ґрунтуються на витратах, тонкими способами.
Зрештою, ви можете скористатися підказками (або керівництвом щодо плану), але це зазвичай не ідеальне рішення.
Додаткові запитання з коментарів
Я погоджуюся, що найкраще спростити запит тощо, але чи є спосіб (прапор сліду) зробити так, щоб оптимізатор продовжував оптимізацію та досягнув того ж результату?
Ні, не існує жодного сліду, який би здійснював вичерпний пошук, і його ви не хочете. Можливий простір пошуку величезний, і час компіляції, що перевищує вік Всесвіту, не був би добре сприйнятий. Крім того, оптимізатор не знає всіх можливих логічних перетворень (ніхто не робить).
Крім того, навіщо потрібне комплексне розширення, оскільки стовпець зберігається? Чому оптимізатор не може уникнути його розширення, трактувати його як звичайний стовпець і досягти тієї ж відправної точки?
Обчислювані стовпці розширюються (як і представлення), щоб забезпечити додаткові можливості оптимізації. Розширення може відповідати тому, наприклад, збереженому стовпцю або індексу пізніше в процесі, але це відбувається після фіксації початкового замовлення на з'єднання .