Das Aufteilen einer SQL-Abfrage mit vielen Verknüpfungen in kleinere Verknüpfungen hilft?

18

Auf unserem SQL Server 2008 R2 müssen wir jede Nacht einige Berichte erstellen. Die Berechnung der Berichte dauert mehrere Stunden. Um die Zeit zu verkürzen, berechnen wir eine Tabelle vor. Diese Tabelle basiert auf der Verknüpfung von 12 recht großen (zig Millionen Zeilen) Tabellen.

Die Berechnung dieser Aggregationstabelle dauerte bis vor wenigen Tagen ca. 4 Stunden. Unser Datenbankadministrator hat diesen großen Join in 3 kleinere Joins aufgeteilt (wobei jeder 4 Tabellen verknüpft). Das temporäre Ergebnis wird jedes Mal in einer temporären Tabelle gespeichert, die beim nächsten Join verwendet wird.

Das Ergebnis der DBA-Erweiterung ist, dass die Aggregationstabelle in 15 Minuten berechnet wird. Ich habe mich gefragt, wie das möglich ist. Der DBA hat mir mitgeteilt, dass die Anzahl der Daten, die der Server verarbeiten muss, geringer ist. Mit anderen Worten, dass der Server im großen Original-Join mit mehr Daten arbeiten muss als in summierten kleineren Joins. Ich würde jedoch davon ausgehen, dass das Optimierungsprogramm für eine effiziente Ausführung mit dem ursprünglichen großen Join sorgen würde, indem die Joins selbst aufgeteilt und nur die Anzahl der für die nächsten Joins erforderlichen Spalten gesendet würden.

Das andere, was er getan hat, ist, dass er einen Index für eine der temporären Tabellen erstellt hat. Ich würde jedoch noch einmal davon ausgehen, dass der Optimierer bei Bedarf die entsprechenden Hash-Tabellen erstellt und die Berechnung insgesamt besser optimiert.

Ich habe mit unserem DBA darüber gesprochen, aber er selbst war sich nicht sicher, was die Verbesserung der Verarbeitungszeit zur Folge hatte. Er hat gerade erwähnt, dass er dem Server keine Vorwürfe machen würde, da die Berechnung von so großen Datenmengen sehr aufwändig sein kann und es für den Optimierer möglicherweise schwierig ist, den besten Ausführungsplan vorherzusagen. Das verstehe ich, aber ich hätte gerne eine klarere Antwort darauf, warum.

Die Fragen sind also:

  1. Was könnte möglicherweise die große Verbesserung verursachen?

  2. Ist es eine Standardprozedur, große Joins in kleinere zu teilen?

  3. Ist die Datenmenge, die der Server verarbeiten muss, bei mehreren kleineren Joins wirklich kleiner?

Hier ist die ursprüngliche Abfrage:

    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;

Der neue Splitt kommt nach der tollen Arbeit von 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;
Ondrej Peterka
quelle
3
Ein Wort der Warnung - WITH (NOLOCK) ist böse - kann dazu führen, dass schlechte Daten zurückkommen. Ich schlage vor, es mit WITH (ROWCOMMITTED) zu versuchen.
TomTom
1
@TomTom Meinten Sie READCOMMITTED? Ich habe ROWCOMMITTED noch nie gesehen.
Ypercubeᵀᴹ
4
WITH (NOLOCK) ist nicht böse. Es ist einfach nicht die magische Kugel, die die Leute zu glauben scheinen. Wie die meisten Dinge in der SQL Server- und Softwareentwicklung im Allgemeinen hat es seinen Platz.
Zane
2
Ja, aber da NOLOCK möglicherweise Warnungen im Protokoll ausgibt und - was noch wichtiger ist - falsche Daten zurückgibt, halte ich das für böse. Es ist so gut wie nur für Tabellen verwendbar, die GARANTIERT sind, den Primärschlüssel und die ausgewählten Schlüssel während der Ausführung der Abfrage nicht zu ändern. Und ja, ich meand READCOMMMITED, sorry.
TomTom

Antworten:

11

1 Reduzierung des 'Suchraums', gepaart mit einer besseren Statistik für die Intermediate / Late Joins.

