Dividere la query SQL con molti join in più piccoli aiuta?


18

Dobbiamo fare alcuni rapporti ogni sera sul nostro SQL Server 2008 R2. Il calcolo dei rapporti richiede diverse ore. Al fine di ridurre il tempo precalcoliamo una tabella. Questa tabella è creata sulla base della JOINining 12 tabelle abbastanza grandi (decine di milioni di righe).

Il calcolo di questa tabella di aggregazione ha richiesto fino a pochi giorni fa circa 4 ore. Il nostro DBA ha diviso questo grande join in 3 join più piccoli (ognuno dei quali ha unito 4 tabelle). Il risultato temporaneo viene salvato in una tabella temporanea ogni volta, che viene utilizzato nel join successivo.

Il risultato del miglioramento DBA è che la tabella di aggregazione viene calcolata in 15 minuti. Mi chiedevo come fosse possibile. DBA mi ha detto che è perché il numero di dati che il server deve elaborare è inferiore. In altre parole, nel grande join originale il server deve lavorare con più dati rispetto ai join più piccoli sommati. Tuttavia, presumo che l'ottimizzatore si occuperebbe di farlo in modo efficiente con il big join originale, suddividendo i join da solo e inviando solo il numero di colonne necessarie ai join successivi.

L'altra cosa che ha fatto è che ha creato un indice su una delle tabelle temporanee. Tuttavia, ancora una volta penso che l'ottimizzatore creerà le tabelle hash appropriate se necessario e ottimizzerà del tutto il calcolo.

Ne ho parlato con il nostro DBA, ma lui stesso non era sicuro di cosa avesse comportato il miglioramento dei tempi di elaborazione. Ha appena detto che non darebbe la colpa al server in quanto può essere schiacciante calcolare tali dati di grandi dimensioni e che è possibile che l'ottimizzatore abbia difficoltà a prevedere il miglior piano di esecuzione .... Questo lo capisco, ma vorrei avere una risposta più precisa sul perché esattamente.

Quindi, le domande sono:

  1. Cosa potrebbe causare il grande miglioramento?

  2. È una procedura standard per dividere i join grandi in più piccoli?

  3. La quantità di dati che il server deve elaborare è davvero più piccola in caso di più piccoli join?

Ecco la query originale:

    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;

La nuova divisione si unisce dopo l'ottimo lavoro di 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;

3
Un avvertimento - WITH (NOLOCK) è male - può provocare la restituzione di dati errati. Suggerisco di provare WITH (ROWCOMMITTED).
TomTom,

1
@TomTom Intendevi READCOMMITTED? Non ho mai visto ROWCOMMITTED prima.
ypercubeᵀᴹ

4
WITH (NOLOCK) non è male. Non è solo il proiettile magico che la gente sembra pensare che sia. Come la maggior parte delle cose in SQL Server e lo sviluppo del software in generale ha il suo posto.
Zane,

2
Sì, ma dato che NOLOCK può generare avvisi nel registro e, cosa più importante, restituire DATI ERRATI, lo considero un male. È praticamente utilizzabile solo sulle tabelle GARANTITE per non cambiare la chiave primaria e le chiavi selezionate durante l'esecuzione della query. E sì, volevo dire READCOMMMITED, scusa.
TomTom,

Risposte:


11

1 Riduzione dello "spazio di ricerca", abbinata a migliori statistiche per i join intermedi / tardivi.

Ho avuto a che fare con join di 90 tabelle (design di mickey mouse) in cui il processore di query ha rifiutato persino di creare un piano. Suddividere un tale join in 10 subjoin di 9 tabelle ciascuno, ha drasticamente ridotto la complessità di ciascun join, che cresce esponenzialmente con ogni tabella aggiuntiva. Inoltre lo Strumento per ottimizzare le query ora li considera come 10 piani, spendendo (potenzialmente) più tempo in generale (Paul White potrebbe anche avere metriche!).

Le tabelle dei risultati intermedi ora avranno nuove statistiche proprie, unendo quindi molto meglio rispetto alle statistiche di un albero profondo che si inclinano presto e finiscono per diventare fantascienza poco dopo.

Inoltre, è possibile forzare prima i join più selettivi, riducendo i volumi di dati che si spostano sull'albero. Se riesci a stimare la selettività dei tuoi predicati molto meglio dell'ottimizzatore, perché non forzare l'ordine di join. Potrebbe valere la pena cercare "Bushy Plans".

2 A mio avviso, si dovrebbe considerare se l'efficienza e le prestazioni sono importanti

3 Non necessariamente, ma potrebbe essere se i join più selettivi vengono eseguiti in anticipo


3
+1 Grazie. Soprattutto per la descrizione della tua esperienza. È vero nel dire questo "Se riesci a stimare la selettività dei tuoi predicati molto meglio dell'ottimizzatore, perché non forzare l'ordine dei join".
Ondrej Peterka,

2
In realtà è una domanda molto valida. L'unione di 90 tavoli potrebbe essere forzata a produrre un piano semplicemente usando l'opzione 'Forza ordine'. Non importava che l'ordine fosse probabilmente casuale e non ottimale, solo ridurre lo spazio di ricerca era sufficiente per aiutare l'Ottimizzatore a creare un piano in un paio di secondi (senza il suggerimento che sarebbe scaduto dopo 20 secondi).
John Alan,

