El índice filtrado solo se usa cuando la parte filtrada está en JOIN, no en WHERE

10

Creé el índice filtrado a continuación, sin embargo, cuando ejecuto las 2 consultas más abajo, este índice solo se usa para una búsqueda en el primer ejemplo que tiene END_DTTM en JOIN en lugar de la cláusula where (esa es la única diferencia en las consultas) . ¿Alguien puede explicar por qué sucede esto?

Creación 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
fuente

Respuestas:

12

Para que el optimizador haga coincidir un predicado con un índice (filtrado o no), el predicado debe aparecer adyacente a la operación Get en el árbol de consulta lógica. Para facilitar esto, los predicados generalmente se acercan lo más posible a las hojas del árbol lógico antes de que comience la optimización.

Para simplificar enormemente, la implementación de la estrategia de índice físico hace esto:

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

La consulta que le interesa comienza con el predicado sobre una combinación externa:

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

Esta forma no coincide con la regla de estrategia de índice porque el predicado no es adyacente al Get. Entonces, la primera parte de la respuesta es que la coincidencia de índice filtrada fallará a menos que el predicado pueda ser empujado más allá de la unión externa.

La segunda parte es simplemente que el optimizador no contiene la regla de exploración necesaria para mover un predicado más allá de una unión externa en el lado preservado, porque la transformación rara vez es válida. Una característica general del optimizador es que solo se implementan las reglas útiles con mayor frecuencia.

Como resultado, la coincidencia del índice filtrado falla en este caso. Para ser claros, la reescritura sería válida en el caso muy específico que usted cita (segunda consulta).

Para el primer formulario de consulta (con semántica diferente), el predicado está asociado con la unión desde el principio, y la lógica de empuje hacia abajo del predicado puede mover esto la corta distancia al Get porque no tiene que moverse más allá de una unión externa como explicado anteriormente

Antecedentes y más información:

Paul White 9
fuente
9

Estas no son semánticamente las mismas consultas, ya que una puede filtrar antes de la unión y la otra puede filtrar después. Permítanme ilustrar con un ejemplo más simple:

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

La consulta 1 devuelve las tres filas:

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;

La consulta 2, sin embargo, omite 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;

Prueba de SQLfiddle

Si está intentando realizar una unión anti-semi, la columna probada no debe ser anulable . Mover los criterios entre ON y WHERE no hace ninguna diferencia lógica cuando se trata de uniones INNER únicamente, pero con OUTER hay una diferencia significativa. Y debe preocuparse más de que sus resultados sean correctos que si un índice filtrado podría o no usarse.

Aaron Bertrand
fuente
gracias por la respuesta pero no estoy afirmando que las consultas sean las mismas, pregunto por qué una consulta usa el índice filtrado y la otra no.
Chris
@chris ¿Intentaste forzar ese índice con una pista de índice? Sería curioso comparar planes reales posteriores a la ejecución con y sin esa pista. Para mí, está claro que el optimizador está ignorando ese índice cuando cree que está haciendo una unión anti-semi (ya que no esperaría que se use una columna anulable en ese caso), pero no estoy seguro de si eso es así hacer con el costo o el orden de las operaciones o algún conocimiento subyacente de que potencialmente hay muchas más filas provenientes del lado izquierdo que las que están en el índice filtrado. Ver los planes podría ayudar.
Aaron Bertrand
3

Las dos consultas son diferentes: en significado y resultados. Aquí hay una reescritura, por lo que es más obvio lo que están haciendo las dos consultas:

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

y 2do:

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

Creo que ahora es bastante obvio que para la segunda parte de la consulta 2nq, el índice filtrado no se puede usar.


En detalle, con respecto a estas consultas, hay 4 tipos de LIST_IDvalores en la primera tabla:

  • (a) valores que tienen filas coincidentes en la segunda tabla, todos con END_DTTM IS NULL.

  • (b) valores que tienen filas coincidentes en la segunda tabla, tanto con END_DTTM IS NULLcomo con END_DTTM IS NOT NULL.

  • (c) valores que tienen filas coincidentes en la segunda tabla, todos con END_DTTM IS NOT NULL.

  • (d) valores que no tienen filas coincidentes en la segunda tabla.

Ahora, la primera consulta devolverá todos los valores de tipo (a) y (b) posiblemente muchas veces (tantas como tengan una fila coincidente en la segunda tabla con END_DTTM IS NULL) y todas las filas de tipo (c) y (d) exactamente una vez ( esa es la parte no coincidente de la unión externa).

La segunda consulta devolverá todos los valores de tipo (a) y (b) posiblemente muchas veces (tantas como tengan una fila coincidente en la segunda tabla END_DTTM IS NULL) y todas las filas de tipo (d) exactamente una vez.
Será no devolver ningún valor del tipo (c) debido a la unión encontrarán filas coincidentes en la segunda tabla (pero éstos tendrán END_DTTM IS NOT NULL) y que será eliminado por el posterior WHEREcláusula.

ypercubeᵀᴹ
fuente