web-dev-qa-db-fra.com

L'index sur la colonne calculée a besoin de la recherche clé de la colonne pour obtenir des colonnes dans l'expression calculée

J'ai une colonne calculée persistée sur une table qui est simplement composée de colonnes concaténées, par ex.

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

Dans ce type Comp n'est pas unique, et d est la date de chaque combinaison de chaque combinaison de A, B, C, donc j'utilise la requête suivante pour obtenir la date de fin de chaque A, B, C (Fondamentalement, la prochaine date de début de la même valeur 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;

J'ai ensuite ajouté un index à la colonne calculée pour aider à cette requête (ainsi que d'autres):

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

Le plan de requête m'a toutefois surpris. J'aurais pensé que depuis que j'ai une clause où indiquant que D IS NOT NULL et je trie par Comp, et ne référencez aucune colonne en dehors de l'indice que l'index sur la colonne calculée pourrait être utilisé pour analyser T1 et T2, mais j'ai vu une analyse d'index en cluster.

enter image description here

J'ai donc forcé l'utilisation de cet indice à voir si elle a donné un meilleur plan:

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;

Qui a donné ce plan

enter image description here

Cela montre qu'une recherche clé est utilisée, dont les détails sont:

enter image description here

Maintenant, selon la documentation SQL-Server:

Vous pouvez créer un index sur une colonne calculée définie avec une expression déterministe, mais imprécise, si la colonne est marquée persistée dans la table Créer une table ou Alter Table. Cela signifie que le moteur de base de données stocke les valeurs calculées dans la table et les met à jour lorsque d'autres colonnes sur lesquelles dépend de la colonne calculée sont mises à jour. Le moteur de base de données utilise ces valeurs persistées lorsqu'elle crée un index sur la colonne et lorsque l'index est référencé dans une requête. Cette option vous permet de créer un index sur une colonne calculée lorsque le moteur de base de données ne peut pas prouver avec précision si une fonction renvoie les expressions de colonne calculées, en particulier une fonction CLR créée dans le cadre .NET, est à la fois déterministe et précis.

Donc, si, comme dit les documents ", le moteur de base de données stocke les valeurs calculées dans la table" , et la valeur est également stockée dans mon index, pourquoi Une recherche clé est-elle nécessaire pour obtenir A, B et C quand ils ne sont pas référencés dans la requête du tout? Je suppose qu'ils sont utilisés pour calculer Comp, mais pourquoi? En outre, pourquoi la requête peut-elle utiliser l'index sur t2, mais pas sur t1?

Queries et DDL sur SQL FIDDLE

n.b. J'ai marqué SQL Server 2008 car il s'agit de la version que mon principal problème est activée, mais je reçois aussi le même comportement en 2012.

24
GarethD

Pourquoi une recherche clé est-elle nécessaire pour obtenir A, B et C quand ils ne sont pas référencés dans la requête du tout? Je suppose qu'ils sont utilisés pour calculer Comp, mais pourquoi?

Colonnes A, B, and Csont référencé dans le plan de requête - ils sont utilisés par la recherche sur T2.

Aussi, pourquoi la requête peut-elle utiliser l'index sur T2, mais pas sur T1?

L'optimiseur a décidé que la numérisation de l'index en cluster était moins chère que la numérisation de l'index filtré non clustered, puis effectuant une recherche pour extraire les valeurs des colonnes A, B et C.

Explication

La vraie question est de savoir pourquoi l'optimiseur a ressenti la nécessité de récupérer A, B et C pour la recherche de l'indice. Nous nous attendions à ce qu'il lu la colonne Comp à l'aide d'une analyse index non clusteriée, puis effectuez une recherche sur le même index (alias T2) pour localiser le dernier enregistrement.

L'optimiseur de requête élargit les références de colonne calculées avant que l'optimisation commence, afin de lui donner une chance d'évaluer les coûts de divers projets de requête. Pour certaines requêtes, élargir la définition d'une colonne calculée permet à l'optimiseur de trouver des plans plus efficaces.

Lorsque l'optimiseur rencontre une sous-requête corrélée, il tente de "le dérouler" à une forme qu'il trouve plus facile à raisonner. S'il ne peut pas trouver une simplification plus efficace, il a recours à la réécriture de la sous-requête corrélée en tant que demande (une jointure corrélée):

Apply rewrite

Il arrive simplement que cela appliquent le déroulement met l'arborescence de la requête logique dans une forme qui ne fonctionne pas bien avec normalisation du projet (une étape ultérieure qui semble correspondre aux expressions générales aux colonnes calculées, entre autres).

Dans votre cas, la manière dont la requête est écrite interagit avec des détails internes de l'optimiseur de sorte que la définition d'expression expansée ne soit pas adaptée à la colonne calculée, et vous vous retrouvez avec une recherche que les colonnes de références A, B, and C Au lieu de la colonne calculée, Comp. C'est la cause fondamentale.

Workaround

Une idée à la solution de contournement Cet effet secondaire est d'écrire la requête comme une demande manuelle:

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;

Malheureusement, cette requête n'utilisera pas l'indice filtré comme nous l'espérons non plus. Le test d'inégalité sur la colonne D à l'intérieur de l'application rejette NULLs, donc le prédicat apparemment redondant WHERE T1.D IS NOT NULL est optimisé.

Sans ce prédicat explicite, la logique de correspondance d'index filtrée décide qu'il ne peut pas utiliser l'index filtré. Il existe un certain nombre de façons de contourner ce second effet secondaire, mais le plus facile est probablement de changer la croix s'appliquer à une application extérieure (reflétant la logique de la réécriture de l'optimiseur effectuée plus tôt sur la sous-requête corrélée):

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;

Maintenant, l'optimiseur n'a pas besoin d'utiliser la réécriture Appliquer elle-même (donc la correspondance de colonne calculée fonctionne comme prévu) et le prédicat n'est pas optimisé non plus non plus, l'index filtré peut donc être utilisé pour les opérations d'accès aux données et la recherche utilise la Comp colonne des deux côtés:

Outer Apply Plan

Cela serait généralement préféré sur l'ajout de colonnes A, B et C comme INCLUDEd dans l'index filtré, car il aborde la cause première du problème et ne nécessite pas d'élargir inutilement l'index.

Colonnes calculées persistées

En tant que note latérale, il n'est pas nécessaire de marquer la colonne calculée comme PERSISTED, si cela ne vous dérange pas de répéter sa définition dans une contrainte de CHECK:

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 colonne calculée n'est requise que pour être PERSISTED dans ce cas si vous souhaitez utiliser un NOT NULL Contrainte ou pour référencer directement la colonne Comp (au lieu de répéter sa définition) dans une contrainte CHECK.

20
Paul White 9

Bien que cela puisse être un peu de co-incidence en raison de la nature artificielle de vos données de test, comme vous avez mentionné SQL 2012, j'ai essayé une réécriture:

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;

Cela a donné un bon plan à faible coût à l'aide de votre index et avec des lectures de manière significative que les autres options (et les mêmes résultats pour vos données de test).

Plan Explorer costs for four options: Original; original with hint; outer apply and Lead

Je soupçonne que vos données réelles sont plus compliquées, il pourrait y avoir des scénarios où cette requête se comporte sémantine différente de la vôtre, mais elle montre parfois que les nouvelles fonctionnalités peuvent faire une réelle différence.

J'ai expérimenté certaines données plus variées et j'ai trouvé certains scénarios à correspondre et certains non:

--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'
6
wBob