Ich musste mich mit Joins mit 90 Tabellen (Mickey-Mouse-Design) befassen, bei denen der Abfrageprozessor sich weigerte, überhaupt einen Plan zu erstellen. Durch das Aufteilen eines solchen Joins in 10 Unter-Joins mit jeweils 9 Tabellen wurde die Komplexität jedes Joins drastisch verringert, die mit jeder weiteren Tabelle exponentiell zunimmt. Außerdem behandelt das Abfrageoptimierungsprogramm sie jetzt als 10 Pläne, wodurch insgesamt (möglicherweise) mehr Zeit aufgewendet wird (Paul White verfügt möglicherweise sogar über Messdaten!).

Die Zwischenergebnistabellen verfügen nun über eigene neue Statistiken, sodass sie sich viel besser mit den Statistiken eines tiefen Baums verbinden, der früh verzerrt wird und bald darauf als Science-Fiction-Tabelle endet.

Außerdem können Sie die selektivsten Verknüpfungen zuerst erzwingen, indem Sie die Datenvolumina verringern, die sich in der Baumstruktur nach oben bewegen. Wenn Sie die Selektivität Ihrer Prädikate besser einschätzen können als mit dem Optimierer, erzwingen Sie die Verknüpfungsreihenfolge. Vielleicht lohnt es sich, nach "Bushy Plans" zu suchen.

2 Es ist meines Erachtens zu prüfen, ob Effizienz und Leistung wichtig sind

3 Nicht unbedingt, aber es könnte sein, dass die selektivsten Verknüpfungen früh ausgeführt werden

John Alan
quelle
3
+1 Danke. Vor allem für die Beschreibung Ihrer Erfahrung. Sehr zutreffend, wenn Sie sagen: "Wenn Sie die Selektivität Ihrer Prädikate viel besser einschätzen können als das Optimierungsprogramm, warum nicht die Verknüpfungsreihenfolge erzwingen."
Ondrej Peterka
2
Das ist eine sehr berechtigte Frage. Der 90-Tabellen-Join kann mithilfe der Option "Reihenfolge erzwingen" gezwungen werden, einen Plan zu erstellen. Es spielte keine Rolle, dass die Reihenfolge wahrscheinlich zufällig und suboptimal war. Nur die Reduzierung des Suchbereichs half dem Optimierer, innerhalb weniger Sekunden einen Plan zu erstellen (ohne den Hinweis, dass das Zeitlimit nach 20 Sekunden abläuft).
John Alan
6
  1. Der SQL Server-Optimierer leistet normalerweise gute Arbeit. Ziel ist es jedoch nicht, den bestmöglichen Plan zu generieren, sondern den Plan zu finden, der schnell gut genug ist. Bei einer bestimmten Abfrage mit vielen Verknüpfungen kann dies zu einer sehr schlechten Leistung führen. Ein guter Hinweis auf einen solchen Fall ist ein großer Unterschied zwischen der geschätzten und der tatsächlichen Anzahl von Zeilen im tatsächlichen Ausführungsplan. Außerdem bin ich mir ziemlich sicher, dass der Ausführungsplan für die erste Abfrage viele 'Nested Loops Join' anzeigen wird, was langsamer ist als 'Merge Join'. Letzteres erfordert, dass beide Eingaben mit demselben Schlüssel sortiert werden, was teuer ist, und der Optimierer verwirft normalerweise eine solche Option. Speichern der Ergebnisse in einer temporären Tabelle und Hinzufügen geeigneter Indizes, wie Sie es erraten haben, um einen besseren Algorithmus für weitere Verknüpfungen zu wählen (Randnotiz: Sie befolgen die bewährten Methoden, indem Sie zuerst die temporäre Tabelle ausfüllen. und Hinzufügen von Indizes nach). Darüber hinaus generiert und speichert SQLServer Statistiken für temporäre Tabellen, die auch bei der Auswahl des richtigen Index hilfreich sind.
  2. Ich kann nicht sagen, dass es einen Standard für die Verwendung temporärer Tabellen gibt, wenn die Anzahl der Joins größer ist als eine feste Anzahl, aber dies ist definitiv eine Option, die die Leistung verbessern kann. Das passiert nicht oft, aber ich hatte einige Male ähnliche Probleme (und ähnliche Lösungen). Alternativ können Sie versuchen, den besten Ausführungsplan selbst zu finden, ihn zu speichern und zu erzwingen, die Wiederverwendung wird jedoch enorm viel Zeit in Anspruch nehmen (keine 100% ige Garantie für den Erfolg). Eine weitere Randnotiz: Wenn die in der temporären Tabelle gespeicherte Ergebnismenge relativ klein ist (etwa 10.000 Datensätze), ist die Tabellenvariable besser als die temporäre Tabelle.
  3. Ich hasse es, zu sagen, dass es darauf ankommt, aber es ist wahrscheinlich meine Antwort auf Ihre dritte Frage. Der Optimierer muss schnell Ergebnisse liefern. Sie möchten nicht, dass es stundenlang versucht, den besten Plan zu finden. Jeder Join fügt zusätzliche Arbeit hinzu und manchmal wird der Optimierer "verwirrt".
