Como configurar a exibição indexada ao selecionar TOP 1 com ORDER BY de tabelas diferentes

11

Estou com dificuldades para configurar uma exibição indexada no cenário a seguir para que a consulta a seguir seja executada sem duas verificações de índice em cluster. Sempre que eu crio uma exibição de índice para esta consulta e a uso, ela parece ignorar qualquer índice que eu coloquei nela:

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

A configuração da tabela é a seguinte:

  • duas mesas
  • eles são unidos por uma junção interna pela consulta acima
  • e ordenada por uma coluna da primeira e, em seguida, uma coluna da segunda tabela pela consulta acima; apenas TOP 1 está selecionado
  • (no script abaixo, também existem algumas linhas para gerar dados de teste, caso isso ajude a reproduzir o 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
    

A exibição indexada provavelmente deve ser definida da seguinte maneira e a consulta TOP 1 resultante está abaixo. Mas de quais índices eu preciso para que essa consulta tenha um desempenho melhor do que sem a exibição indexada?

    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
fonte

Respostas:

12

Parece ignorar qualquer índice que eu coloquei nele

A menos que você esteja usando o SQL Server Enterprise Edition (ou equivalente, Trial and Developer), será necessário usar WITH (NOEXPAND)a referência de exibição para usá-lo. De fato, mesmo se você estiver usando o Enterprise, há boas razões para usar essa dica .

Sem a dica, o otimizador de consulta (no Enterprise Edition) pode fazer uma escolha baseada em custo entre usar a exibição materializada ou acessar as tabelas base. Onde a visualização é tão grande quanto as tabelas base, esse cálculo pode favorecer as tabelas base.

Outro ponto de interesse é que, sem uma NOEXPANDdica, as referências de exibição são sempre expandidas para a consulta base antes do início da otimização. À medida que a otimização progride, o otimizador pode ou não conseguir corresponder a definição expandida à visualização materializada, dependendo da atividade de otimização anterior. Esse certamente não é o caso da sua consulta simples, mas eu a menciono por completo.

Portanto, usar a NOEXPANDdica da tabela é sua opção principal, mas você também pode pensar em materializar as chaves da tabela base e as colunas necessárias para solicitar na visualização. Crie um índice clusterizado exclusivo nas colunas-chave combinadas e, em seguida, um índice não clusterizado separado nas colunas de pedidos.

Isso reduzirá o tamanho da visualização materializada e limitará o número de atualizações automáticas que devem ser feitas para manter a visualização sincronizada com as tabelas base. Sua consulta pode ser gravada para buscar as 1 chaves principais na ordem exigida da visualização (de preferência com NOEXPAND) e, em seguida, associar-se novamente às tabelas base para buscar as colunas restantes usando as chaves da visualização.

Outra variação é agrupar a visualização nas colunas de pedidos e nas chaves da tabela e, em seguida, escreva a consulta para buscar manualmente as colunas que não são da vista da tabela base usando as chaves. A melhor opção para você depende do contexto mais amplo. Uma boa maneira de decidir é testá-lo com os dados e a carga de trabalho reais.

Solução 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;

Plano de execução:

Índice de força bruta

Usando um índice não clusterizado

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

Plano de execução:

Índice não clusterizado para pedidos

Há uma pesquisa nesse plano, mas é usada apenas para buscar uma única linha.

Visualização 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);

Inquerir:

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;

Plano de execução:

Plano de consulta final

Isso mostra as chaves da tabela que estão sendo recuperadas (uma busca de linha única no índice clusterizado da exibição em ordem), seguida de duas pesquisas de linha única nas tabelas base para buscar as colunas restantes.

Paul White 9
fonte