Comment optimiser la requête T-SQL à l'aide du plan d'exécution

15

J'ai une requête SQL que j'ai passé les deux derniers jours à essayer d'optimiser à l'aide d'essais et d'erreurs et du plan d'exécution, mais en vain. Veuillez me pardonner de le faire, mais je publierai le plan d'exécution complet ici. J'ai fait l'effort de rendre les noms de table et de colonne dans le plan de requête et d'exécution génériques à la fois pour la brièveté et pour protéger l'IP de mon entreprise. Le plan d'exécution peut être ouvert avec SQL Sentry Plan Explorer .

J'ai fait pas mal de T-SQL, mais l'utilisation de plans d'exécution pour optimiser ma requête est un nouveau domaine pour moi et j'ai vraiment essayé de comprendre comment le faire. Donc, si quelqu'un pouvait m'aider avec cela et expliquer comment ce plan d'exécution peut être déchiffré pour trouver des moyens dans la requête pour l'optimiser, je serais éternellement reconnaissant. J'ai beaucoup plus de requêtes à optimiser - j'ai juste besoin d'un tremplin pour m'aider avec cette première.

Voici la requête:

DECLARE @Param0 DATETIME     = '2013-07-29';
DECLARE @Param1 INT          = CONVERT(INT, CONVERT(VARCHAR, @Param0, 112))
DECLARE @Param2 VARCHAR(50)  = 'ABC';
DECLARE @Param3 VARCHAR(100) = 'DEF';
DECLARE @Param4 VARCHAR(50)  = 'XYZ';
DECLARE @Param5 VARCHAR(100) = NULL;
DECLARE @Param6 VARCHAR(50)  = 'Text3';

SET NOCOUNT ON

DECLARE @MyTableVar TABLE
(
    B_Var1_PK int,
    Job_Var1 varchar(512),
    Job_Var2 varchar(50)
)

INSERT INTO @MyTableVar (B_Var1_PK, Job_Var1, Job_Var2) 
SELECT B_Var1_PK, Job_Var1, Job_Var2 FROM [fn_GetJobs] (@Param1, @Param2, @Param3, @Param4, @Param6);

CREATE TABLE #TempTable
(
    TTVar1_PK INT PRIMARY KEY,
    TTVar2_LK VARCHAR(100),
    TTVar3_LK VARCHAR(50),
    TTVar4_LK INT,
    TTVar5 VARCHAR(20)
);

INSERT INTO #TempTable
SELECT DISTINCT
    T.T1_PK,
    T.T1_Var1_LK,
    T.T1_Var2_LK,
    MAX(T.T1_Var3_LK),
    T.T1_Var4_LK
FROM
    MyTable1 T
    INNER JOIN feeds.MyTable2 A ON A.T2_Var1 = T.T1_Var4_LK
    INNER JOIN @MyTableVar B ON B.Job_Var2 = A.T2_Var2 AND B.Job_Var1 = A.T2_Var3
GROUP BY T.T1_PK, T.T1_Var1_LK, T.T1_Var2_LK, T.T1_Var4_LK

-- This is the slow statement...
SELECT 
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK,
    C.C_Var1_PK,
    SUM(CONVERT(DECIMAL(18,4), A.A_Var1) + CONVERT(DECIMAL(18,4), A.A_Var2))
FROM #TempTable T
    INNER JOIN TableA (NOLOCK) A ON A.A_Var4_FK_LK  = T.TTVar1_PK
    INNER JOIN @MyTableVar     B ON B.B_Var1_PK     = A.Job
    INNER JOIN TableC (NOLOCK) C ON C.C_Var2_PK     = A.A_Var5_FK_LK
    INNER JOIN TableD (NOLOCK) D ON D.D_Var1_PK     = A.A_Var6_FK_LK
    INNER JOIN TableE (NOLOCK) E ON E.E_Var1_PK     = A.A_Var7_FK_LK  
    LEFT OUTER JOIN feeds.TableF (NOLOCK) F ON F.F_Var1 = T.TTVar5
WHERE A.A_Var8_FK_LK = @Param1
GROUP BY
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK 
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK, 
    C.C_Var1_PK


IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END
IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END

Ce que j'ai trouvé, c'est que la troisième déclaration (commentée comme étant lente) est la partie qui prend le plus de temps. Les deux déclarations avant de revenir presque instantanément.

Le plan d'exécution est disponible en XML sur ce lien .

Mieux vaut cliquer avec le bouton droit et enregistrer, puis ouvrir dans SQL Sentry Plan Explorer ou un autre logiciel de visualisation plutôt que de l'ouvrir dans votre navigateur.

Si vous avez besoin de plus d'informations de ma part sur les tableaux ou les données, n'hésitez pas à demander.

