web-dev-qa-db-fra.com

Comment configurer une vue indexée lors de la sélection du top 1 avec la commande de différentes tables

Je me lance de configurer une vue indexée dans le scénario suivant afin que la requête suivante effectue sans deux analyses d'index en cluster. Chaque fois que je crée une vue d'index pour cette requête puis l'utiliser, il semble ignorer tout index que je lui ai mis dessus:

    -- +++ 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 configuration de la table est la suivante:

  • deux tables
  • ils sont rejoints par une rejoindre interne par la requête ci-dessus
  • et commandé par une colonne de la première, puis une colonne de la deuxième table par la requête ci-dessus; Seul le top 1 est sélectionné
  • (Dans le script ci-dessous, il y a aussi certaines lignes pour générer des données de test, au cas où cela aide à reproduire le problème)

    -- +++ 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 vue indexée doit probablement être définie comme suit et la première requête supérieure résultante est ci-dessous. Mais quels indices ai-ils besoin pour que cette requête fonctionne mieux que sans la vue indexée?

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

Il semble ignorer tout index que je l'ai mis dessus

Sauf si vous utilisez SQL Server Enterprise Edition (ou équivalent, essai et développeur), vous devrez utiliser WITH (NOEXPAND) sur la référence de la vue afin de l'utiliser. En fait, même si vous utilisez l'entreprise, il y a bonnes raisons d'utiliser cet indice .

Sans l'indice, l'optimiseur de requêtes (dans l'édition d'entreprise) peut effectuer un choix de coûts entre l'utilisation de la vue matérialisée ou l'accès aux tables de base. Lorsque la vue est aussi importante que les tables de base, ce calcul peut favoriser les tables de base.

Un autre point d'intérêt est que sans indice NOEXPAND, voir les références sont toujours étendues à la requête de base avant que l'optimisation commence. À mesure que l'optimisation progresse, l'optimiseur peut ou non être en mesure de correspondre à la définition expansée à la vue matérialisée, en fonction de l'activité d'optimisation précédente. Ce n'est presque certainement pas le cas avec votre requête simple, mais je le mentionne pour la complétude.

Ainsi, à l'aide de l'indice de table NOEXPAND est votre option principale, mais vous pouvez également penser à la fabrication des clés de la table de base et des colonnes nécessaires à la commande dans la vue. Créez un index en cluster unique sur les colonnes de touches combinées, puis un index non clusterisé séparé sur les colonnes de commande.

Cela réduira la taille de la vue matérialisée et limitera le nombre de mises à jour automatiques à faire pour maintenir la vue synchronisée avec les tables de base. Votre requête peut alors être écrite pour aller chercher les 1 touches TOP 1 dans la commande requise à partir de la vue (idéalement avec NOEXPAND), puis rejoignez les tables de base pour récupérer les colonnes restantes à l'aide des touches de la vue.

Une autre variante consiste à regrouper la vue sur les colonnes de commande et les touches de table, puis écrivez la requête pour extraire manuellement les colonnes de non-vue de la table de base à l'aide des touches. La meilleure option pour vous dépend du contexte plus large. Un bon moyen de décider est de le tester avec les données réelles et la charge de travail.

Solution basique

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 d'exécution:

Brute force index

Utilisation d'un index non clustered

-- 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 d'exécution:

Nonclustered index for ordering

Il y a une recherche dans ce plan, mais il n'est utilisé que pour chercher une seule ligne.

Vue indexée minimale

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

Mettre en doute:

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 d'exécution:

Final query plan

Cela indique que les touches de table ont été récupérées (une seule ligne d'extraction de la ligne à partir de l'indice en cluster à la vue dans l'ordre suivi de deux recherches à une seule rangée sur les tables de base pour récupérer les colonnes restantes.

12
Paul White 9