Чому підзапит знижує оцінку рядка до 1?


26

Розглянемо наступний надуманий, але простий запит:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP;

Я б очікував, що остаточна оцінка рядка для цього запиту буде дорівнює кількості рядків у X_HEAPтаблиці. Що б я не робив у підзапиті, не має значення для оцінки рядків, оскільки він не може відфільтрувати жодні рядки. Однак у SQL Server 2016 я бачу оцінку рядків зменшено до 1 через підзапит:

поганий запит

Чому це відбувається? Що я можу з цим зробити?

Дуже легко відтворити це питання правильним синтаксисом. Ось один набір визначень таблиць, які будуть виконувати це:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);

INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;

CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;

db скрипка посилання .

Відповіді:


22

Ця оцінка кардинальності (CE) видає поверхні, коли:

  1. Приєднання - це зовнішнє приєднання з присудком проходження
  2. Селективність в прохідному предикат оцінюється рівно 1 .

Примітка: Конкретний калькулятор, який використовується для визначення селективності, не важливий.


Деталі

СЕ обчислює вибірковість зовнішнього з'єднання як суму :

  • Внутрішнє з'єднання селективності з тим же предикатом
  • The боротьбі приєднатися до вибірковості з тим же предикатом

Єдина відмінність між зовнішнім і внутрішнім з'єднанням полягає в тому, що зовнішнє з'єднання також повертає рядки, які не збігаються в предикаті приєднання. Анти-приєднання забезпечує саме таку різницю. Оцінка кардинальності для внутрішнього та антиприєднання легше, ніж для зовнішнього з'єднання безпосередньо.

Процес оцінки вибірковості приєднання дуже простий:

  • По-перше, вибірковість SPT оцінюється предикатного провідника.
    • Це робиться за допомогою будь-якого калькулятора, який відповідає обставинам.
    • Присудок - це вся справа, включаючи будь-який заперечуючий IsFalseOrNullкомпонент.
  • Внутрішня вибірковість приєднання: = 1 - SPT
  • Антиселективність приєднання: = SPT

Антиз'єднання являє собою рядки, які будуть "проходити" через з'єднання. Внутрішнє з'єднання являє собою рядки, які не будуть "проходити". Зауважте, що "проходити" означає рядки, що протікають через з'єднання, не виконуючи внутрішню сторону. Щоб підкреслити: всі рядки будуть повернуті з’єднанням, відмінність між рядками, які виконують внутрішню сторону з'єднання до появи, та тими, які не виконують.

Зрозуміло, що додавання до завжди повинно давати загальну вибірковість 1, тобто всі рядки повертаються об'єднанням, як очікувалося.1 - SPTSPT

Дійсно, вищевказаний розрахунок працює точно так, як описано для всіх значень, крім 1 .SPT

Коли = 1, як внутрішня, так і антиприєднана селективність оцінюються нульовими, в результаті чого оцінюється кардинальність (для з'єднання в цілому) одного ряду. Наскільки я можу сказати, це ненавмисно, і його слід повідомляти як помилку.SPT


Пов'язане питання

Ця помилка, швидше за все, виявиться, ніж можна подумати, через окреме обмеження CE. Це виникає, коли в CASEвиразі використовується EXISTSпункт (як це прийнято). Наприклад, наступний модифікований запит із запитання не стикається з несподіваною оцінкою кардинальності:

-- This is fine
SELECT 
    CASE
        WHEN XH.ID = 1
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

Введення дрібниці EXISTSдійсно викликає проблему:

-- This is not fine
SELECT 
    CASE
        WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

Використовуючи EXISTSвводити напівприєднання (виділено) до плану виконання:

Напівприєднати план

Оцінка для півфіналу придатна. Проблема полягає в тому, що СЕ трактує пов'язаний стовпчик зонда як просту проекцію з фіксованою селективністю 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

Це автоматично відповідає одній із умов, необхідних для маніфестації цього питання CE, незалежно від змісту EXISTSпункту.


Щоб отримати важливу довідкову інформацію, див. Підзапити в CASEвиразах Крейга Фрідмана.


22

Це безумовно здається ненавмисною поведінкою. Це правда, що оцінки кардинальності не повинні бути узгодженими на кожному кроці плану, але це відносно простий план запитів, і остаточна оцінка кардинальності не відповідає тому, що робить запит. Така низька оцінка кардинальності може призвести до поганого вибору типів з'єднання та методів доступу для інших таблиць нижче за потоком у складнішому плані.

За допомогою проб і помилок ми можемо створити кілька подібних запитів, щодо яких проблема не з’являється:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Ми також можемо запропонувати більше запитів, щодо яких з’являється проблема:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Здається, що існує закономірність: якщо є вираз у межах, CASEякий не очікується виконати, а вираз результату є підзапитом проти таблиці, то оцінка рядка падає на 1 після цього виразу.

Якщо я запитую запит до таблиці з кластерним індексом, правила дещо змінюються. Ми можемо використовувати ті самі дані:

CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))

INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;

UPDATE STATISTICS X_CI WITH FULLSCAN;

Цей запит має остаточну оцінку в 1000 рядків:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI;

Але цей запит має остаточну оцінку в 1 рядок:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI;

Для подальшого запитання ми можемо скористатися недокументованим прапором трасування 2363, щоб отримати інформацію про те, як оптимізатор запитів виконував обчислення селективності. Мені було корисно поєднати цей прапор сліду з недокументованим прапором трассі 8606 . TF 2363, здається, дає розрахунки вибірковості як для спрощеного дерева, так і для дерева після нормалізації проекту. Якщо увімкнено обидва трассирові прапори, стає зрозумілим, які обчислення застосовуються до якого дерева.

Спробуємо його для оригінального запиту, розміщеного у запитанні:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Ось частина тієї частини продукції, яка, на мою думку, є актуальною, а також деякі коментарі:

Plan for computation:

  CSelCalcColumnInInterval -- this is the type of calculator used

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation

Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000

      CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

      CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join

          CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)

Тепер спробуємо скористатися аналогічним запитом, який не має проблеми. Я буду використовувати цей:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Вихід налагодження в самому кінці:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table

Спробуємо ще один запит, для якого існує неправильна оцінка рядків:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

В самому кінці оцінка кардинальності падає до 1 ряду, знову ж таки після проходження селективності = 1. Оцінка кардинальності зберігається після селективності 0,550 і 0,499.

Plan for computation:

 CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.501

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression

      CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)

          CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)

              CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

              CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

          CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)

Давайте знову перейдемо до іншого подібного запиту, який не має проблеми. Я буду використовувати цей:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

У виході налагодження ніколи не відбувається крок, що має прохідність селективності 1. Оцінка кардинальності залишається на рівні 1000 рядків.

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

End selectivity computation

Що з запитом, коли він включає таблицю з кластерним індексом? Розглянемо наступний запит із проблемою оцінки рядків:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Кінець виводу налагодження аналогічний тому, що ми вже бачили:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_CI].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

Однак запит на CI без випуску має різний результат. Використовуючи цей запит:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Результати використання різних калькуляторів. CSelCalcColumnInIntervalбільше не з’являється:

Plan for computation:

  CSelCalcFixedFilter (0.559)

Pass-through selectivity: 0.559

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

      CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

      CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

...

Plan for computation:

  CSelCalcUniqueKeyFilter

Pass-through selectivity: 0.001

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)

На закінчення ми, мабуть, отримуємо неправильну оцінку рядків після підзапиту за таких умов:

  1. Використовується CSelCalcColumnInIntervalкалькулятор вибірковості. Я точно не знаю, коли це використовується, але, схоже, це з’являється набагато частіше, коли базовий стіл - це купа.

  2. Селективність пропуску = 1. Іншими словами, CASEочікується , що один із виразів буде оцінений як хибний для всіх рядків. Не має значення, чи перший CASEвираз оцінюється як істинне для всіх рядків.

  3. Існує зовнішнє приєднання до CStCollBaseTable. Іншими словами, CASEвираження результату є підзапитом проти таблиці. Постійна величина не буде працювати.

Можливо, за таких умов оптимізатор запитів ненавмисно застосовує селективність проходження до оцінки рядків зовнішньої таблиці замість роботи, виконаної у внутрішній частині вкладеного циклу. Це зменшило б оцінку рядка до 1.

Мені вдалося знайти два обхідні шляхи. Я не зміг відтворити проблему під час використання APPLYзамість підзапиту. Вихід прапора сліду 2363 сильно відрізнявся APPLY. Ось один із способів переписати оригінальний запит у запитання:

SELECT 
  h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END
) a(ID2);

хороший запит 1

Здається, що спадщина CE також уникає проблеми.

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

хороший запит 2

Для цього питання було надіслано з'єднання (з деякими деталями, які Пол відповів у своїй відповіді).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.