Impossible de créer un index filtré sur une colonne calculée

18

Dans une question précédente, est-ce une bonne idée de désactiver l'escalade de verrous lors de l'ajout de nouvelles colonnes calculées à une table? , Je crée une colonne calculée:

ALTER TABLE dbo.tblBGiftVoucherItem
ADD isUsGift AS CAST
(
    ISNULL(
        CASE WHEN sintMarketID = 2 
            AND strType = 'CARD'
            AND strTier1 LIKE 'GG%' 
        THEN 1 
        ELSE 0 
        END
    , 0) 
    AS BIT
) PERSISTED;

La colonne calculée est PERSISTED, et selon la définition_colonne_calculée (Transact-SQL) :

PERSISTE

Spécifie que le moteur de base de données stockera physiquement les valeurs calculées dans la table et mettra à jour les valeurs lorsque toute autre colonne dont dépend la colonne calculée est mise à jour. Marquer une colonne calculée comme PERSISTED permet de créer un index sur une colonne calculée qui est déterministe, mais pas précis. Pour plus d'informations, consultez Index sur les colonnes calculées. Toutes les colonnes calculées utilisées comme colonnes de partitionnement d'une table partitionnée doivent être explicitement marquées PERSISTED. expression_colonne_calculée doit être déterministe lorsque PERSISTED est spécifié.

Mais lorsque j'essaie de créer un index sur ma colonne, j'obtiens l'erreur suivante:

CREATE INDEX FIX_tblBGiftVoucherItem_incl
ON dbo.tblBGiftVoucherItem (strItemNo) 
INCLUDE (strTier3)
WHERE isUsGift = 1;

Impossible de créer l'index filtré 'FIX_tblBGiftVoucherItem_incl' sur la table 'dbo.tblBGiftVoucherItem' car la colonne 'isUsGift' dans l'expression de filtre est une colonne calculée. Réécrivez l'expression de filtre afin qu'elle n'inclue pas cette colonne.

Comment puis-je créer un index filtré sur une colonne calculée?

ou

Existe-t-il une solution alternative?

Marcello Miorelli
la source
3
Vous pouvez cependant créer un index filtré WHERE (sintMarketID = 2 AND strType = 'CARD' AND strTier1 LIKE 'GG%').
ypercubeᵀᴹ

Réponses:

21

Malheureusement, depuis SQL Server 2014, il n'est pas possible de créer un Filtered Indexemplacement où le filtre se trouve sur une colonne calculée (indépendamment du fait qu'il soit persistant ou non).

Un élément Connect est ouvert depuis 2009, alors allez-y et votez pour. Peut-être que Microsoft corrigera cela un jour.

Aaron Bertrand a un article qui couvre un certain nombre d'autres problèmes avec les indices filtrés .

Mark Sinkinson
la source
21

Bien que vous ne puissiez pas créer un index filtré sur une colonne persistante, il existe une solution de contournement assez simple que vous pouvez utiliser.

À titre de test, j'ai créé une table simple avec une IDENTITYcolonne et une colonne calculée persistante basée sur la colonne d'identité:

USE tempdb;

CREATE TABLE dbo.PersistedViewTest
(
    PersistedViewTest_ID INT NOT NULL
        CONSTRAINT PK_PersistedViewTest
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(2000) NOT NULL
    , TestComputedColumn AS (PersistedViewTest_ID - 1) PERSISTED
);
GO

Ensuite, j'ai créé une vue liée au schéma basée sur la table avec un filtre sur la colonne calculée:

CREATE VIEW dbo.PersistedViewTest_View
WITH SCHEMABINDING
AS
SELECT PersistedViewTest_ID
    , SomeData 
    , TestComputedColumn
FROM dbo.PersistedViewTest
WHERE TestComputedColumn < CONVERT(INT, 27);

Ensuite, j'ai créé un index cluster sur la vue liée au schéma, ce qui a pour effet de conserver les valeurs stockées dans la vue, y compris la valeur de la colonne calculée:

CREATE UNIQUE CLUSTERED INDEX IX_PersistedViewTest
ON dbo.PersistedViewTest_View(PersistedViewTest_ID);
GO

Insérez des données de test dans le tableau:

INSERT INTO dbo.PersistedViewTest (SomeData)
SELECT o.name + o1.name + o2.name
FROM sys.objects o
    CROSS JOIN sys.objects o1
    CROSS JOIN sys.objects o2;

Créez un élément de statistiques et un index sur la vue:

CREATE STATISTICS ST_PersistedViewTest_View
ON dbo.PersistedViewTest_View(TestComputedColumn)
WITH FULLSCAN;

CREATE INDEX IX_PersistedViewTest_View_TestComputedColumn
ON dbo.PersistedViewTest_View(TestComputedColumn);

L'exécution d' SELECTinstructions sur la table avec la colonne persistante peut désormais utiliser automatiquement la vue persistante, si l'optimiseur de requête détermine qu'il est logique de le faire:

SELECT pv.PersistedViewTest_ID
    , pv.TestComputedColumn
FROM dbo.PersistedViewTest pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)

