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 clusterisé ItemTran
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 y a une conversion implicite bit_field_v41
de son type de base ( tinyint
ou smallint
) eninteger
.
La conversion se produit car l' 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é du type de données signifient la priorité inférieurebit_field_v41
valeur de champ de 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 l' bit_field_v41
est tinyint
pas de conversions se produisent à tous. De même, CONVERT(smallint, 4)
pourrait être utilisé si tel bit_field_v41
est le cas 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 ( 1 <> 2
c'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 pour 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 approprié FILLFACTOR
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 table comme indiqué dans le plan de requête de la question, un index clusterisé très fragmenté a SELECT * FROM view
pris 15 secondes aprèsDBCC 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 de l'explicite CONVERT
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
Je montre le prédicat de filtre ici principalement pour illustrer comment les deux NOT IN
listes 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 étendue 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 dans le 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 booking_no_v32
colonne (Hash Keys Build) de la table ItemTran, puis recherche des correspondances à l'aide de la booking_no
colonne (Hash Keys Probe) de la table Bookings. 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:
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 par hachage) vaut le faible coût du filtre supplémentaire. Le filtre entier ne peut pas être poussé vers le bas car il comprend un test sur une colonne de la table des réservations, mais la plupart peuvent l'être.
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 vers le stockage physique de tempdb , 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 à une vitesse très lentealgorithme de renflouement. Le déversement de 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 la classe d'événements d'avertissement de hachage à l' aide de Profiler, mais il peut être difficile de corréler les avertissements avec une requête particulière.
Corriger les 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 manuelles à plusieurs colonnes:
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 du 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, donc 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 les 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 une analyse et des tests très minutieux .
-- 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';
Dans le nouveau plan, la correspondance de hachage n'a pas de prédicat résiduel , il n'y a pas de filtre complexe , pas de prédicat résiduel sur la recherche de vue indexée 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:
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 WHERE
prédicats de la clause complexe 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 SELECT * FROM view
test 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 le 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.