Cómo configurar una vista indizada al SELECCIONAR TOP 1 con ORDER BY de diferentes tablas

11

Estoy luchando por configurar una vista indizada en el siguiente escenario para que la siguiente consulta se realice sin dos escaneos de índice agrupados. Cada vez que creo una vista de índice para esta consulta y luego la uso, parece ignorar cualquier índice que coloque en ella:

    -- +++ THE QUERY THAT I WANT TO IMPROVE PERFORMANCE-WISE +++

    SELECT TOP 1 *
    FROM    dbo.TB_test1 t1
            INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1
    ORDER BY t1.somethingelse1
           ,t2.somethingelse2;


    GO

La configuración de la tabla es la siguiente:

  • dos mesas
  • están unidos por una unión interna por la consulta anterior
  • y ordenado por una columna de la primera y luego una columna de la segunda tabla por la consulta anterior; solo se selecciona TOP 1
  • (en el siguiente script también hay algunas líneas para generar datos de prueba, en caso de que ayude a reproducir el problema)

    -- +++ TABLE SETUP +++
    
    CREATE TABLE [dbo].[TB_test1]
        (
         [PK_ID1] [INT] IDENTITY(1, 1)  NOT NULL
        ,[something1] VARCHAR(40) NOT NULL
        ,[somethingelse1] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test1] PRIMARY KEY CLUSTERED ( [PK_ID1] ASC )
        );
    
    GO
    
    create TABLE [dbo].[TB_test2]
        (
         [PK_ID2] [INT] IDENTITY(1, 1)  NOT NULL
        ,[FK_ID1] [INT] NOT NULL
        ,[something2] VARCHAR(40) NOT NULL
        ,[somethingelse2] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test2] PRIMARY KEY CLUSTERED ( [PK_ID2] ASC )
        );
    
    GO
    
    ALTER TABLE [dbo].[TB_test2]  WITH CHECK ADD  CONSTRAINT [FK_TB_Test1] FOREIGN KEY([FK_ID1])
    REFERENCES [dbo].[TB_test1] ([PK_ID1])
    GO
    
    ALTER TABLE [dbo].[TB_test2] CHECK CONSTRAINT [FK_TB_Test1]
    
    GO
    
    
    -- +++ TABLE DATA GENERATION +++
    
    -- this might not be the quickest way, but it's only to set up test data
    
    INSERT INTO dbo.TB_test1
            ( something1, somethingelse1 )
    VALUES  ( CONVERT(VARCHAR(40), NEWID())  -- something1 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse1 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test1', 0, 1) WITH NOWAIT    
    
    GO    
    
    INSERT INTO dbo.TB_test2
            ( FK_ID1, something2, somethingelse2 )
    VALUES  ( ISNULL(ABS(CHECKSUM(NewId())) % ((SELECT MAX(PK_ID1) FROM dbo.TB_test1) - 1), 0) + 1 -- FK_ID1 - int
              ,CONVERT(VARCHAR(40), NEWID())  -- something2 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse2 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test2', 0, 1) WITH NOWAIT          
    
    GO
    

La vista indizada probablemente debería definirse de la siguiente manera y la consulta TOP 1 resultante se encuentra a continuación. Pero, ¿qué índices necesito para que esta consulta funcione mejor que sin la vista indizada?

    CREATE VIEW VI_test
    WITH SCHEMABINDING
    AS
        SELECT  t1.PK_ID1
               ,t1.something1
               ,t1.somethingelse1
               ,t2.PK_ID2
               ,t2.FK_ID1
               ,t2.something2
               ,t2.somethingelse2
        FROM    dbo.TB_test1 t1
                INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1


    GO


    SELECT TOP 1 * FROM dbo.VI_test ORDER BY somethingelse1,somethingelse2


    GO
ManOnAMission
fuente

Respuestas:

12

Parece ignorar cualquier índice que puse en él

A menos que esté utilizando SQL Server Enterprise Edition (o equivalente, Trial y Developer), deberá usarlo WITH (NOEXPAND)en la referencia de vista para poder usarlo. De hecho, incluso si está utilizando Enterprise, hay buenas razones para usar esa sugerencia .

Sin la sugerencia, el optimizador de consultas (en Enterprise Edition) puede hacer una elección basada en el costo entre usar la vista materializada o acceder a las tablas base. Cuando la vista es tan grande como las tablas base, este cálculo puede favorecer las tablas base.

Otro punto de interés es que sin una NOEXPANDsugerencia, las referencias de vista siempre se expanden a la consulta base antes de que comience la optimización. A medida que avanza la optimización, el optimizador puede o no ser capaz de hacer coincidir la definición expandida con la vista materializada, dependiendo de la actividad de optimización previa. Es casi seguro que este no sea el caso con su consulta simple, pero lo menciono por completo.

Por lo tanto, utilizar la NOEXPANDsugerencia de tabla es su opción principal, pero también puede pensar en materializar las claves de la tabla base y las columnas necesarias para ordenar en la vista. Cree un índice agrupado único en las columnas de teclas combinadas, luego un índice no agrupado separado en las columnas de pedido.

Esto reducirá el tamaño de la vista materializada y limitará el número de actualizaciones automáticas que deben realizarse para mantener la vista sincronizada con las tablas base. Su consulta se puede escribir para obtener las 1 teclas principales en el orden requerido de la vista (idealmente con NOEXPAND), luego unirse de nuevo a las tablas base para recuperar las columnas restantes utilizando las teclas de la vista.

Otra variación es agrupar la vista en las columnas de ordenación y las claves de la tabla, luego escribir la consulta para recuperar manualmente las columnas sin vista de la tabla base utilizando las claves. La mejor opción para usted depende del contexto más amplio. Una buena manera de decidir es probarlo con los datos reales y la carga de trabajo.

Solución básica

CREATE VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t1.something1,
        t1.somethingelse1,
        t2.PK_ID2,
        t2.FK_ID1,
        t2.something2,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Brute force unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);
GO
SELECT TOP (1) * 
FROM dbo.VI_test WITH (NOEXPAND)
ORDER BY somethingelse1,somethingelse2;

Plan de ejecución:

Índice de fuerza bruta

Usar un índice no agrupado

-- Minimal unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (PK_ID1, PK_ID2)
WITH (DROP_EXISTING = ON);
GO
-- Nonclustered index for ordering
CREATE NONCLUSTERED INDEX ix 
ON dbo.VI_test (somethingelse1, somethingelse2);

Plan de ejecución:

Índice no agrupado para ordenar

Hay una búsqueda en este plan, pero solo se usa para obtener una sola fila.

Vista indexada mínima

ALTER VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t2.PK_ID2,
        t1.somethingelse1,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);

Consulta:

SELECT TOP (1)
    V.PK_ID1,
    TT1.something1,
    V.somethingelse1,
    V.PK_ID2,
    TT2.FK_ID1,
    TT2.something2,
    V.somethingelse2
FROM dbo.VI_test AS V WITH (NOEXPAND)
JOIN dbo.TB_test1 AS TT1 ON TT1.PK_ID1 = V.PK_ID1
JOIN dbo.TB_test2 AS TT2 ON TT2.PK_ID2 = V.PK_ID2
ORDER BY somethingelse1,somethingelse2;

Plan de ejecución:

Plan de consulta final

Esto muestra las claves de la tabla que se están recuperando (una búsqueda de una sola fila del índice agrupado de la vista en orden) seguido de dos búsquedas de una sola fila en las tablas base para obtener las columnas restantes.

Paul White 9
fuente