web-dev-qa-db-fra.com

Le plan d'exécution montre une opération CONVERT_IMPLICIT coûteuse. Puis-je résoudre ce problème avec l'indexation ou dois-je changer la table?

J'ai une vue très importante et très lente qui inclut des conditions vraiment laides comme celle-ci dans sa clause where. Je suis également conscient que les jointures sont des jointures brutes et lentes sur varchar(13) au lieu des champs d'identité entiers, mais je voudrais améliorer la requête simple ci-dessous qui utilise cette vue:

CREATE VIEW [dbo].[vwReallySlowView]  AS  
AS  
SELECT     
  I.booking_no_v32 AS bkno, 
  I.trans_type_v41 AS trantype, 
  B.Assigned_to_v61 AS Assignbk, 
  B.order_date AS dateo, B.HourBooked AS HBooked,   
  B.MinBooked AS MBooked, B.SecBooked AS SBooked, 
  I.prep_on AS Pon, I.From_locn AS Flocn, 
  I.Trans_to_locn AS TTlocn,   
                      (CASE I.prep_on WHEN 'Y' THEN I.PDate ELSE I.FirstDate END) AS PrDate, I.PTimeH AS PrTimeH, I.PTimeM AS PrTimeM,   
                      (CASE WHEN I.RetnDate < I.FirstDate THEN I.FirstDate ELSE I.RetnDate END) AS RDatev, I.bit_field_v41 AS bitField, I.FirstDate AS FDatev, I.BookDate AS DBooked,   
                      I.TimeBookedH AS TBookH, I.TimeBookedM AS TBookM, I.TimeBookedS AS TBookS, I.del_time_hour AS dth, I.del_time_min AS dtm, I.return_to_locn AS rtlocn,   
                      I.return_time_hour AS rth, I.return_time_min AS rtm, (CASE WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty < I.QtyCheckedOut)   
                      THEN 0 WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty >= I.QtyCheckedOut) THEN I.Trans_Qty - I.QtyCheckedOut ELSE I.trans_qty END) AS trqty,   
                      (CASE WHEN I.Trans_type_v41 IN (6, 7) THEN 0 ELSE I.QtyCheckedOut END) AS MyQtycheckedout, (CASE WHEN I.Trans_type_v41 IN (6, 7)   
                      THEN 0 ELSE I.QtyReturned END) AS retqty, I.ID, B.BookingProgressStatus AS bkProg, I.product_code_v42, I.return_to_locn, I.AssignTo, I.AssignType,   
                      I.QtyReserved, B.DeprepOn,  
        (CASE  B.DeprepOn       
        WHEN 1 THEN  B.DeprepDateTime     
        ELSE   I.RetnDate  
           END)  AS DeprepDateTime, I.InRack 
FROM         dbo.tblItemtran AS I 

INNER JOIN  -- booking_no = varchar(13)
         dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32  --  string inner-join

INNER JOIN  -- product_code = varchar(13) 
        dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code  --  string inner-join

