web-dev-qa-db-fra.com

La division de la requête SQL avec de nombreuses jointures en plus petites aide?

Nous devons faire des rapports tous les soirs sur notre SQL Server 2008 R2. Le calcul des rapports prend plusieurs heures. Afin de raccourcir le temps, nous précalculons une table. Ce tableau est créé sur la base de JOINining 12 tables assez grandes (des dizaines de millions de lignes).

Le calcul de cette table d'agrégation a pris jusqu'à il y a quelques jours environ 4 heures. Notre DBA a ensuite divisé cette grande jointure en 3 jointures plus petites (chacune joignant 4 tables). Le résultat temporaire est enregistré à chaque fois dans une table temporaire, qui est utilisée dans la jointure suivante.

Le résultat de l'amélioration DBA est que la table d'agrégation est calculée en 15 minutes. Je me demandais comment c'était possible. DBA m'a dit que c'est parce que le nombre de données que le serveur doit traiter est plus petit. En d'autres termes, dans la grande jointure d'origine, le serveur doit travailler avec plus de données que dans les petites jointures additionnées. Cependant, je suppose que l'optimiseur se chargera de le faire efficacement avec la grande jointure d'origine, en divisant les jointures par elle-même et en envoyant uniquement le nombre de colonnes nécessaires aux jointures suivantes.

Il a également créé un index sur l'une des tables temporaires. Cependant, une fois de plus, je pense que l'optimiseur créera les tables de hachage appropriées si nécessaire et optimisera mieux le calcul.

J'en ai parlé avec notre administrateur de base de données, mais lui-même n'était pas sûr de ce qui entraînait l'amélioration du temps de traitement. Il vient de mentionner qu'il ne blâmerait pas le serveur car il peut être accablant de calculer de telles données volumineuses et qu'il est possible que l'optimiseur ait du mal à prédire le meilleur plan d'exécution .... Je comprends cela, mais j'aimerais avoir une réponse plus précise quant à la raison exacte.

Donc, les questions sont:

  1. Qu'est-ce qui pourrait éventuellement provoquer la grande amélioration?

  2. Est-ce une procédure standard pour diviser les grandes jointures en plus petites?

  3. La quantité de données que le serveur doit traiter est-elle vraiment plus petite en cas de plusieurs jointures plus petites?

Voici la requête d'origine:

    Insert Into FinalResult_Base
SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TSK.CategoryId
    ,TT.[TestletId]
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) 
    ,TQ.[QuestionId]
    ,TS.StudentId
    ,TS.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] 
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,TS.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,TQ.[Position]  
    ,RA.SpecialNeeds        
    ,[Version] = 1 
    ,TestAdaptationId = TA.Id
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,AnswerType = TT.TestletAnswerTypeId
FROM 
    [TestQuestion] TQ WITH (NOLOCK)
    Join [TestTask] TT WITH (NOLOCK)            On TT.Guid = TQ.TestTaskId
    Join [Question] Q WITH (NOLOCK)         On TQ.QuestionId =  Q.QuestionId
    Join [Testlet] TL WITH (NOLOCK)         On TT.TestletId  = TL.Guid 
    Join [Test]     T WITH (NOLOCK)         On TL.TestId     =  T.Guid
    Join [TestSet] TS WITH (NOLOCK)         On T.TestSetId   = TS.Guid 
    Join [RoleAssignment] RA WITH (NOLOCK)  On TS.StudentId  = RA.PersonId And RA.RoleId = 1
    Join [Task] TSK WITH (NOLOCK)       On TSK.TaskId = TT.TaskId
    Join [Category] C WITH (NOLOCK)     On C.CategoryId = TSK.CategoryId
    Join [TimeWindow] TW WITH (NOLOCK)      On TW.Id = TS.TimeWindowId 
    Join [TestAdaptation] TA WITH (NOLOCK)  On TA.Id = TW.TestAdaptationId
    Join [TestCampaign] TC WITH (NOLOCK)        On TC.TestCampaignId = TA.TestCampaignId 
WHERE
    T.TestTypeId = 1    -- eliminuji ankety 
    And t.ProcessedOn is not null -- ne vsechny, jen dokoncene
    And TL.ShownOn is not null
    And TS.Redizo not in (999999999, 111111119)
END;

Les nouvelles jointures fractionnées après un excellent travail DBA:

    SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) -- prevod na A5, B4, B5 ...
    ,TS.StudentId
    ,TS.ClassId
    ,TS.Redizo
    ,[Version] = 1 -- ? 
    ,TestAdaptationId = TA.Id
    ,TL.Guid AS TLGuid
    ,TS.TimeWindowId
