Отфильтрованный индекс используется только тогда, когда отфильтрованная часть находится в СОЕДИНЕНИИ, а не ГДЕ

10

Я создал отфильтрованный индекс ниже, однако, когда я запускаю 2 запроса дальше вниз, этот индекс используется только для поиска в первом примере, который имеет END_DTTM в JOIN, а не в предложении where (это единственная разница в запросах) , Кто-нибудь может объяснить, почему это происходит?

Создание индекса

CREATE NONCLUSTERED INDEX [ix_PATIENT_LIST_BESPOKE_LIST_ID_includes] ON [dbo].[PATIENT_LIST_BESPOKE] 
(
    [LIST_ID] ASC,
    [END_DTTM] ASC
)
WHERE ([END_DTTM] IS NULL)

Запросы

DECLARE @LIST_ID INT = 3655

--This one seeks on the index

SELECT  
    PATIENT_LISTS.LIST_ID
FROM    
    DBO.PATIENT_LISTS
    LEFT JOIN DBO.PATIENT_LIST_BESPOKE ON PATIENT_LISTS.LIST_ID = PATIENT_LIST_BESPOKE.LIST_ID  
                                      AND PATIENT_LIST_BESPOKE.END_DTTM IS NULL
WHERE
    PATIENT_LISTS.LIST_ID = @LIST_ID

--This one scans on the index

SELECT  
    PATIENT_LISTS.LIST_ID
FROM    
    DBO.PATIENT_LISTS
    LEFT JOIN DBO.PATIENT_LIST_BESPOKE ON PATIENT_LISTS.LIST_ID = PATIENT_LIST_BESPOKE.LIST_ID  
WHERE   
    PATIENT_LISTS.LIST_ID = @LIST_ID AND
    PATIENT_LIST_BESPOKE.END_DTTM IS NULL   
Крис
источник

Ответы:

12

Чтобы оптимизатор сопоставил предикат с индексом (отфильтрованным или иным), предикат должен отображаться рядом с операцией Get в дереве логических запросов. Чтобы облегчить это, предикаты обычно передвигаются как можно ближе к листьям логического дерева перед началом оптимизации.

Чтобы значительно упростить, реализация стратегии физического индекса делает это:

Predicate + Logical Get -> Physical Get (using Index)

Интересующий вас запрос начинается с предиката над внешним соединением:

Predicate on T2 --+-- LOJ -- Get (T1)
                       |
                       +---- Get (T2)

Эта форма не соответствует правилу стратегии индекса, поскольку предикат не смежен с Get. Итак, первая часть ответа заключается в том, что отфильтрованное сопоставление индекса не будет выполнено, если предикат не может быть передан после внешнего соединения.

Вторая часть заключается просто в том, что оптимизатор не содержит необходимого правила исследования для перемещения предиката за пределы внешнего соединения на сохраненной стороне, потому что преобразование так редко допустимо. Общей особенностью оптимизатора является то, что реализованы только наиболее часто используемые правила.

В результате сопоставление отфильтрованного индекса не выполняется в этом случае. Чтобы было ясно, переписывание будет допустимым в очень конкретном случае, который вы цитируете (второй запрос).

Для первой формы запроса (с другой семантикой) предикат связан с объединением с самого начала, и логика нажатия предиката может переместить это на короткое расстояние к получению, поскольку ему не нужно перемещаться за внешнее соединение, как объяснил выше.

Справочная информация и дополнительная информация:

Пол Уайт 9
источник
9

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

CREATE TABLE dbo.Lefty(LeftyID INT PRIMARY KEY);

CREATE TABLE dbo.Righty(LeftyID INT, SomeList INT);

INSERT dbo.Lefty(LeftyID) VALUES(1),(2),(3);

INSERT dbo.Righty(LeftyID, SomeList) VALUES(1,1),(1,NULL),(2,2);

Запрос 1 возвращает все три строки:

SELECT l.LeftyID, r.SomeList
FROM dbo.Lefty AS l
LEFT OUTER JOIN dbo.Righty AS r
ON l.LeftyID = r.LeftyID
AND r.SomeList IS NULL;

