L'index sur la colonne calculée persistante nécessite une recherche de clé pour obtenir les colonnes dans l'expression calculée

24

J'ai une colonne calculée persistante sur une table qui est simplement constituée de colonnes concaténées, par exemple

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 Compn'est pas unique, et D est la date de début valide de chaque combinaison de A, B, C, donc j'utilise la requête suivante pour obtenir la date de fin pour chacun A, B, C(essentiellement la prochaine date de début pour 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 (et aussi à 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 cependant surpris. J'aurais pensé que puisque j'ai une clause where qui l'indique D IS NOT NULLet que je trie par Comp, et que je ne fais référence à aucune colonne en dehors de l'index, l'index sur la colonne calculée pourrait être utilisé pour analyser t1 et t2, mais j'ai vu un index clusterisé balayage.

entrez la description de l'image ici

J'ai donc forcé l'utilisation de cet indice pour voir s'il donnait 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

entrez la description de l'image ici

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

entrez la description de l'image ici

Maintenant, selon la documentation de 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 PERSISTED dans l'instruction CREATE 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 toute autre colonne dont dépend la colonne calculée est mise à jour. Le moteur de base de données utilise ces valeurs persistantes lorsqu'il 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 qui renvoie des expressions de colonne calculées, en particulier une fonction CLR créée dans le .NET Framework, est à la fois déterministe et précise.

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

Requêtes et DDL sur SQL Fiddle

NB J'ai marqué SQL Server 2008 car c'est la version sur laquelle se trouve mon problème principal, mais j'ai également le même comportement en 2012.

GarethD
la source

Réponses:

20

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

Les colonnes A, B, and C sont référencées dans le plan de requête - elles sont utilisées par la recherche T2.

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

L'optimiseur a décidé que l'analyse de l'index cluster était moins coûteuse que l'analyse de l'index filtré non cluster, puis l'exécution d'une recherche pour récupérer les valeurs des colonnes A, B et C.

Explication

La vraie question est de savoir pourquoi l'optimiseur a ressenti le besoin de récupérer A, B et C pour la recherche d'index. Nous nous attendons à ce qu'il lise la Compcolonne à l'aide d'un balayage d'index non cluster, puis effectue une recherche sur le même index (alias T2) pour localiser l'enregistrement Top 1.

L'optimiseur de requêtes développe les références de colonnes calculées avant le début de l'optimisation, pour lui permettre d'évaluer les coûts des différents plans de requête. Pour certaines requêtes, l'extension de 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» dans un formulaire qu'il trouve plus facile à raisonner. S'il ne parvient pas à trouver une simplification plus efficace, il a recours à la réécriture de la sous-requête corrélée en tant que demande (jointure corrélée):

Appliquer la réécriture

Il se trouve que cette application du déroulage place l'arbre de requête logique dans un formulaire qui ne fonctionne pas bien avec la normalisation du projet (une étape ultérieure qui cherche à faire correspondre les expressions générales aux colonnes calculées, entre autres).

Dans votre cas, la façon dont la requête est écrit Interagir avec les détails internes de l'optimiseur de telle sorte que la définition de l' expression élargie ne correspond pas à la colonne calculée, et vous finissez avec cherchent que les colonnes de références au A, B, and Clieu de la colonne calculée, Comp. Telle est la cause profonde.

solution de contournement

Une idée pour contourner cet effet secondaire est d'écrire la requête en tant qu'appliquer manuellement:

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'index filtré comme nous l'espérons. Le test d'inégalité sur la colonne Dà l'intérieur des rejets d'application NULLs, de sorte que le prédicat apparemment redondant WHERE T1.D IS NOT NULLest optimisé.

Sans ce prédicat explicite, la logique de correspondance d'index filtré décide qu'elle ne peut pas utiliser l'index filtré. Il existe un certain nombre de façons de contourner ce deuxième effet secondaire, mais la plus simple consiste probablement à modifier l'application croisée en une application externe (reflétant la logique de la réécriture de l'optimiseur effectuée précédemment 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;

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

Plan d'application externe

Cela serait généralement préférable à l'ajout de A, B et C en tant que INCLUDEdcolonnes dans l'index filtré, car il résout la cause première du problème et ne nécessite pas d'élargir inutilement l'index.

Colonnes calculées persistantes

En remarque, 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 CHECKcontrainte:

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 ne doit être PERSISTEDdans ce cas que si vous souhaitez utiliser une NOT NULLcontrainte ou référencer Compdirectement la colonne (au lieu de répéter sa définition) dans une CHECKcontrainte.

Paul White dit GoFundMonica
la source
2
+1 BTW Je suis tombé sur un autre cas de recherche superflue en regardant cela que vous pourriez (ou non) trouver intéressant. SQL Fiddle .
Martin Smith
@MartinSmith Oui, c'est intéressant. Une autre règle générique rewrite ( FOJNtoLSJNandLASJN) qui se traduit par des choses qui ne fonctionnent pas comme nous l'espérons, et qui laisse des ordures (BaseRow / Checksums) qui sont utiles dans certains types de plans (par exemple les curseurs) mais pas nécessaires ici.
Paul White dit GoFundMonica
Ah Chkest la somme de contrôle! Merci, je n'en étais pas sûr. À l'origine, je pensais que cela pourrait être lié aux contraintes de vérification.
Martin Smith
6

Bien que cela puisse être un peu une co-incidence en raison de la nature artificielle de vos données de test, comme vous l'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 en utilisant votre index et avec des lectures beaucoup plus faibles que les autres options (et les mêmes résultats pour vos données de test).

Plan Explorer coûte quatre options: Original;  original avec un soupçon;  application externe et plomb

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

J'ai fait des expériences avec des données plus variées et j'ai trouvé des scénarios pour correspondre et d'autres 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'
wBob
la source
1
Eh bien, il utilise l'index, mais seulement jusqu'à un certain point. Si compn'est pas une colonne calculée, vous ne voyez pas le tri.
Martin Smith
Merci. Mon scénario réel n'est pas beaucoup plus compliqué et la LEADfonction a fonctionné exactement comme je le souhaiterais sur mon instance locale de 2012 express. Malheureusement, cet inconvénient mineur pour moi n'a pas encore été considéré comme une raison suffisante pour mettre à niveau les serveurs de production ...
GarethD
-1

Lorsque j'ai essayé d'effectuer les mêmes actions, j'ai obtenu les autres résultats. Tout d'abord, mon plan d'exécution pour une table sans index se présente comme suit:entrez la description de l'image ici

Comme nous pouvons le voir à partir de l'analyse d'index clusterisé (t2), le prédicat est utilisé pour déterminer les lignes nécessaires à renvoyer (en raison de la condition):

entrez la description de l'image ici

Lorsque l'index a été ajouté, peu importe qu'il ait été défini par l'opérateur WITH ou non, le plan d'exécution est devenu le suivant:

entrez la description de l'image ici

Comme nous pouvons le voir, l'analyse d'index en cluster est remplacée par l'analyse d'index. Comme nous l'avons vu ci-dessus, SQL Server utilise les colonnes source de la colonne calculée pour effectuer la correspondance de la requête imbriquée. Pendant l'analyse d'index clusterisé, toutes ces valeurs peuvent être acquises en même temps (aucune opération supplémentaire n'est requise). Lorsque l'index a été ajouté, le filtrage des lignes nécessaires de la table (dans la sélection principale) s'effectue en fonction de l'index, mais les valeurs des colonnes source pour la colonne calculée compdoivent encore être obtenues (dernière opération Nested Loop) .

entrez la description de l'image ici

Pour cette raison, l'opération de recherche de clé est utilisée - pour obtenir les données des colonnes source de celle calculée.

PS ressemble à un bogue dans SQL Server.

Sandr
la source