Le plan d'exécution réel pour la requête ci-dessus montre que l'optimiseur de requête a choisi d'utiliser la vue persistante pour renvoyer les résultats:

entrez la description de l'image ici

Vous avez peut-être remarqué la conversion explicite dans la WHEREclause ci-dessus. Cette explicite CONVERT(INT, 26)permet à l'optimiseur de requête d'utiliser correctement l'objet de statistiques pour estimer le nombre de lignes qui seront renvoyées par la requête. Si nous écrivons la requête avec WHERE pv.TestComputedColumn = 26, l'optimiseur de requête peut ne pas estimer correctement le nombre de lignes car 26 est en fait considéré comme un TINY INT; cela peut empêcher SQL Server d'utiliser la vue persistante. Les conversions implicites peuvent être très douloureuses et il est avantageux d'utiliser systématiquement les types de données corrects pour les comparaisons et les jointures.

Bien sûr, tous les "pièges" standard résultant de l'utilisation de la liaison de schéma s'appliquent au scénario ci-dessus; cela peut empêcher l'utilisation de cette solution de contournement dans tous les scénarios. Par exemple, il ne sera plus possible de modifier la table de base sans d'abord supprimer la liaison de schéma de la vue. Pour ce faire, vous devrez supprimer l'index cluster de la vue.

Si vous ne disposez pas de SQL Server Enterprise Edition, l'optimiseur de requête n'utilisera pas automatiquement la vue persistante pour les requêtes qui ne référencent pas directement la vue à l'aide de l' WITH (NOEXPAND)indicateur. Pour tirer parti de l'utilisation de la vue persistante dans les versions non Enterprise Edition, vous devrez réécrire la requête ci-dessus sur quelque chose comme:

SELECT pv.PersistedViewTest_ID
    , pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv WITH (NOEXPAND)
WHERE pv.TestComputedColumn = CONVERT(INT, 26)

Merci à Ian Ringrose pour avoir souligné la limitation Enterprise Edition ci-dessus et à Paul White pour l' (NOEXPAND)astuce.

Cette réponse de Paul contient des détails intéressants sur l'optimiseur de requêtes par rapport aux vues persistantes.

Max Vernon
la source
Le travail autour montre qu'un index cluster et un index non cluster sont créés sur la vue. L'index non cluster doit-il être utilisé sur l'index cluster pour une raison quelconque? Ou, l'index non cluster est-il plus performant? Si l'index cluster était utilisé dans la requête, que montreraient les statistiques?
Bob Bryan
Question intéressante, @BobBryan - l'index cluster est requis pour permettre à la vue de persister, bien qu'il ne doive pas réellement être un index unique. J'aurais pu créer l'index cluster de la vue sur une autre colonne, comme la TestComputedColumnplace. Cependant, comme l'index cluster contient toutes les données de la table / vue, j'ai décidé qu'il serait probablement préférable d'utiliser un nombre croissant monotone comme clé de clustering. Remarque, je n'ai pas réellement testé cette supposition, et elle peut en fait être incorrecte pour certaines variations de la repro.
Max Vernon
Remarque, l'index non cluster n'est pas un index de couverture, et en tant que telle, toute requête qui filtre, joint ou renvoie des colonnes de la vue ou de la table sous-jacente devra effectuer une opération de recherche de clé sur la table de base ou la vue. Il est probable que pour un scénario réel, la portée limitée de ma réponse pourrait être exposée avec des performances encore meilleures à l'esprit.
Max Vernon
4

Depuis Create Indexet sa whereclause, cela n'est pas possible:

Crée un index filtré en spécifiant les lignes à inclure dans l'index. L'index filtré doit être un index non cluster sur une table. Crée des statistiques filtrées pour les lignes de données dans l'index filtré.

Le prédicat de filtre utilise une logique de comparaison simple et ne peut pas référencer une colonne calculée, une colonne UDT, une colonne de type de données spatiales ou une colonne de type de données hierarchyID. Les comparaisons utilisant des littéraux NULL ne sont pas autorisées avec les opérateurs de comparaison. Utilisez plutôt les opérateurs IS NULL et IS NOT NULL.

Source: MSDN

Julien Vavasseur
la source
3
  • Vous avez besoin d'une colonne qui n'est pas calculée pour mettre l'index filtré.
  • Vous devez calculer la valeur pour aller dans cette colonne.

Avant de calculer les colonnes, nous utilisions des déclencheurs pour calculer la valeur des colonnes chaque fois que la ligne était modifiée ou insérée.

(Un déclencheur peut également être utilisé pour insérer / supprimer le PK de l'élément d'une deuxième table qui a ensuite été utilisée dans les requêtes.)

Ian Ringrose
la source
3

Il s'agit d'une tentative d'améliorer le travail de Max Vernon . Dans sa solution, il propose d'utiliser 2 index sur la vue et un objet statistique.

Le 1er index est clusterisé, ce qui est en fait nécessaire car contrairement à un index non clusterisé sur une table, une erreur sera générée si la création d'un index non clusterisé sur la vue est tentée sans avoir au préalable un index clusterisé.

