Почему подзапрос уменьшает оценку строки до 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;

дБ скриптовая ссылка .

Джо Оббиш
источник

Ответы:

22

Эта проблема оценки (CE) возникает, когда:

  1. Объединение является внешним соединением с предикатом передачи
  2. Селективность в проходном предикат оценивается ровно 1 .

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


Детали

CE вычисляет селективность внешнего соединения как сумму :

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

Единственное различие между внешним и внутренним объединением состоит в том, что внешнее соединение также возвращает строки, которые не совпадают в предикате соединения. Анти-соединение обеспечивает именно эту разницу. Оценка кардинальности для внутреннего и антисоединения проще, чем для внешнего соединения напрямую.

Процесс оценки избирательности соединения очень прост:

  • Сначала оценивается селективность сквозного предиката. 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вводит полусоединение (выделено) в план выполнения:

Полу присоединиться план

Оценка для полусоединения в порядке. Проблема в том, что CE рассматривает связанный столбец зонда как простую проекцию с фиксированной селективностью 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

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


Для получения важной справочной информации см. Подзапросы в CASEвыражениях Крейга Фридмана.

Пол Уайт говорит, что GoFundMonica
источник
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,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)

В заключение мы получаем неправильную оценку строки после подзапроса при следующих условиях:

  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

Устаревшая СЕ, похоже, также позволяет избежать этой проблемы.

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

Подключения пункт был представлен на этот вопрос (с некоторыми из деталей, Пол Уайт , предоставленных в его ответе).

Джо Оббиш
источник