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.
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
Cela montre qu'une recherche clé est utilisée, dont les détails sont:
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
?
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.
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 C
sont 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.
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):
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.
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:
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.
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
.
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).
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'