Índice filtrado usado apenas quando a parte filtrada está em JOIN e não no WHERE

10

Eu criei o índice filtrado abaixo, no entanto, quando executo as 2 consultas mais abaixo, esse índice só é usado para uma pesquisa no primeiro exemplo que possui o END_DTTM na JOIN e não na cláusula where (essa é a única diferença nas consultas) . Alguém pode explicar por que isso acontece?

Criação de Índice

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)

Consultas

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   
chris
fonte

Respostas:

12

Para o otimizador corresponder um predicado a um índice (filtrado ou não), o predicado deve aparecer adjacente à operação Get na árvore de consultas lógicas. Para facilitar isso, os predicados geralmente são empurrados o mais próximo possível das folhas da árvore lógica antes do início da otimização.

Para simplificar bastante, a implementação da estratégia de índice físico faz o seguinte:

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

A consulta em que você está interessado começa com o predicado acima de uma junção externa:

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

Essa forma não corresponde à regra da estratégia de índice porque o predicado não é adjacente ao Get. Portanto, a primeira parte da resposta é que a correspondência de índice filtrada falhará, a menos que o predicado possa ser passado pela junção externa.

A segunda parte é simplesmente que o otimizador não contém a regra de exploração necessária para mover um predicado além de uma junção externa no lado preservado, porque a transformação raramente é válida. É um recurso geral do otimizador que apenas as regras úteis com mais freqüência são implementadas.

Como resultado, a correspondência do índice filtrado falha nesse caso. Para ser claro, a reescrita seria válida no caso muito específico que você citar (segunda consulta).

Para o primeiro formulário de consulta (com semântica diferente), o predicado está associado à junção desde o início, e a lógica push-down do predicado pode movê-la a uma curta distância do Get, pois não precisa passar por uma junção externa, pois explicado acima.

Antecedentes e informações adicionais:

Paul White 9
fonte
9

Essas não são semanticamente as mesmas consultas, pois uma pode filtrar antes da junção e a outra pode filtrar depois. Deixe-me ilustrar com um exemplo mais simples:

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);

A consulta 1 retorna todas as três linhas:

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;

A consulta 2, no entanto, deixa de fora o 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;

Prova de SQLfiddle

Se você estiver tentando executar uma junção anti-semi, a coluna testada precisará não ser anulável . Mover critérios entre ON e WHERE não faz diferença lógica quando você está lidando apenas com junções INNER, mas com OUTER há uma diferença significativa. E você deve se preocupar mais que seus resultados estejam corretos do que se um índice filtrado pode ou não ser usado.

Aaron Bertrand
fonte
obrigado pela resposta, mas não estou afirmando que as consultas são iguais, estou perguntando por que uma consulta usa o índice filtrado e a outra não.
Chris
@chris Você tentou forçar esse índice com uma dica de índice? Gostaria de comparar planos reais de pós-execução com e sem essa dica. Para mim, é claro que o otimizador está ignorando esse índice quando acredita que está fazendo uma junção anti-semifinal (já que não espera que uma coluna anulável seja usada nesse caso), mas não tenho certeza se isso é necessário. faça com o custo ou a ordem das operações ou com algum conhecimento subjacente de que há potencialmente muito mais linhas vindas do lado esquerdo do que aquelas que estão no índice filtrado. Ver os planos pode ajudar.
Aaron Bertrand
3

As duas consultas são diferentes - em significado e resultados. Aqui está uma reescrita, por isso é mais óbvio o que as duas consultas estão fazendo:

-- 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

e 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
      ) ;

Eu acho que agora é bastante óbvio que, para a segunda parte da consulta 2nq, o índice filtrado não pode ser usado.


Em detalhes, em relação a essas consultas, existem 4 tipos de LIST_IDvalores na primeira tabela:

  • (a) valores que possuem linhas correspondentes na segunda tabela, todos com END_DTTM IS NULL.

  • (b) valores que possuem linhas correspondentes na segunda tabela, com END_DTTM IS NULLe com END_DTTM IS NOT NULL.

  • (c) valores que possuem linhas correspondentes na segunda tabela, todos com END_DTTM IS NOT NULL.

  • (d) valores que não possuem linhas correspondentes na segunda tabela.

Agora, a primeira consulta retornará todos os valores do tipo (a) e (b) possivelmente várias vezes (quantas tiverem uma linha correspondente na segunda tabela com END_DTTM IS NULL) e todas as linhas do tipo (c) e (d) exatamente uma vez ( essa é a parte não correspondente da junção externa).

A 2ª consulta retornará todos os valores do tipo (a) e (b) possivelmente várias vezes (quantas tiverem uma linha correspondente na segunda tabela com END_DTTM IS NULL) e todas as linhas do tipo (d) exatamente uma vez.
Ele não retornará nenhum valor do tipo (c) porque a junção encontrará linhas correspondentes na segunda tabela (mas elas terão END_DTTM IS NOT NULL) e serão removidas pela WHEREcláusula subsequente .

ypercubeᵀᴹ
fonte