Le 2e index est un index non cluster, qui est utilisé comme index derrière la requête. Dans la section des commentaires de sa réponse, j'ai demandé ce qui se passerait si un index cluster était utilisé au lieu d'un index non cluster.

L'analyse suivante tente de répondre à cette question.

J'utilise son même code exact, sauf que je ne crée pas d'index non cluster sur la vue.

Je ne crée pas non plus d'objet de statistiques. Si vous suivez et utilisez SQL Server Management Studio (SSMS) pour entrer le code ci-dessous, vous devez savoir que vous pouvez voir des lignes rouges ondulées - qui ressemblent à des erreurs. Ce ne sont (probablement) pas des erreurs, mais impliquent un problème avec intellisense.

Vous pouvez désactiver Intellisense ou simplement ignorer les erreurs et exécuter les commandes. Ils doivent se terminer sans erreur.

-- Create the test table that uses a computed column.
USE tempdb;
CREATE TABLE dbo.PersistedViewTest
(
    PersistedViewTest_ID INT NOT NULL
    CONSTRAINT PK_PersistedViewTest
    PRIMARY KEY CLUSTERED
    IDENTITY(1,1)
    , SomeData VARCHAR(2000) NOT NULL
    , TestComputedColumn AS (PersistedViewTest_ID - 1) PERSISTED
);
GO

-- Insert some test data into the table.
INSERT INTO dbo.PersistedViewTest (SomeData)
SELECT o.name + o1.name + o2.name
FROM sys.objects o
    CROSS JOIN sys.objects o1
    CROSS JOIN sys.objects o2;
GO

Le plan d'exécution suivant (sans vue vue / index) est produit après l'exécution de la requête suivante sur la table:

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

entrez la description de l'image ici

Cela donne une référence à comparer. Notez qu'une fois la requête terminée, un objet de statistiques a été créé (_WA_Sys_00000003_1FCDBCEB). L'objet de statistiques PK_PersistedViewTest a été créé lors de la création de l'index de table en cluster.

Ensuite, la vue filtrée et l'index cluster sur cette vue sont créés:

-- Create filtered view on the computed column.
CREATE VIEW dbo.PersistedViewTest_View
WITH SCHEMABINDING
AS
SELECT PersistedViewTest_ID, SomeData, TestComputedColumn
FROM dbo.PersistedViewTest
WHERE TestComputedColumn < CONVERT(INT, 27);
GO

-- Create unique clustered index to persist the values, including the computed column.
CREATE UNIQUE CLUSTERED INDEX IX_PersistedViewTest
ON dbo.PersistedViewTest_View(PersistedViewTest_ID);
GO

Maintenant, essayons de relancer la requête, mais cette fois par rapport à la vue:

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

Le nouveau plan d'exécution est désormais:

entrez la description de l'image ici

Si l'on en croit le nouveau plan, après l'ajout de la vue et de l'index cluster sur cette vue, les statistiques semblent indiquer que le temps requis pour exécuter la requête a maintenant doublé. Notez également qu'aucun nouvel objet de statistiques n'a été créé pour prendre en charge le nouvel index après l'exécution de la requête, ce qui est différent de la requête sur la table.

Le plan de requête suggère toujours que la création d'un index non cluster serait très utile pour améliorer les performances de la requête. Cela signifie-t-il donc qu'un index non cluster doit être ajouté à la vue avant d'obtenir l'amélioration des performances souhaitée? Il y a une dernière chose à essayer. Modifiez la requête pour utiliser l'option "WITH NOEXPAND":

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv WITH (NOEXPAND)
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

Il en résulte le plan de requête suivant:

entrez la description de l'image ici

Ce plan d'exécution ressemble assez à celui qui a été produit avec l'index non cluster donné dans la réponse de Max Vernon. Mais, celui-ci se fait avec un index de moins (non cluster) et un objet de statistiques en moins.

Il s'avère que l'option NOEXPAND doit être utilisée avec les versions express et standard de SQL Server pour utiliser correctement une vue indexée. Paul White a un excellent article qui expose les avantages de l'utilisation de l'option NOEXPAND. Il recommande également que cette option soit utilisée avec l'édition entreprise pour garantir que la garantie d'unicité fournie par les index de vue est utilisée par l'optimiseur.

L'analyse ci-dessus a été effectuée avec l'édition express de SQL Sever 2014. Je l'ai également essayée avec l'édition développeur de SQL Server 2016. L'option NOEXPAND ne semble pas être requise avec l'édition de développement pour obtenir des gains de performances, mais est toujours recommandée. .

Il y a moins de 5 mois, Microsoft a rendu les éditions développeur gratuites . La licence limite l'utilisation au développement uniquement, ce qui signifie que la base de données ne peut pas être utilisée dans un environnement de production. Donc, si vous avez cherché à tester des tables optimisées en mémoire, le chiffrement, R, etc., vous n'avez plus d'excuse sans licence. Je l'ai installé avec succès sur mon ordinateur il y a quelques jours avec SQL Server 2014 Express sans aucun problème.

Bob Bryan
la source