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:
Qu'est-ce qui pourrait éventuellement provoquer la grande amélioration?
Est-ce une procédure standard pour diviser les grandes jointures en plus petites?
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;
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
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é.