INTO
    [#FinalResult_Base_1]
FROM 
    [TestSet] [TS] WITH (NOLOCK)
    JOIN [Test] [T] WITH (NOLOCK) 
        ON [T].[TestSetId] = [TS].[Guid] AND [TS].[Redizo] NOT IN (999999999, 111111119) AND [T].[TestTypeId] = 1 AND [T].[ProcessedOn] IS NOT NULL
    JOIN [Testlet] [TL] WITH (NOLOCK)
        ON [TL].[TestId] = [T].[Guid] AND [TL].[ShownOn] IS NOT NULL
    JOIN [TimeWindow] [TW] WITH (NOLOCK)
        ON [TW].[Id] = [TS].[TimeWindowId] AND [TW].[IsActive] = 1
    JOIN [TestAdaptation] [TA] WITH (NOLOCK)
        ON [TA].[Id] = [TW].[TestAdaptationId] AND [TA].[IsActive] = 1
    JOIN [TestCampaign] [TC] WITH (NOLOCK)
        ON [TC].[TestCampaignId] = [TA].[TestCampaignId] AND [TC].[IsActive] = 1
    JOIN [TestCampaignContainer] [TCC] WITH (NOLOCK)
        ON [TCC].[TestCampaignContainerId] = [TC].[TestCampaignContainerId] AND [TCC].[IsActive] = 1
    ;

 SELECT       
    FR1.TestCampaignContainerId,
    FR1.TestCampaignCategoryId,
    FR1.Grade,
    FR1.TestCampaignId,    
    FR1.TestSetId
    ,FR1.TestId
    ,TSK.CategoryId AS [TaskCategoryId]
    ,TT.[TestletId]
    ,FR1.SectionNo
    ,FR1.Difficulty
    ,TestletName = Char(65+FR1.SectionNo) + CONVERT(varchar(4),6 - FR1.Difficulty) -- prevod na A5, B4, B5 ...
    ,FR1.StudentId
    ,FR1.ClassId
    ,FR1.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,[Version] = 1 -- ? 
    ,FR1.TestAdaptationId
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,AnswerType = TT.TestletAnswerTypeId
    ,TT.Guid AS TTGuid

INTO
    [#FinalResult_Base_2]
FROM 
    #FinalResult_Base_1 FR1
    JOIN [TestTask] [TT] WITH (NOLOCK)
        ON [TT].[TestletId] = [FR1].[TLGuid] 
    JOIN [Task] [TSK] WITH (NOLOCK)
        ON [TSK].[TaskId] = [TT].[TaskId] AND [TSK].[IsActive] = 1
    JOIN [Category] [C] WITH (NOLOCK)
        ON [C].[CategoryId] = [TSK].[CategoryId]AND [C].[IsActive] = 1
    ;    

DROP TABLE [#FinalResult_Base_1]

CREATE NONCLUSTERED INDEX [#IX_FR_Student_Class]
ON [dbo].[#FinalResult_Base_2] ([StudentId],[ClassId])
INCLUDE ([TTGuid])

SELECT       
    FR2.TestCampaignContainerId,
    FR2.TestCampaignCategoryId,
    FR2.Grade,
    FR2.TestCampaignId,    
    FR2.TestSetId
    ,FR2.TestId
    ,FR2.[TaskCategoryId]
    ,FR2.[TestletId]
    ,FR2.SectionNo
    ,FR2.Difficulty
    ,FR2.TestletName
    ,TQ.[QuestionId]
    ,FR2.StudentId
    ,FR2.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] -- 1+ good, 0 wrong, null no answer
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 -- cookie
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,FR2.Redizo
    ,FR2.ViewCount
    ,FR2.SpentTime
    ,TQ.[Position] AS [QuestionPosition]  
    ,RA.SpecialNeeds -- identifikace SVP        
    ,[Version] = 1 -- ? 
    ,FR2.TestAdaptationId
    ,FR2.TaskId
    ,FR2.TaskPosition
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,FR2.AnswerType
INTO
    [#FinalResult_Base]
FROM 
    [#FinalResult_Base_2] FR2
    JOIN [TestQuestion] [TQ] WITH (NOLOCK)
        ON [TQ].[TestTaskId] = [FR2].[TTGuid]
    JOIN [Question] [Q] WITH (NOLOCK)
        ON [Q].[QuestionId] = [TQ].[QuestionId] AND [Q].[IsActive] = 1

    JOIN [RoleAssignment] [RA] WITH (NOLOCK)
        ON [RA].[PersonId] = [FR2].[StudentId]
        AND [RA].[ClassId] = [FR2].[ClassId] AND [RA].[IsActive] = 1 AND [RA].[RoleId] = 1

    drop table #FinalResult_Base_2;

    truncate table [dbo].[FinalResult_Base];
    insert into [dbo].[FinalResult_Base] select * from #FinalResult_Base;

    drop table #FinalResult_Base;
19
Ondrej Peterka

1 Réduction de "l'espace de recherche", couplée à de meilleures statistiques pour les jointures intermédiaires/tardives.

J'ai dû faire face à des jointures de 90 tables (conception de mickey mouse) où le processeur de requêtes a même refusé de créer un plan. Briser une telle jointure en 10 sous-jointures de 9 tables chacune, a considérablement réduit la complexité de chaque jointure, qui croît de façon exponentielle avec chaque table supplémentaire. De plus, l'Optimiseur de requête les traite désormais comme 10 plans, passant (potentiellement) plus de temps dans l'ensemble (Paul White peut même avoir des mesures!).

Les tableaux de résultats intermédiaires auront désormais leurs propres statistiques, rejoignant ainsi beaucoup mieux que les statistiques d'un arbre profond qui se faussent tôt et finissent par devenir Science Fiction peu de temps après.

De plus, vous pouvez forcer les jointures les plus sélectives en premier, en réduisant les volumes de données remontant dans l'arborescence. Si vous pouvez estimer la sélectivité de vos prédicats bien mieux que l'Optimiseur, pourquoi ne pas forcer l'ordre de jointure. Peut-être vaut-il la peine de rechercher des "plans broussailleux".

2 Il devrait être considéré à mon avis, si l'efficacité et les performances sont importantes

Pas nécessairement, mais cela pourrait l'être si les jointures les plus sélectives sont exécutées tôt

12
John Alan
  1. L'optimiseur SQLServer fait généralement du bon travail. Cependant, son objectif n'est pas de générer le meilleur plan possible, mais de trouver rapidement le plan qui est assez bon. Pour une requête particulière avec plusieurs jointures, les performances peuvent être très faibles. Une bonne indication d'un tel cas est une grande différence entre le nombre estimé et réel de lignes dans le plan d'exécution réel. En outre, je suis presque sûr que le plan d'exécution de la requête initiale affichera de nombreuses "jointures de boucles imbriquées", ce qui est plus lent que "fusionner la jointure". Ce dernier nécessite que les deux entrées soient triées à l'aide de la même clé, ce qui est coûteux, et l'optimiseur rejette généralement une telle option. Stocker les résultats dans une table temporaire et ajouter des index appropriés comme vous l'avez fait, mon choix, en choisissant un meilleur algorithme pour d'autres jointures (note latérale - vous suivez les meilleures pratiques en remplissant la table temporaire en premier et en ajoutant des index par la suite). De plus, SQLServer génère et conserve des statistiques pour les tables temporaires, ce qui permet également de choisir l'index approprié.
  2. Je ne peux pas dire qu'il existe une norme sur l'utilisation de tables temporaires lorsque le nombre de jointures est supérieur à un certain nombre fixe, mais c'est certainement une option qui peut améliorer les performances. Cela n'arrive pas souvent, mais j'ai eu plusieurs fois des problèmes similaires (et une solution similaire). Alternativement, vous pouvez essayer de trouver vous-même le meilleur plan d'exécution, le stocker et le forcer à le réutiliser, mais cela prendra énormément de temps (pas de garantie à 100% que vous réussirez). Une autre note latérale - dans le cas où l'ensemble de résultats stocké dans une table temporaire est relativement petit (disons environ 10k enregistrements), la variable de table fonctionne mieux que la table temporaire.
  3. Je déteste dire "ça dépend", mais c'est probablement ma réponse à votre troisième question. L'optimiseur doit donner des résultats rapidement; vous ne voulez pas qu'il passe des heures à essayer de trouver le meilleur plan; chaque jointure ajoute du travail supplémentaire, et parfois l'optimiseur "devient confus".
7
a1ex07

Eh bien, permettez-moi de commencer par dire que vous travaillez sur de petites données - 10 millions de millions ne sont pas importants. Le dernier projet DWH j'avais 400 millions de lignes ajoutées à la table de faits. PAR JOUR. Stockage pendant 5 ans.

Le problème vient du matériel, en partie. Comme les grandes jointures peuvent utiliser BEAUCOUP d'espace temporaire et qu'il n'y a que peu de RAM, au moment où vous débordez dans le disque, les choses deviennent beaucoup plus lentes. En tant que tel, il peut être judicieux de diviser le travail en parties plus petites simplement parce que, tandis que SQL vit dans un monde d'ensembles et ne se soucie pas de la taille, le serveur sur lequel vous exécutez n'est pas infini. J'ai l'habitude de sortir des erreurs d'espace dans un tempdb de 64 Go pendant certaines opérations.

Sinon, tant que les statistiques sont en ordre, l'optimiseur de requête n'est pas dépassé. Il ne se soucie pas vraiment de la taille du tableau - il fonctionne selon des statistiques qui ne se développent vraiment pas. CELA A DIT: Si vous avez vraiment une grande table (nombre de milliards de chiffres à deux chiffres), alors elles peuvent être un peu grossières.

Il y a aussi une question de verrouillage - à moins que vous ne programmiez si bien que la grande jointure puisse verrouiller la table pendant des heures. Je fais des opérations de copie de 200 Go en ce moment, et je les divise en plus petite partie par une clé d'entreprise (en boucle efficace) qui maintient les verrous beaucoup plus courts.

À la fin, nous travaillons avec un matériel limité.

5
TomTom