Néo
la source
2
Vos statistiques sont loin. À quand remonte la dernière fois que vous avez fragmenté les index ou mis à jour les statistiques? En outre, j'essayerais d'utiliser une table temporaire, au lieu de la variable de table, @MyTableVar, car l'optimiseur ne peut vraiment pas utiliser de statistiques sur les variables de table.
Adam Haines
Merci pour ta réponse Adam. Changer le @MyTableVar en une table temporaire n'a aucun effet, mais c'est seulement un petit nombre de lignes (qui peut être vu depuis le plan d'exécution). Qu'est-ce qui, dans le plan d'exécution, montre que mes statistiques sont loin? Indique-t-il quels index doivent être réorganisés ou reconstruits, et quelles tables doivent avoir des statistiques mises à jour?
Neo
3
Cette jointure de hachage en bas à droite a environ 24 000 lignes dans l'entrée de génération, mais 3 285 620 réels peuvent donc bien se répandre tempdb. c'est-à-dire les estimations pour les lignes résultant de la jointure entre TableAet @MyTableVarsont très éloignées. De plus, le nombre de lignes entrant dans les tris est beaucoup plus élevé que prévu, de sorte qu'elles pourraient également se répandre.
Martin Smith

Réponses:

22

Avant d'arriver à la réponse principale, vous devez mettre à jour deux logiciels.

Mises à jour logicielles requises

Le premier est SQL Server. Vous exécutez SQL Server 2008 Service Pack 1 (build 2531). Vous devez être mis à jour avec au moins le Service Pack actuel (SQL Server 2008 Service Pack 3 - build 5500). La version la plus récente de SQL Server 2008 au moment de la rédaction est le Service Pack 3, mise à jour cumulative 12 (build 5844).

Le deuxième logiciel est SQL Sentry Plan Explorer . Les dernières versions ont d'importantes nouvelles fonctionnalités et corrections, y compris la possibilité de télécharger directement un plan de requête pour une analyse experte (pas besoin de coller du XML n'importe où!)

Analyse du plan de requête

L'estimation de cardinalité pour la variable de table est exacte, grâce à une recompilation au niveau de l'instruction:

estimation variable de table

Malheureusement, les variables de table ne conservent pas de statistiques de distribution, tout ce que l'optimiseur sait, c'est qu'il y a six lignes; il ne sait rien des valeurs qui pourraient être dans ces six lignes. Cette information est cruciale étant donné que la prochaine opération est une jointure à une autre table. L'estimation de cardinalité à partir de cette jointure est basée sur une supposition sauvage par l'optimiseur:

première estimation jointe

À partir de là, le plan choisi par l'optimiseur est basé sur des informations incorrectes, il n'est donc pas étonnant que les performances soient si mauvaises. En particulier, la mémoire réservée aux tris et aux tables de hachage pour les jointures de hachage sera beaucoup trop petite. Au moment de l'exécution, les opérations de tri et de hachage débordantes seront déversées sur le disque physique tempdb.

SQL Server 2008 ne met pas cela en évidence dans les plans d'exécution; vous pouvez surveiller les déversements à l'aide des événements étendus ou des avertissements de tri du profileur et des avertissements de hachage . La mémoire est réservée aux tris et aux hachages basés sur les estimations de cardinalité avant le début de l'exécution et ne peut pas être augmentée pendant l'exécution, quelle que soit la quantité de mémoire disponible de votre serveur SQL. Des estimations précises du nombre de lignes sont donc cruciales pour tout plan d'exécution impliquant des opérations consommatrices de mémoire dans l'espace de travail.

Votre requête est également paramétrée. Vous devez envisager d'ajouter OPTION (RECOMPILE)à la requête si différentes valeurs de paramètres affectent le plan de requête. Vous devriez probablement envisager de l'utiliser de toute façon, afin que l'optimiseur puisse voir la valeur de @Param1au moment de la compilation. Si rien d'autre, cela peut aider l'optimiseur à produire une estimation plus raisonnable pour la recherche d'index indiquée ci-dessus, étant donné que la table est très grande et partitionnée. Il peut également permettre l'élimination de la partition statique.

Essayez à nouveau la requête avec une table temporaire au lieu de la variable de table et OPTION (RECOMPILE) . Vous devez également essayer de matérialiser le résultat de la première jointure dans une autre table temporaire et d'exécuter le reste de la requête par rapport à cela. Le nombre de lignes n'est pas si grand (3 285 620), donc cela devrait être assez rapide. L'optimiseur aura alors une estimation de cardinalité exacte et des statistiques de distribution pour le résultat de la jointure. Avec de la chance, le reste du plan se mettra bien en place.

En partant des propriétés affichées dans le plan, la requête matérialisante serait:

SELECT
    A.A_Var7_FK_LK,
    A.A_Var4_FK_LK,
    A.A_Var6_FK_LK, 
    A.A_Var5_FK_LK,
    A.A_Var1,
    A.A_Var2,
    A.A_Var3_FK_LK
