Comment configurer la vue indexée lors de la sélection du TOP 1 avec ORDER BY à partir de différentes tables

11

J'ai du mal à configurer une vue indexée dans le scénario suivant afin que la requête suivante s'exécute sans deux analyses d'index en cluster. Chaque fois que je crée une vue d'index pour cette requête et que je l'utilise, il semble ignorer tout index que je mets 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 jointure interne par la requête ci-dessus
  • et ordonné par une colonne de la première puis une colonne de la deuxième table par la requête ci-dessus; seul TOP 1 est sélectionné
  • (dans le script ci-dessous, il y a aussi quelques lignes pour générer des données de test, juste au cas où cela aiderait à 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 requête TOP 1 résultante est ci-dessous. Mais de quels index ai-je 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
ManOnAMission
la source

Réponses:

12

Il semble ignorer tout index que je mets dessus

À moins que vous n'utilisiez SQL Server Enterprise Edition (ou de manière équivalente, Trial and Developer), vous devrez utiliser WITH (NOEXPAND)la référence de vue pour l'utiliser. En fait, même si vous utilisez Enterprise, il y a de bonnes raisons d'utiliser cet indice .

Sans l'indication, l'optimiseur de requêtes (dans Enterprise Edition) peut faire un choix basé sur les coûts entre l'utilisation de la vue matérialisée ou l'accès aux tables de base. Lorsque la vue est aussi grande que les tables de base, ce calcul peut favoriser les tables de base.

Un autre point intéressant est que sans NOEXPANDindice, les références de vue sont toujours étendues à la requête de base avant le début de l'optimisation. À mesure que l'optimisation progresse, l'optimiseur peut ou non être en mesure de faire correspondre la définition étendue à 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 simple requête, mais je le mentionne pour être complet.

Ainsi, l'utilisation de l' NOEXPANDindicateur de tableau est votre principale option, mais vous pouvez également penser à matérialiser simplement les clés de la table de base et les colonnes nécessaires au classement dans la vue. Créez un index cluster unique sur les colonnes clés combinées, puis un index non cluster 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 qui doivent être effectuées pour maintenir la vue synchronisée avec les tables de base. Votre requête peut ensuite être écrite pour extraire les 1 premières clés dans l'ordre requis de la vue (idéalement avec NOEXPAND), puis rejoindre les tables de base pour récupérer toutes les colonnes restantes à l'aide des clés de la vue.

Une autre variante consiste à regrouper la vue sur les colonnes de classement et les clés de table, puis à écrire la requête pour extraire manuellement les colonnes non affichées de la table de base à l'aide des clés. 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 et la charge de travail réelles.

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:

Indice de force brute

Utilisation d'un index non cluster

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

Index non clusterisé pour la commande

Il existe une recherche dans ce plan, mais elle n'est utilisée que pour récupérer 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);

Requete:

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:

Plan de requête final

Cela montre les clés de table en cours de récupération (une extraction de ligne unique à partir de l'index en cluster de vues dans l'ordre) suivie de deux recherches sur une seule ligne sur les tables de base pour extraire les colonnes restantes.

Paul White 9
la source