Запрос 2, однако, не учитывает LeftyID 2:

SELECT l.LeftyID, r.SomeList
FROM dbo.Lefty AS l
LEFT OUTER JOIN dbo.Righty AS r
ON l.LeftyID = r.LeftyID
WHERE r.SomeList IS NULL;

Доказательство SQLfiddle

Если вы пытаетесь выполнить анти-полусоединение, проверяемый столбец должен быть не обнуляемым . Перемещение критериев между ON и WHERE не имеет никакого логического значения, когда вы имеете дело только с INNER, но с OUTER есть существенная разница. И вам следует больше заботиться о том, чтобы ваши результаты были правильными, чем о том, можно ли вообще использовать отфильтрованный индекс.

Аарон Бертран
источник
спасибо за ответ, но я не утверждаю, что запросы совпадают, я спрашиваю, почему один запрос использует отфильтрованный индекс, а другой нет.
Крис
@chris Вы пытались форсировать этот индекс индексной подсказкой? Мне было бы любопытно сравнить реальные планы после исполнения с этим указанием и без него. Для меня ясно, что оптимизатор игнорирует этот индекс, когда считает, что он выполняет соединение против полу (так как он не будет ожидать, что в этом случае будет использоваться обнуляемый столбец), но я не уверен, что это делать с затратами или порядком операций или некоторыми базовыми знаниями, что потенциально может быть намного больше строк, идущих с левой стороны, чем те, которые находятся в фильтрованном индексе Видя планы, может помочь.
Аарон Бертран
3

Два запроса различны - по смыслу и результатам. Вот переписывание, поэтому более очевидно, что делают два запроса:

-- 1st query
SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
    LEFT JOIN  
      ( SELECT LIST_ID                    -- the filtered index
        FROM   DBO.PATIENT_LIST_BESPOKE   -- can be used
        WHERE  END_DTTM IS NULL           -- for the subquery
      ) AS b
    ON  a.LIST_ID = b.LIST_ID ;           -- and the join

и 2-й:

-- 2nd query
SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
    JOIN  
      ( SELECT LIST_ID                    -- the filtered index
        FROM   DBO.PATIENT_LIST_BESPOKE   -- can be used
        WHERE  END_DTTM IS NULL           -- for the subquery
      ) AS b
    ON  a.LIST_ID = b.LIST_ID             -- and the join

UNION ALL

SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
WHERE NOT EXISTS  
      ( SELECT *
        FROM   DBO.PATIENT_LIST_BESPOKE AS b
        WHERE  a.LIST_ID = b.LIST_ID         -- but not for this
      ) ;

Я думаю, что теперь совершенно очевидно, что для второй части запроса 2nq отфильтрованный индекс не может быть использован.


Подробно, относительно этих запросов, LIST_IDв первой таблице есть 4 типа значений:

  • (a) значения, которые имеют совпадающие строки во второй таблице, все с END_DTTM IS NULL.

  • (б) значения, которые имеют совпадающие строки во второй таблице, как с, так END_DTTM IS NULLи с END_DTTM IS NOT NULL.

  • (c) значения, которые имеют совпадающие строки во второй таблице, все с END_DTTM IS NOT NULL.

  • (d) значения, которые не имеют совпадающих строк во второй таблице.

Теперь 1-й запрос вернет все значения типа (a) и (b), возможно, много раз (столько, сколько у них совпадающей строки во второй таблице с END_DTTM IS NULL), и все строки типа (c) и (d) ровно один раз ( это несоответствующая часть внешнего соединения).

Второй запрос вернет все значения типа (a) и (b), возможно, много раз (столько, сколько у них совпадающей строки во второй таблице с END_DTTM IS NULL) и все строки типа (d) ровно один раз.
Он не будет возвращать никакого значения типа (c), потому что соединение найдет совпадающие строки во второй таблице (но они будут иметь END_DTTM IS NOT NULL), и они будут удалены последующим WHEREпредложением.

ypercubeᵀᴹ
источник