a1ex07
quelle
3
+1 Danke für die Bestätigung und Erklärung. Was Sie geschrieben haben, macht Sinn.
Ondrej Peterka
4

Lassen Sie mich zunächst sagen, dass Sie an kleinen Daten arbeiten - 10 ns von Millionen sind nicht groß. Das letzte DWH-Projekt, für das ich 400 Millionen Zeilen in die Faktentabelle aufgenommen hatte. PRO TAG. Lagerung für 5 Jahre.

Das Problem ist teilweise die Hardware. Da große Joins möglicherweise eine Menge temporären Speicherplatz belegen und nur so viel RAM zur Verfügung steht, werden die Dinge in dem Moment, in dem Sie überlaufen, viel langsamer. Daher kann es sinnvoll sein, die Arbeit in kleinere Teile zu unterteilen, da SQL zwar in einer Welt von Mengen lebt und sich nicht um die Größe kümmert, der Server, auf dem Sie ausgeführt werden, jedoch nicht unendlich ist. Ich bin es gewohnt, bei manchen Vorgängen Speicherfehler in einer 64-GB-Tempdb zu vermeiden.

Andernfalls ist der Abfrageoptimierer nicht überfordert, solange die Zustände in Ordnung sind. Es ist egal, wie groß die Tabelle ist - es funktioniert mit Statistiken, die nicht wirklich wachsen. DAS GESAGT: Wenn Sie wirklich eine GROSSE Tabelle (zweistellige Milliarden-Zeilenanzahl) haben, können sie etwas grob sein.

Es gibt auch eine Frage des Sperrens - es sei denn, Sie programmieren, dass der große Join die Tabelle für Stunden sperren kann. Momentan führe ich 200-GB-Kopiervorgänge durch und teile sie durch einen Geschäftsschlüssel (der sich tatsächlich in einer Schleife befindet) in kleine Gruppen auf, wodurch die Sperren viel kürzer bleiben.

Am Ende arbeiten wir mit begrenzter Hardware.

TomTom
quelle
1
+1 danke für deine antwort. Es ist sinnvoll zu sagen, dass dies von der Hardware abhängt. Wir haben nur 32 GB RAM, was wahrscheinlich nicht ausreicht.
Ondrej Peterka
2
Ich bin jedes Mal ein bisschen frustriert, wenn ich Antworten wie diese lese - sogar ein paar Dutzend Millionen Zeilen verursachen stundenlang CPU-Last auf unserem Datenbankserver. Vielleicht ist die Anzahl der Dimensionen hoch, aber 30 Dimensionen scheinen keine zu große Zahl zu sein. Ich denke, die sehr hohe Anzahl von Zeilen, die Sie verarbeiten können, stammt von einem einfachen Modell. Schlimmer noch: Die gesamten Daten passen in den Arbeitsspeicher. Und dauert immer noch Stunden.
Flaschenpost
1
30 Dimensions ist eine Menge - sind Sie sicher, dass das Modell richtig in einen Stern optimiert ist? Einige Fehler zum Beispiel, die die CPU kosten - bei der OP-Abfrage werden GUIDs als Primärschlüssel verwendet (Uniqueidentifier). Ich liebe sie auch - als eindeutiger Index ist der Primärschlüssel ein ID-Feld, wodurch der gesamte Vergleich schneller und der Index nawwox (4 oder 8 Bytes, nicht 18). Solche Tricks sparen eine Menge CPU.
TomTom