WHERE     (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR  
                      (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR  
                      (I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR  
                      (I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)  

Cette vue est généralement utilisée comme ceci:

select * from vwReallySlowView
where product_code_v42  = 'LIGHTBULB100W'  -- find "100 watt lightbulb" rows

Lorsque je l'exécute, j'obtiens cet élément de plan d'exécution qui coûte 20 à 80% du coût total du lot, avec le prédicat CONVERT_IMPLICIT( .... &(4)) montrant qu'il semble être très lent à faire ces bitwise boolean tests Comme (I.ibitfield & 4 = 0).

Je ne suis pas un expert en MS SQL ou en travail de type DBA en général car je suis un développeur de logiciels non-SQL la plupart du temps. Mais je soupçonne que de telles combinaisons au niveau du bit sont une mauvaise idée, et qu'il aurait été préférable d'avoir des champs booléens discrets.

Pourrais-je en quelque sorte améliorer cet index, pour mieux gérer cette vue sans changer le schéma (qui est déjà en production à des milliers d'emplacements) ou dois-je changer la table sous-jacente qui a plusieurs valeurs booléennes regroupées dans un entier bit_field_v41, pour résoudre ce problème?

Voici mon index cluster sur tblItemtran qui est analysé dans ce plan d'exécution:

-- goal:  speed up  select * from vwReallySlowView where productcode  = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran] 
(
    [product_code_v42] ASC,  -- varchar(13)
    [trans_type_v41] ASC     -- int
)WITH ( PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, 
        IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, 
        ALLOW_PAGE_LOCKS  = ON) 
ON [PRIMARY]

Voici le plan d'exécution, pour l'un des autres produits qui entraîne un coût de 27% sur ce prédicat CONVERT_IMPLICIT. mise à jour Notez que dans ce cas, mon pire nœud est maintenant "hash match" sur un inner join, qui coûte 34% I crois que c'est un coût que je ne peux pas éviter à moins que je ne puisse éviter de faire des jointures sur des chaînes dont je ne peux pas actuellement me débarrasser. Les deux opérations INNER JOIN Dans la vue ci-dessus se trouvent sur les champs varchar(13).

Zoom avant dans le coin inférieur droit:

enter image description here

L'ensemble du plan d'exécution au format .sqlplan est disponible sur skydrive. Cette image n'est qu'un aperçu visuel. Cliquez sur ici pour voir l'image par elle-même.

enter image description here

La mise à jour a publié l'intégralité du plan d'exécution. Je n'arrive pas à trouver quelle valeur product_code Était pathologiquement mauvaise, mais une façon de le faire est de select count(*) from view au lieu de faire un seul produit. Mais les produits qui ne sont utilisés que dans 5% des enregistrements de la table sous-jacente ou moins semblent afficher des coûts beaucoup plus faibles dans l'opération CONVERT_IMPLICIT. Si je devais corriger le SQL ici, je pense que je prendrais la clause brute WHERE dans la vue, et calculer et stocker le résultat de cette condition géante de clause where en tant que bit "IncludeMeInTheView" - dans la table sous-jacente. Presto, problème résolu, non?

24
Warren P

Vous ne devriez pas trop vous fier aux pourcentages de coûts dans les plans d'exécution. Ce sont toujours des coûts estimés, même dans les plans de post-exécution avec des chiffres "réels" pour des choses comme le nombre de lignes. Les coûts estimés sont basés sur un modèle qui fonctionne assez bien pour l'objectif auquel il est destiné: permettre à l'optimiseur de choisir entre différents plans d'exécution candidats pour la même requête. Les informations sur les coûts sont intéressantes et un facteur à considérer, mais elles devraient rarement être une métrique principale pour le réglage des requêtes. L'interprétation des informations du plan d'exécution nécessite une vue plus large des données présentées.

Opérateur de recherche d'index clustered ItemTran

ItemTran Clustered Index Seek

Cet opérateur est vraiment deux opérations en une. D'abord, une opération de recherche d'index trouve toutes les lignes qui correspondent au prédicat product_code_v42 = 'M10BOLT', Puis chaque ligne a le prédicat résiduel bit_field_v41 & 4 = 0 Appliqué. Il existe une conversion implicite de bit_field_v41 De son type de base (tinyint ou smallint) vers integer.

La conversion se produit car opérateur AND au niveau du bit (&) requiert que les deux opérandes soient du même type. Le type implicite de la valeur constante "4" est un entier et les règles de priorité des types de données signifient que la valeur du champ bit_field_v41 De priorité inférieure est convertie.

Le problème (tel qu'il est) est facilement corrigé en écrivant le prédicat sous la forme bit_field_v41 & CONVERT(tinyint, 4) = 0 - ce qui signifie que la valeur constante a la priorité la plus faible et est convertie (pendant le pliage constant) plutôt que la valeur de la colonne. Si bit_field_v41 Est tinyint aucune conversion ne se produit. De même, CONVERT(smallint, 4) pourrait être utilisé si bit_field_v41 Est smallint. Cela dit, la conversion n'est pas un problème de performances dans ce cas, mais il est toujours recommandé de faire correspondre les types et d'éviter les conversions implicites lorsque cela est possible.

La majeure partie du coût estimé de cette recherche est due à la taille de la table de base. Bien que la clé d'index cluster soit elle-même raisonnablement étroite, la taille de chaque ligne est grande. Une définition de la table n'est pas donnée, mais seules les colonnes utilisées dans la vue totalisent une largeur de ligne significative. Étant donné que l'index cluster inclut toutes les colonnes, la distance entre les clés d'index cluster est la largeur de la ligne, pas la largeur des clés d'index. L'utilisation de suffixes de version sur certaines colonnes suggère que la vraie table a encore plus de colonnes pour les versions précédentes.

En regardant les colonnes de recherche, de prédicat résiduel et de sortie, les performances de cet opérateur peuvent être vérifiées isolément en créant la requête équivalente (le 1 <> 2 Est une astuce pour empêcher l'auto-paramétrage, la contradiction est supprimée par l'optimiseur et n'apparaît pas dans le plan de requête):

SELECT
    it.booking_no_v32,
    it.QtyCheckedOut,
    it.QtyReturned,
    it.Trans_qty,
    it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
    1 <> 2
    AND it.product_code_v42 = 'M10BOLT'
    AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;

Les performances de cette requête avec un cache de données froid sont intéressantes, car la lecture anticipée serait affectée par la fragmentation de la table (index cluster). La clé de clustering de cette table invite à la fragmentation, il peut donc être important de maintenir (réorganiser ou reconstruire) cet index régulièrement et d'utiliser un FILLFACTOR approprié pour laisser de l'espace pour les nouvelles lignes entre les fenêtres de maintenance d'index.

J'ai effectué un test de l'effet de la fragmentation sur la lecture anticipée à l'aide d'exemples de données générées à l'aide de SQL Data Generator . En utilisant le même nombre de lignes de tableau comme indiqué dans le plan de requête de la question, un index clusterisé très fragmenté a entraîné SELECT * FROM view 15 secondes après DBCC DROPCLEANBUFFERS. Le même test dans les mêmes conditions avec un index cluster fraîchement reconstruit sur la table ItemTrans terminé en 3 secondes.

Si les données de la table sont généralement entièrement dans le cache, le problème de fragmentation est beaucoup moins important. Mais, même avec une faible fragmentation, les larges lignes du tableau peuvent signifier que le nombre de lectures logiques et physiques est beaucoup plus élevé que prévu. Vous pouvez également expérimenter l'ajout et la suppression du CONVERT explicite pour valider mon attente que le problème de conversion implicite n'est pas important ici, sauf en tant que violation des meilleures pratiques.

Plus précisément, le nombre estimé de lignes quittant l'opérateur de recherche. L'estimation du temps d'optimisation est de 165 lignes, mais 4 226 ont été produites au moment de l'exécution. Je reviendrai sur ce point plus tard, mais la principale raison de cet écart est que la sélectivité du prédicat résiduel (impliquant le bit à bit ET) est très difficile à prédire pour l'optimiseur - en fait, il a recours à la supposition.

Opérateur de filtre

Filter Operator

Je montre le prédicat de filtre ici principalement pour illustrer comment les deux listes NOT IN Sont combinées, simplifiées puis développées, et également pour fournir une référence pour la discussion de correspondance de hachage suivante. La requête de test de la recherche peut être développée pour incorporer ses effets et déterminer l'effet de l'opérateur Filter sur les performances:

SELECT
    it.booking_no_v32,
    it.trans_type_v41,
    it.Trans_qty,
    it.QtyReturned,
    it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
    it.product_code_v42 = 'M10BOLT'
    AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
    AND
    (
        (
            it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
            AND it.trans_type_v41 NOT IN (6, 7)
        )
        OR
        (
            it.trans_type_v41 NOT IN (6, 7)
        )
        OR 
        (
            it.trans_type_v41 IN (6, 7)
            AND it.QtyCheckedOut = 0
        )
        OR 
        (
            it.trans_type_v41 IN (6, 7)
            AND it.QtyCheckedOut > 0
            AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
        )
    );

L'opérateur Calculer scalaire du plan définit l'expression suivante (le calcul lui-même est différé jusqu'à ce que le résultat soit demandé par un opérateur ultérieur):

[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))

L'opérateur Hash Match

La réalisation d'une jointure sur des types de données de caractères n'est pas la raison du coût estimé élevé de cet opérateur. L'info-bulle SSMS affiche uniquement une entrée de sonde de clés de hachage, mais les détails importants se trouvent dans la fenêtre Propriétés SSMS.

L'opérateur Hash Match crée une table de hachage à l'aide des valeurs de la colonne booking_no_v32 (Hash Keys Build) de la table ItemTran, puis recherche des correspondances à l'aide de la colonne booking_no (Hash Keys Probe) à partir de la table Réservations. L'info-bulle SSMS afficherait également normalement une sonde résiduelle, mais le texte est beaucoup trop long pour une info-bulle et est simplement omis.

Un résidu de sonde est similaire au résidu observé après la recherche de l'indice plus tôt; le prédicat résiduel est évalué sur toutes les lignes qui correspondent au hachage pour déterminer si la ligne doit être transmise à l'opérateur parent. La recherche de correspondances de hachage dans une table de hachage bien équilibrée est extrêmement rapide, mais l'application d'un prédicat résiduel complexe à chaque ligne qui correspond est assez lente en comparaison. L'info-bulle Hash Match dans Plan Explorer affiche les détails, y compris l'expression résiduelle de la sonde:

Hash Match Operator

Le prédicat résiduel est complexe et inclut la vérification de l'état d'avancement de la réservation maintenant que la colonne est disponible dans le tableau des réservations. L'info-bulle montre également la même différence entre le nombre de lignes estimé et réel vu plus tôt dans la recherche d'index. Il peut sembler étrange qu'une grande partie du filtrage soit effectué deux fois, mais ce n'est que l'optimiseur optimiste. Il ne s'attend pas à ce que les parties du filtre qui peuvent être poussées vers le bas du plan à partir du résidu de la sonde éliminent toutes les lignes (les estimations du nombre de lignes sont les mêmes avant et après le filtre), mais l'optimiseur sait qu'il pourrait se tromper à ce sujet. La possibilité de filtrer les lignes plus tôt (en réduisant le coût de la jointure de hachage) vaut le faible coût du filtre supplémentaire. Le filtre entier ne peut pas être poussé vers le bas car il inclut un test sur une colonne de la table des réservations, mais la plupart peuvent l'être. Le test complet est toujours requis dans le test résiduel de hachage pour l'exactitude.

La sous-estimation du nombre de lignes est un problème pour l'opérateur de correspondance de hachage car la quantité de mémoire réservée pour la table de hachage est basée sur le nombre estimé de lignes. Lorsque la mémoire est trop petite pour la taille de la table de hachage requise au moment de l'exécution (en raison du plus grand nombre de lignes), la table de hachage se propage récursivement au physique tempdb stockage, ce qui entraîne souvent de très mauvaises performances. Dans le pire des cas, le moteur d'exécution arrête de manière récursive de déverser des seaux de hachage et recourt à un algorithme de sauvetage très lent. Le hachage (récursif ou renflouement) est la cause la plus probable des problèmes de performances décrits dans la question (pas les colonnes de jointure de type caractère ou les conversions implicites). La cause première serait que le serveur réserve trop peu de mémoire pour la requête basée sur une estimation incorrecte du nombre de lignes (cardinalité).

Malheureusement, avant SQL Server 2012, rien dans le plan d'exécution n'indique qu'une opération de hachage a dépassé son allocation de mémoire (qui ne peut pas croître dynamiquement après avoir été réservée avant le début de l'exécution, même si le serveur a des masses de mémoire libre) et a dû déborder tempdb. Il est possible de surveiller Hash Warning Event Class à l'aide de Profiler, mais il peut être difficile de corréler les avertissements avec une requête particulière.

Correction des problèmes

Les trois problèmes sont la fragmentation, le résidu de sonde complexe dans l'opérateur de correspondance de hachage et l'estimation de cardinalité incorrecte résultant de la supposition à la recherche d'index.

Solution recommandée

Vérifiez la fragmentation et corrigez-la si nécessaire, en planifiant la maintenance pour vous assurer que l'index reste organisé de manière acceptable. La manière habituelle de corriger l'estimation de cardinalité est de fournir des statistiques. Dans ce cas, l'optimiseur a besoin de statistiques pour la combinaison (product_code_v42, bitfield_v41 & 4 = 0). Nous ne pouvons pas créer directement de statistiques sur une expression, nous devons donc d'abord créer une colonne calculée pour l'expression de champ de bits, puis créer les statistiques multicolonnes manuelles:

ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);

CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);

La définition de texte de la colonne calculée doit correspondre à peu près exactement au texte de la définition de la vue pour les statistiques à utiliser.Par conséquent, la correction de la vue pour éliminer la conversion implicite doit être effectuée en même temps et il faut veiller à assurer une correspondance textuelle.

Les statistiques multi-colonnes devraient aboutir à de bien meilleures estimations, réduisant considérablement les chances que l'opérateur de correspondance de hachage utilise un déversement récursif ou l'algorithme de renflouement. L'ajout de la colonne calculée (qui est une opération de métadonnées uniquement et ne prend pas d'espace dans la table car elle n'est pas marquée PERSISTED) et les statistiques multi-colonnes est ma meilleure estimation d'une première solution.

Lors de la résolution de problèmes de performances de requête, il est important de mesurer des éléments tels que le temps écoulé, l'utilisation du processeur, les lectures logiques, les lectures physiques, les types et durées d'attente ... et ainsi de suite. Il peut également être utile d'exécuter des parties de la requête séparément pour valider les causes suspectes, comme indiqué ci-dessus.

Dans certains environnements, où une vue à la seconde près des données n'est pas importante, il peut être utile d'exécuter un processus d'arrière-plan qui matérialise la vue entière dans une table d'instantanés de temps en temps. Cette table est juste une table de base normale et peut être indexée pour les requêtes de lecture sans se soucier de l'impact sur les performances de mise à jour.

Afficher l'indexation

Ne soyez pas tenté d'indexer directement la vue d'origine. Les performances de lecture seront incroyablement rapides (une seule recherche sur un index de vue) mais (dans ce cas) tous les problèmes de performances dans les plans de requête existants seront transférés vers des requêtes qui modifient l'une des colonnes de table référencées dans la vue. Les requêtes qui modifient les lignes de la table de base seront en effet très affectées.

Solution avancée avec une vue indexée partielle

Il existe une solution de vue indexée partielle pour cette requête particulière qui corrige les estimations de cardinalité et supprime le filtre et la sonde résiduelle, mais elle est basée sur certaines hypothèses sur les données (principalement ma conjecture sur le schéma) et nécessite une mise en œuvre experte, en particulier en ce qui concerne approprié index pour prendre en charge les plans de maintenance des vues indexées. Je partage le code ci-dessous par intérêt, je ne vous propose pas de l'implémenter sans très prudent analyse et test.

-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
    it.ID,
    it.product_code_v42,
    it.trans_type_v41,
    it.booking_no_v32,
    it.Trans_qty,
    it.QtyReturned,
    it.QtyCheckedOut,
    it.QtyReserved,
    it.bit_field_v41,
    it.prep_on,
    it.From_locn,
    it.Trans_to_locn,
    it.PDate,
    it.FirstDate,
    it.PTimeH,
    it.PTimeM,
    it.RetnDate,
    it.BookDate,
    it.TimeBookedH,
    it.TimeBookedM,
    it.TimeBookedS,
    it.del_time_hour,
    it.del_time_min,
    it.return_to_locn,
    it.return_time_hour,
    it.return_time_min,
    it.AssignTo,
    it.AssignType,
    it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
    tb.booking_no = it.booking_no_v32
WHERE
    (
        it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
        AND it.trans_type_v41 NOT IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
    )
    OR
    (
        it.trans_type_v41 NOT IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND tb.BookingProgressStatus = 1
    )
    OR 
    (
        it.trans_type_v41 IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND it.QtyCheckedOut = 0
    )
    OR 
    (
        it.trans_type_v41 IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND it.QtyCheckedOut > 0
        AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
    );
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO

La vue existante a été modifiée pour utiliser la vue indexée ci-dessus:

CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
    I.booking_no_v32 AS bkno,
    I.trans_type_v41 AS trantype,
    B.Assigned_to_v61 AS Assignbk,
    B.order_date AS dateo,
    B.HourBooked AS HBooked,
    B.MinBooked AS MBooked,
    B.SecBooked AS SBooked,
    I.prep_on AS Pon,
    I.From_locn AS Flocn,
    I.Trans_to_locn AS TTlocn,
    CASE I.prep_on 
        WHEN 'Y' THEN I.PDate
        ELSE I.FirstDate
    END AS PrDate,
    I.PTimeH AS PrTimeH,
    I.PTimeM AS PrTimeM,
    CASE
        WHEN I.RetnDate < I.FirstDate 
        THEN I.FirstDate 
        ELSE I.RetnDate
    END AS RDatev,
    I.bit_field_v41 AS bitField,
    I.FirstDate AS FDatev,
    I.BookDate AS DBooked,
    I.TimeBookedH AS TBookH,
    I.TimeBookedM AS TBookM,
    I.TimeBookedS AS TBookS,
    I.del_time_hour AS dth,
    I.del_time_min AS dtm,
    I.return_to_locn AS rtlocn,
    I.return_time_hour AS rth,
    I.return_time_min AS rtm,
    CASE
        WHEN
            I.Trans_type_v41 IN (6, 7) 
            AND I.Trans_qty < I.QtyCheckedOut
            THEN 0 
        WHEN
            I.Trans_type_v41 IN (6, 7)
            AND I.Trans_qty >= I.QtyCheckedOut
            THEN I.Trans_Qty - I.QtyCheckedOut
        ELSE
            I.trans_qty
    END AS trqty,
    CASE
        WHEN I.Trans_type_v41 IN (6, 7)
        THEN 0
        ELSE I.QtyCheckedOut
    END AS MyQtycheckedout,
    CASE
        WHEN I.Trans_type_v41 IN (6, 7)
        THEN 0
        ELSE I.QtyReturned
    END AS retqty,
    I.ID,
    B.BookingProgressStatus AS bkProg,
    I.product_code_v42,
    I.return_to_locn,
    I.AssignTo,
    I.AssignType,
    I.QtyReserved,
    B.DeprepOn,
    CASE B.DeprepOn
        WHEN 1 THEN B.DeprepDateTime
        ELSE I.RetnDate
    END AS DeprepDateTime,
    I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
    B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
    I.product_code_v42 = M.product_code;

Exemple de plan de requête et d'exécution:

SELECT
    vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';

New execution plan

Dans le nouveau plan, la correspondance de hachage a pas de prédicat résiduel, il y a pas de filtre complexe, pas de prédicat résiduel activé la vue indexée cherche, et les estimations de cardinalité sont exactement correctes.

Comme exemple de la façon dont les plans d'insertion/mise à jour/suppression seraient affectés, voici le plan d'une insertion dans la table ItemTrans:

Insert plan

La section en surbrillance est nouvelle et requise pour la maintenance des vues indexées. La bobine de table rejoue les lignes de table de base insérées pour la maintenance de la vue indexée. Chaque ligne est jointe à la table des réservations à l'aide d'une recherche d'index clusterisé, puis un filtre applique les prédicats de la clause complexe WHERE pour voir si la ligne doit être ajoutée à la vue. Si tel est le cas, une insertion est effectuée dans l'index cluster de la vue.

Le même test SELECT * FROM view Effectué précédemment s'est terminé en 150 ms avec la vue indexée en place.

Dernière chose: je remarque que votre serveur 2008 R2 est toujours à RTM. Cela ne résoudra pas vos problèmes de performances, mais Service Pack 2 pour 2008 R2 est disponible depuis juillet 2012, et il existe de nombreuses bonnes raisons de rester aussi à jour que possible avec les service packs.

49
Paul White 9