El índice en la columna computada persistente necesita una búsqueda clave para obtener columnas en la expresión calculada

24

Tengo una columna calculada persistente en una tabla que simplemente está formada por columnas concatenadas, por ejemplo

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

En esto Compno es único, y D es la fecha de inicio de validez de cada combinación de A, B, C, por lo tanto, uso la siguiente consulta para obtener la fecha de finalización de cada uno A, B, C(básicamente la próxima fecha de inicio para el mismo valor de Comp):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

Luego agregué un índice a la columna calculada para ayudar en esta consulta (y también en otras):

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

Sin embargo, el plan de consulta me sorprendió. Pensé que, dado que tengo una cláusula where que indica eso D IS NOT NULLy estoy clasificando por Comp, y no haciendo referencia a ninguna columna fuera del índice, el índice en la columna calculada podría usarse para escanear t1 y t2, pero vi un índice agrupado escanear.

ingrese la descripción de la imagen aquí

Así que forcé el uso de este índice para ver si producía un plan mejor:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

Que dio este plan

ingrese la descripción de la imagen aquí

Esto muestra que se está utilizando una búsqueda de claves, cuyos detalles son:

ingrese la descripción de la imagen aquí

Ahora, de acuerdo con la documentación del servidor SQL:

Puede crear un índice en una columna calculada que se define con una expresión determinista, pero imprecisa, si la columna está marcada como PERSISTA en la instrucción CREATE TABLE o ALTER TABLE. Esto significa que el Motor de base de datos almacena los valores calculados en la tabla y los actualiza cuando se actualizan cualesquiera otras columnas de las que depende la columna calculada. El Motor de base de datos utiliza estos valores persistentes cuando crea un índice en la columna y cuando se hace referencia al índice en una consulta. Esta opción le permite crear un índice en una columna calculada cuando Database Engine no puede probar con precisión si una función que devuelve expresiones de columna calculadas, particularmente una función CLR que se crea en .NET Framework, es determinista y precisa.

Entonces, si, como dicen los documentos, "el Motor de base de datos almacena los valores calculados en la tabla" , y el valor también se almacena en mi índice, ¿por qué se requiere una Búsqueda de clave para obtener A, B y C cuando no se hace referencia en ellos? la consulta en absoluto? Supongo que se están utilizando para calcular Comp, pero ¿por qué? Además, ¿por qué la consulta puede usar el índice activado t2, pero no activado t1?

Consultas y DDL en SQL Fiddle

Nota: he etiquetado SQL Server 2008 porque esta es la versión en la que se encuentra mi problema principal, pero también tengo el mismo comportamiento en 2012.

GarethD
fuente

Respuestas:

20

¿Por qué se requiere una búsqueda clave para obtener A, B y C cuando no se hace referencia en la consulta? Supongo que se están utilizando para calcular Comp, pero ¿por qué?

Las columnas A, B, and C se mencionan en el plan de consulta: son utilizadas por la búsqueda en T2.

Además, ¿por qué la consulta puede usar el índice en t2, pero no en t1?

El optimizador decidió que escanear el índice agrupado era más barato que escanear el índice no agrupado filtrado y luego realizar una búsqueda para recuperar los valores de las columnas A, B y C.

Explicación

La verdadera pregunta es por qué el optimizador sintió la necesidad de recuperar A, B y C para la búsqueda del índice. Es de esperar que lea la Compcolumna usando un escaneo de índice no agrupado, y luego realice una búsqueda en el mismo índice (alias T2) para ubicar el registro Top 1.

El optimizador de consultas amplía las referencias de columnas calculadas antes de que comience la optimización, para darle la oportunidad de evaluar los costos de varios planes de consultas. Para algunas consultas, expandir la definición de una columna calculada permite al optimizador encontrar planes más eficientes.

Cuando el optimizador encuentra una subconsulta correlacionada, intenta 'desenrollarla' a un formulario que le resulta más fácil razonar. Si no puede encontrar una simplificación más efectiva, recurre a reescribir la subconsulta correlacionada como una solicitud (una unión correlacionada):

Aplicar reescribir

Sucede que esta aplicación desenrolla pone el árbol de consulta lógica en un formulario que no funciona bien con la normalización del proyecto (una etapa posterior que busca hacer coincidir las expresiones generales con las columnas calculadas, entre otras cosas).

En su caso, la forma en que se escribe la consulta interactúa con los detalles internos del optimizador de modo que la definición de expresión expandida no coincida con la columna calculada, y termina con una búsqueda que hace referencia a columnas en A, B, and Clugar de a la columna calculada Comp. Esta es la causa raíz.

Solución alternativa

