Precisamos fazer alguns relatórios todas as noites no nosso SQL Server 2008 R2. O cálculo dos relatórios leva várias horas. Para diminuir o tempo, pré-calculamos uma tabela. Esta tabela é criada com base em JOINining 12 tabelas bastante grandes (dezenas de milhões de linhas).
O cálculo dessa tabela de agregação levou até alguns dias atrás, cerca de 4 horas. Nosso DBA do que dividiu essa junção grande em 3 junções menores (cada uma juntando 4 tabelas). O resultado temporário é salvo em uma tabela temporária toda vez, usada na próxima associação.
O resultado do aprimoramento do DBA é que a tabela de agregação é calculada em 15 minutos. Eu me perguntava como isso é possível. O DBA me disse que é porque o número de dados que o servidor deve processar é menor. Em outras palavras, que na grande junção original, o servidor precisa trabalhar com mais dados do que nas junções menores somadas. No entanto, eu presumiria que o otimizador cuidaria de fazê-lo eficientemente com a grande junção original, dividindo as junções por conta própria e enviando apenas o número de colunas necessárias para as próximas junções.
A outra coisa que ele fez foi criar um índice em uma das tabelas temporárias. No entanto, mais uma vez, eu pensaria que o otimizador criará as tabelas de hash apropriadas, se necessário, e otimizará totalmente o cálculo.
Conversei sobre isso com nosso DBA, mas ele próprio não tinha certeza do que causava a melhoria no tempo de processamento. Ele acabou de mencionar que não culparia o servidor, pois pode ser esmagador calcular tais dados grandes e que é possível que o otimizador tenha dificuldade em prever o melhor plano de execução .... Isso eu entendo, mas gostaria de ter uma resposta mais definitiva sobre exatamente o porquê.
Então, as perguntas são:
O que poderia causar a grande melhoria?
É um procedimento padrão dividir grandes junções em menores?
A quantidade de dados que o servidor precisa processar é realmente menor no caso de várias junções menores?
Aqui está a consulta original:
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;
A nova divisão se junta após um ótimo trabalho do 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;
fonte
READCOMMITTED
? Eu nunca vi ROWCOMMITTED antes.Respostas:
1 Redução do 'espaço de pesquisa', juntamente com melhores estatísticas para as junções intermediárias / tardias.
Eu tive que lidar com junções de 90 mesas (design do mouse mickey), onde o Query Processor se recusou a criar um plano. A divisão dessa junção em 10 sub-junções de 9 tabelas cada reduziu drasticamente a complexidade de cada junção, que cresce exponencialmente com cada tabela adicional. Além disso, o Query Optimizer agora os trata como 10 planos, gastando (potencialmente) mais tempo no geral (Paul White pode até ter métricas!).
As tabelas de resultados intermediários agora terão novas estatísticas próprias, juntando-se muito melhor em comparação com as estatísticas de uma árvore profunda que se inclina cedo e acaba como Ficção Científica logo depois.
Além disso, você pode forçar as junções mais seletivas primeiro, reduzindo os volumes de dados subindo na árvore. Se você pode estimar a seletividade de seus predicados muito melhor que o Otimizador, por que não forçar a ordem de junção. Pode valer a pena procurar por "Bushy Plans".
2 Na minha opinião, deve ser considerado se a eficiência e o desempenho são importantes
3 Não necessariamente, mas poderia ser se as junções mais seletivas fossem executadas desde o início
fonte
fonte
Bem, deixe-me começar dizendo que você trabalha com dados pequenos - 10ns de milhões não são grandes. O último projeto DWH que tive 400 milhões de linhas adicionadas à tabela de fatos. POR DIA. Armazenamento por 5 anos.
O problema é hardware, parcialmente. Como junções grandes podem usar muito espaço temporário e há apenas muita RAM, no momento em que você transborda para o disco, as coisas ficam muito mais lentas. Como tal, pode fazer sentido dividir o trabalho em partes menores simplesmente porque, enquanto o SQL vive em um mundo de conjuntos e não se importa com o tamanho, o servidor no qual você executa não é infinito. Estou bastante acostumado a obter erros de falta de espaço em um tempdb de 64GB durante algumas operações.
Caso contrário, enquanto os staitsics estiverem em ordem, o otimizador de consultas não ficará sobrecarregado. Realmente não se importa com o tamanho da tabela - ela funciona com estatísticas que realmente não crescem. O QUE DISSE: Se você realmente tem uma tabela GRANDE (número de bilhões de dígitos com dois dígitos), elas podem ser um pouco grosseiras.
Há também uma questão de bloqueio - a menos que você programe muito bem que a junção grande possa bloquear a tabela por horas. Atualmente, estou executando operações de cópia de 200 gb e as estou dividindo em partes por uma chave comercial (efetivamente em loop) que mantém os bloqueios muito mais curtos.
No final, trabalhamos com hardware limitado.
fonte