Це безумовно здається ненавмисною поведінкою. Це правда, що оцінки кардинальності не повинні бути узгодженими на кожному кроці плану, але це відносно простий план запитів, і остаточна оцінка кардинальності не відповідає тому, що робить запит. Така низька оцінка кардинальності може призвести до поганого вибору типів з'єднання та методів доступу для інших таблиць нижче за потоком у складнішому плані.
За допомогою проб і помилок ми можемо створити кілька подібних запитів, щодо яких проблема не з’являється:
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)
На закінчення ми, мабуть, отримуємо неправильну оцінку рядків після підзапиту за таких умов:
Використовується CSelCalcColumnInInterval
калькулятор вибірковості. Я точно не знаю, коли це використовується, але, схоже, це з’являється набагато частіше, коли базовий стіл - це купа.
Селективність пропуску = 1. Іншими словами, CASE
очікується , що один із виразів буде оцінений як хибний для всіх рядків. Не має значення, чи перший CASE
вираз оцінюється як істинне для всіх рядків.
Існує зовнішнє приєднання до 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);
Здається, що спадщина 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'));
Для цього питання було надіслано з'єднання (з деякими деталями, які Пол відповів у своїй відповіді).