Una idea para solucionar este efecto secundario es escribir la consulta como una aplicación manual:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Desafortunadamente, esta consulta no usará el índice filtrado como esperamos tampoco. La prueba de desigualdad en la columna Ddentro de la aplicación rechaza NULLs, por lo que el predicado aparentemente redundante WHERE T1.D IS NOT NULLse optimiza.

Sin ese predicado explícito, la lógica de coincidencia del índice filtrado decide que no puede usar el índice filtrado. Hay varias maneras de evitar este segundo efecto secundario, pero la más fácil es probablemente cambiar la aplicación cruzada a una aplicación externa (reflejando la lógica de la reescritura que realizó el optimizador anteriormente en la subconsulta correlacionada):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Ahora, el optimizador no necesita usar la reescritura de aplicar (por lo que la coincidencia de columnas calculada funciona como se esperaba) y el predicado tampoco está optimizado, por lo que el índice filtrado se puede usar para ambas operaciones de acceso a datos, y la búsqueda usa la Compcolumna a ambos lados:

Plan de aplicación externa

En general, esto se preferiría a agregar A, B y C como INCLUDEdcolumnas en el índice filtrado, porque aborda la causa raíz del problema y no requiere ampliar el índice innecesariamente.

Columnas calculadas persistentes

Como nota al margen, no es necesario marcar la columna calculada como PERSISTED, si no le importa repetir su definición en una CHECKrestricción:

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

La columna calculada solo debe estar PERSISTEDen este caso si desea utilizar una NOT NULLrestricción o hacer referencia a la Compcolumna directamente (en lugar de repetir su definición) en una CHECKrestricción.

Paul White dice GoFundMonica
fuente
2
+1 Por cierto, me encontré con otro caso de búsqueda superflua mientras miraba esto que puede (o no) encontrar de interés. SQL Fiddle .
Martin Smith
@ MartininSmith Sí, eso es interesante. Otra regla genérica rewrite ( FOJNtoLSJNandLASJN) que da como resultado que las cosas no funcionen como esperamos, y que deja basura (BaseRow / Checksums) que es útil en algunos tipos de planes (por ejemplo, cursores) pero que no es necesario aquí.
Paul White dice GoFundMonica
Ah Chkes suma de comprobación! Gracias no estaba seguro de eso. Originalmente estaba pensando que podría tener algo que ver con las restricciones de verificación.
Martin Smith
6

Aunque esto podría ser una coincidencia debido a la naturaleza artificial de sus datos de prueba, como mencionó SQL 2012 probé una reescritura:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Esto produjo un buen plan de bajo costo usando su índice y con lecturas significativamente más bajas que las otras opciones (y los mismos resultados para sus datos de prueba).

Plan Explorer cuesta cuatro opciones: Original;  original con pista;  aplicación externa y plomo

Sospecho que sus datos reales son más complicados, por lo que puede haber algunos escenarios en los que esta consulta se comporta semánticamente diferente a la suya, pero a veces muestra que las nuevas características pueden marcar una verdadera diferencia.

Experimenté con algunos datos más variados y encontré algunos escenarios que coincidían y otros no:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'
wBob
fuente
1
Bueno, usa el índice pero solo hasta cierto punto. Si compno es una columna calculada, no verá el orden.
Martin Smith
Gracias. Mi escenario real no es mucho más complicado y la LEADfunción funcionó exactamente como me gustaría en mi instancia local de 2012 express. Desafortunadamente, este inconveniente menor para mí no se consideró una razón suficiente para actualizar los servidores de producción todavía ...
GarethD
-1

Cuando intenté realizar las mismas acciones, obtuve los otros resultados. En primer lugar, mi plan de ejecución para la tabla sin índices tiene el siguiente aspecto:ingrese la descripción de la imagen aquí

Como podemos ver en el Análisis de índice agrupado (t2), el predicado se usa para determinar las filas necesarias que se devolverán (debido a la condición):

ingrese la descripción de la imagen aquí

Cuando se agregó el índice, no importa si fue definido por el operador WITH o no, el plan de ejecución se convirtió en el siguiente:

ingrese la descripción de la imagen aquí

Como podemos ver, la exploración de índice agrupado se reemplaza por la exploración de índice. Como vimos anteriormente, el SQL Server usa las columnas de origen de la columna calculada para realizar la coincidencia de la consulta anidada. Durante el análisis de índice agrupado, todos estos valores se pueden adquirir al mismo tiempo (no se necesitan operaciones adicionales). Cuando se agregó el índice, el filtrado de las filas necesarias de la tabla (en la selección principal) se realiza de acuerdo con el índice, pero los valores de las columnas de origen para la columna calculada compaún deben obtenerse (última operación Nested Loop) .

ingrese la descripción de la imagen aquí

Debido a esto, se utiliza la operación de búsqueda de clave para obtener los datos de las columnas de origen de la calculada.

PD Parece un error en SQL Server.

Sandr
fuente