6
  1. L'ottimizzatore di SQLServer di solito fa un buon lavoro. Tuttavia, il suo obiettivo non è quello di generare il miglior piano possibile, ma di trovare il piano che è abbastanza buono in fretta. Per una query particolare con molti join potrebbe causare prestazioni molto scarse. Una buona indicazione di questo caso è una grande differenza tra il numero stimato e effettivo di righe nel piano di esecuzione effettivo. Inoltre, sono abbastanza sicuro che il piano di esecuzione per la query iniziale mostrerà molti 'loop nidificati join' che è più lento di 'merge join'. Quest'ultimo richiede che entrambi gli input siano ordinati usando la stessa chiave, che è costosa, e di solito l'ottimizzatore scarta tale opzione. Memorizzando i risultati nella tabella temporanea e aggiungendo gli indici corretti come hai fatto tu -messi indovinare- nella scelta dell'algoritmo migliore per ulteriori join (nota a margine - segui le migliori pratiche popolando prima la tabella temporanea, e aggiungendo indici dopo). Inoltre, SQLServer genera e mantiene statistiche per le tabelle temporanee che aiutano anche a scegliere l'indice corretto.
  2. Non posso dire che esiste uno standard sull'uso delle tabelle temporanee quando il numero di join è maggiore di un numero fisso, ma è sicuramente un'opzione che può migliorare le prestazioni. Non succede spesso, ma ho avuto problemi simili (e soluzioni simili) un paio di volte. In alternativa, puoi provare a capire tu stesso il miglior piano di esecuzione, archiviarlo e forzarlo a riutilizzarlo, ma ci vorrà un enorme quantità di tempo (nessun 100% garantito ti riuscirà). Un'altra nota a margine: nel caso in cui il set di risultati archiviato nella tabella temporanea sia relativamente piccolo (diciamo circa 10k record), la variabile della tabella funziona meglio della tabella temporanea.
  3. Odio dire "dipende", ma è probabilmente la mia risposta alla tua terza domanda. L'ottimizzatore deve fornire risultati rapidamente; non vuoi che passi ore a cercare di capire il piano migliore; ogni join aggiunge ulteriore lavoro e talvolta l'ottimizzatore "viene confuso".

3
+1 grazie per la conferma e spiegazione. Quello che hai scritto ha senso.
Ondrej Peterka,

4

Bene, vorrei iniziare dicendo che lavori su piccoli dati - 10ns di milioni non sono grandi. L'ultimo progetto DWH che avevo avuto 400 milioni di righe aggiunte alla tabella dei fatti. AL GIORNO. Conservazione per 5 anni.

Il problema è l'hardware, in parte. Dato che i join di grandi dimensioni possono utilizzare MOLTO spazio temporaneo e c'è solo tanta RAM, nel momento in cui si trabocca sul disco le cose diventano molto più lente. Pertanto, può avere senso dividere il lavoro in parti più piccole semplicemente perché mentre SQL vive in un mondo di insiemi e non si preoccupa delle dimensioni, il server su cui si esegue non è infinito. Sono abbastanza abituato a uscire da errori di spazio in un tempdb a 64 GB durante alcune operazioni.

Altrimenti, fintanto che gli staitsici sono in ordine, Query Optimizer non è sopraffatto. Non importa davvero quanto sia grande la tabella - funziona con statistiche che in realtà non crescono. CHE DETTO: Se hai davvero una tabella LARGE (doppio numero di miliardi di righe), potrebbero essere un po 'grossolani.

C'è anche una questione di blocco - a meno che non si programma che il join di grandi dimensioni può bloccare la tabella per ore. Al momento sto eseguendo operazioni di copia da 200 gb e le sto suddividendo in smllerparty da una chiave business (efficacemente in loop) che mantiene i blocchi molto più brevi.

Alla fine, lavoriamo con hardware limitato.


1
+1 grazie per la risposta. È utile affermare che dipende da HW. Abbiamo solo 32 GB di RAM, il che probabilmente non è sufficiente.
Ondrej Peterka,

2
Sono un po 'frustrato ogni volta che leggo risposte del genere: anche poche dozzine di milioni di righe creano il carico della CPU sul nostro server di database per ore. Forse il numero di dimensioni è alto, ma 30 dimensioni non sembrano un numero troppo grande. Penso che il numero molto elevato di righe che è possibile elaborare provenga da un modello semplice. Ancora peggio: tutti i dati si adattano alla RAM. E ci vogliono ancora ore.
flaschenpost,

1
30 dimensioni sono MOLTE - sei sicuro che il modello sia correttamente ottimizzato in una stella? Alcuni errori, ad esempio, che costano CPU - nella query OP utilizza GUID come chiavi primarie (uniqueidentifier). Li adoro anche io - come indice univoco, la chiave primaria è un campo ID, rende l'intero confronto più veloce e l'indice più nawwox (4 o 8 byte, non 18). Trucchi del genere consentono di risparmiare una tonnellata di CPU.
TomTom,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.