Это определенно похоже на непреднамеренное поведение. Это правда, что оценки количества элементов не обязательно должны быть согласованными на каждом этапе плана, но это относительно простой план запроса, и окончательная оценка количества элементов не соответствует тому, что делает запрос. Такая низкая оценка мощности может привести к неправильному выбору типов соединений и методов доступа для других таблиц в более сложном плане.
Методом проб и ошибок мы можем создать несколько похожих запросов, для которых проблема не возникает:
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,501 и 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);
Устаревшая СЕ, похоже, также позволяет избежать этой проблемы.
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'));
Подключения пункт был представлен на этот вопрос (с некоторыми из деталей, Пол Уайт , предоставленных в его ответе).