INTO #AnotherTempTable
FROM @MyTableVar AS B
JOIN TableA AS A
    ON A.Job = B.B_Var1_PK
WHERE
    A_Var8_FK_LK = @Param1;

Vous pouvez également INSERTentrer dans une table temporaire prédéfinie (les types de données corrects ne sont pas affichés dans le plan, donc je ne peux pas faire cette partie). La nouvelle table temporaire peut ou non bénéficier d'index cluster et non cluster.

Paul White 9
la source
Merci beaucoup pour cette réponse approfondie. Désolé, il a fallu une semaine pour répondre - j'ai travaillé sur ce sujet chaque jour entrecoupé d'autres travaux. J'ai implémenté vos suggestions matérialisant la jointure à TableA dans #AnotherTempTable. Cela semblait avoir le meilleur impact - les autres suggestions (en utilisant une table temporaire au lieu d'une variable de table pour @MyTableVar, et en utilisant OPTION (RECOMPILE)n'a pas eu beaucoup d'effet ou pas du tout. Les 'Anonymize' et 'Post to SQLPerformance.com' les options de SQL Sentry Plan Explorer sont excellentes - je viens de les utiliser: answers.sqlperformance.com/questions/1087
Neo
-6

Je remarque qu'il devrait y avoir un PK sur @MyTableVar et je suis d'accord que #MyTableVar est souvent plus performant (en particulier avec un plus grand nombre de lignes).

La condition dans la clause where

   WHERE A.A_Var8_FK_LK = @Param1

doit être déplacé vers la jointure interne A ET. L'optimiseur n'est pas assez intelligent dans mon expérience pour le faire (désolé, je n'ai pas regardé le plan) et cela peut faire une énorme différence.

Si ces changements ne montrent pas d'amélioration, je créerais ensuite une autre table temporaire de A et toutes les choses auxquelles elle se joint à contraint (bien?) Par A.A_Var8_FK_LK = @ Param1 si ce regroupement est logique pour vous.

Créez ensuite un index cluster sur cette table temporaire (avant ou après la création) pour la condition de jointure suivante.

Joignez ensuite ce résultat aux quelques tables (F et T) qui restent.

Bam, qui a besoin d'un plan de requête puant lorsque les estimations de ligne sont fausses et ne sont parfois pas facilement améliorables de toute façon). Je suppose que vous disposez d'indices appropriés, ce que je vérifierais d'abord dans le plan.

Une trace peut montrer les déversements de tempdb qui peuvent ou non avoir un impact drastique.

Une autre approche alternative - qui est plus rapide à essayer au moins - consiste à classer les tables du plus petit nombre de lignes (A) au plus élevé, puis à commencer à ajouter la fusion, le hachage et la boucle aux jointures. Lorsque des indices sont présents, l'ordre de jointure est fixé comme spécifié. D'autres utilisateurs évitent judicieusement cette approche car elle peut être nuisible à long terme si le nombre de lignes relatives change considérablement. Un nombre minimum d'indices est souhaitable.

Si vous en faites plusieurs, un optimiseur commercial mérite peut-être un essai (ou un essai) et reste une bonne expérience d'apprentissage.

crokusek
la source
Oui, ça l'est. Il garantit que les lignes retournées par A sont limitées par la contrainte. Sinon, l'optimiseur peut se joindre en premier et appliquer la contrainte ultérieurement. Je m'en occupe quotidiennement.
crokusek
4
@crokusek Vous vous trompez. L'optimiseur de SQL-Server est assez bon pour savoir que les requêtes sont équivalentes (que la condition soit à WHERE ou à la clause ON) lorsqu'il s'agit d'une jointure INNER.
ypercubeᵀᴹ
6
Vous pourriez trouver la série de Paul White sur l'Optimiseur de requête utile.
Martin Smith
C'est une terrible habitude. Peut-être que ce sera pour ce cas particulier (où il y a une contrainte) mais je viens du pays de plusieurs développeurs empilant des conditions ET à la clause where. SQL Server ne les "déplace" pas systématiquement vers la jointure pour vous.
crokusek
Accord incorrect pour les jointures externes (et droites). Mais lorsqu'il n'y a que des expressions AND dans une clause where et que chaque terme correspond uniquement à une jointure interne spécifique, ce terme peut être déplacé en toute sécurité et en toute confiance vers l'emplacement "on" en tant qu'optimisation et meilleure pratique (imo). Qu'il s'agisse d'une "vraie" condition de jonction ou simplement d'une contrainte fixe est secondaire à un gain de performances important. Ce lien est pour un cas trivial. La vie réelle a des conditions multiples avec convert () et mathématiques, ce qui en fait de meilleurs candidats à partir desquels dériver les meilleures pratiques.
crokusek