So optimieren Sie die T-SQL-Abfrage mithilfe des Ausführungsplans

15

Ich habe eine SQL-Abfrage, die ich in den letzten zwei Tagen versucht habe, mithilfe von Trial-and-Error und des Ausführungsplans zu optimieren, aber ohne Erfolg. Bitte verzeihen Sie mir, aber ich werde den gesamten Ausführungsplan hier veröffentlichen. Ich habe mich bemüht, die Tabellen- und Spaltennamen im Abfrage- und Ausführungsplan sowohl aus Gründen der Kürze als auch zum Schutz der IP-Adresse meines Unternehmens generisch zu gestalten. Der Ausführungsplan kann mit dem SQL Sentry Plan Explorer geöffnet werden .

Ich habe viel mit T-SQL gearbeitet, aber die Verwendung von Ausführungsplänen zur Optimierung meiner Abfrage ist für mich ein neuer Bereich, und ich habe wirklich versucht, zu verstehen, wie das geht. Wenn mir jemand dabei helfen und erklären könnte, wie dieser Ausführungsplan entschlüsselt werden kann, um in der Abfrage Möglichkeiten zu finden, ihn zu optimieren, wäre ich auf ewig dankbar. Ich muss noch viele weitere Abfragen optimieren - ich brauche nur ein Sprungbrett, um mir bei diesem ersten zu helfen.

Dies ist die Abfrage:

DECLARE @Param0 DATETIME     = '2013-07-29';
DECLARE @Param1 INT          = CONVERT(INT, CONVERT(VARCHAR, @Param0, 112))
DECLARE @Param2 VARCHAR(50)  = 'ABC';
DECLARE @Param3 VARCHAR(100) = 'DEF';
DECLARE @Param4 VARCHAR(50)  = 'XYZ';
DECLARE @Param5 VARCHAR(100) = NULL;
DECLARE @Param6 VARCHAR(50)  = 'Text3';

SET NOCOUNT ON

DECLARE @MyTableVar TABLE
(
    B_Var1_PK int,
    Job_Var1 varchar(512),
    Job_Var2 varchar(50)
)

INSERT INTO @MyTableVar (B_Var1_PK, Job_Var1, Job_Var2) 
SELECT B_Var1_PK, Job_Var1, Job_Var2 FROM [fn_GetJobs] (@Param1, @Param2, @Param3, @Param4, @Param6);

CREATE TABLE #TempTable
(
    TTVar1_PK INT PRIMARY KEY,
    TTVar2_LK VARCHAR(100),
    TTVar3_LK VARCHAR(50),
    TTVar4_LK INT,
    TTVar5 VARCHAR(20)
);

INSERT INTO #TempTable
SELECT DISTINCT
    T.T1_PK,
    T.T1_Var1_LK,
    T.T1_Var2_LK,
    MAX(T.T1_Var3_LK),
    T.T1_Var4_LK
FROM
    MyTable1 T
    INNER JOIN feeds.MyTable2 A ON A.T2_Var1 = T.T1_Var4_LK
    INNER JOIN @MyTableVar B ON B.Job_Var2 = A.T2_Var2 AND B.Job_Var1 = A.T2_Var3
GROUP BY T.T1_PK, T.T1_Var1_LK, T.T1_Var2_LK, T.T1_Var4_LK

-- This is the slow statement...
SELECT 
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK,
    C.C_Var1_PK,
    SUM(CONVERT(DECIMAL(18,4), A.A_Var1) + CONVERT(DECIMAL(18,4), A.A_Var2))
FROM #TempTable T
    INNER JOIN TableA (NOLOCK) A ON A.A_Var4_FK_LK  = T.TTVar1_PK
    INNER JOIN @MyTableVar     B ON B.B_Var1_PK     = A.Job
    INNER JOIN TableC (NOLOCK) C ON C.C_Var2_PK     = A.A_Var5_FK_LK
    INNER JOIN TableD (NOLOCK) D ON D.D_Var1_PK     = A.A_Var6_FK_LK
    INNER JOIN TableE (NOLOCK) E ON E.E_Var1_PK     = A.A_Var7_FK_LK  
    LEFT OUTER JOIN feeds.TableF (NOLOCK) F ON F.F_Var1 = T.TTVar5
WHERE A.A_Var8_FK_LK = @Param1
GROUP BY
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK 
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK, 
    C.C_Var1_PK


IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END
IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END

Was ich festgestellt habe, ist, dass die dritte Aussage (die als langsam bezeichnet wird) den Teil darstellt, der die meiste Zeit in Anspruch nimmt. Die beiden vorherigen Aussagen kehren fast sofort zurück.

Der Ausführungsplan steht unter diesem Link als XML zur Verfügung .

Klicken Sie besser mit der rechten Maustaste und speichern Sie sie und öffnen Sie sie in SQL Sentry Plan Explorer oder einer anderen Anzeigesoftware, anstatt sie in Ihrem Browser zu öffnen.

Wenn Sie weitere Informationen zu den Tabellen oder Daten von mir benötigen, zögern Sie bitte nicht, nachzufragen.

Neo
quelle
2
Ihre Statistiken sind weit weg. Wann haben Sie zum letzten Mal Indizes oder aktualisierte Statistiken defragmentiert? Außerdem würde ich versuchen, eine temporäre Tabelle anstelle der Tabellenvariablen @MyTableVar zu verwenden, da der Optimierer Statistiken für Tabellenvariablen nicht wirklich verwenden kann.
Adam Haines
Danke für deine Antwort Adam. Das Ändern von @MyTableVar in eine temporäre Tabelle hat keine Auswirkung, sondern nur eine geringe Anzahl von Zeilen (wie aus dem Ausführungsplan ersichtlich). Was im Ausführungsplan zeigt, dass meine Statistiken weit davon entfernt sind? Gibt es an, welche Indizes reorganisiert oder neu erstellt werden sollen und welche Tabellen Statistiken sollten aktualisiert werden?
Neo
3
Der Hash-Join unten rechts enthält schätzungsweise 24.000 Zeilen in der Build-Eingabe, aber tatsächlich 3.285.620 Zeilen tempdb. Das heißt, die Schätzungen für die Zeilen, die sich aus dem Join zwischen TableAund ergeben, @MyTableVarsind weit entfernt. Außerdem ist die Anzahl der Zeilen, die in die Sortierung einfließen, viel größer als angenommen, sodass sie auch überlaufen können.
Martin Smith

Antworten:

21

Bevor Sie zur Hauptantwort gelangen, müssen Sie zwei Softwareteile aktualisieren.

Erforderliche Software-Updates

Der erste ist SQL Server. Sie führen SQL Server 2008 Service Pack 1 (Build 2531) aus. Sie sollten mindestens auf das aktuelle Service Pack (SQL Server 2008 Service Pack 3 - Build 5500) gepatcht sein. Die aktuellste Version von SQL Server 2008 zum Zeitpunkt des Schreibens ist Service Pack 3, Cumulative Update 12 (Build 5844).

Die zweite Software ist SQL Sentry Plan Explorer . Die neuesten Versionen haben bedeutende neue Funktionen und Korrekturen, einschließlich der Möglichkeit, einen Abfrageplan für Expertenanalysen direkt hochzuladen (kein Einfügen von XML erforderlich!).

Abfrageplananalyse

Die Kardinalitätsschätzung für die Tabellenvariable ist dank einer Neukompilierung auf Anweisungsebene genau richtig:

Tabellenvariablenschätzung

Leider verwalten Tabellenvariablen keine Verteilungsstatistiken. Der Optimierer weiß also nur, dass es sechs Zeilen gibt. Es weiß nichts über die Werte in diesen sechs Zeilen. Diese Informationen sind wichtig, da die nächste Operation eine Verknüpfung mit einer anderen Tabelle ist. Die Kardinalitätsschätzung für diesen Join basiert auf einer Vermutung des Optimierers:

Schätzung des ersten Beitritts

Ab diesem Zeitpunkt basiert der vom Optimierer gewählte Plan auf falschen Informationen, sodass es kein Wunder ist, dass die Leistung so schlecht ist. Insbesondere der für Sortierungen und Hash-Tabellen für Hash-Verknüpfungen reservierte Speicher ist viel zu klein. Zur Ausführungszeit werden die überlaufenden Sortier- und Hashing-Vorgänge auf die physische Tempdb-Festplatte verteilt.

SQL Server 2008 hebt dies in Ausführungsplänen nicht hervor. Sie können die Verschüttungen mithilfe von erweiterten Ereignissen oder Profiler- Sortierwarnungen und Hash-Warnungen überwachen . Der Speicher ist für Sortierungen und Hashes reserviert, die auf Kardinalitätsschätzungen basieren, bevor die Ausführung beginnt, und kann während der Ausführung nicht erhöht werden, unabhängig davon, wie viel freier Speicher Ihr SQL Server möglicherweise hat. Genaue Schätzungen der Zeilenanzahl sind daher für jeden Ausführungsplan von entscheidender Bedeutung, der speicherintensive Vorgänge im Arbeitsbereich umfasst.

Ihre Abfrage ist auch parametrisiert. Sie sollten in Betracht ziehen OPTION (RECOMPILE), die Abfrage zu erweitern, wenn sich unterschiedliche Parameterwerte auf den Abfrageplan auswirken. Sie sollten es wahrscheinlich trotzdem verwenden, damit der Optimierer den Wert zum @Param1Zeitpunkt der Kompilierung sehen kann. Dies kann dem Optimierer helfen, eine vernünftigere Schätzung für die oben gezeigte Indexsuche zu erstellen, da die Tabelle sehr groß und partitioniert ist. Dies kann auch die Beseitigung statischer Partitionen ermöglichen.

Versuchen Sie die Abfrage erneut mit einer temporären Tabelle anstelle der Tabellenvariablen und OPTION (RECOMPILE) . Sie sollten auch versuchen, das Ergebnis des ersten Joins in einer anderen temporären Tabelle zu materialisieren, und den Rest der Abfrage dagegen ausführen. Die Anzahl der Zeilen ist nicht allzu groß (3.285.620), daher sollte dies relativ schnell gehen. Das Optimierungsprogramm verfügt dann über eine genaue Kardinalitätsschätzung und Verteilungsstatistik für das Ergebnis des Joins. Mit etwas Glück wird der Rest des Plans gut zusammenpassen.

Ausgehend von den im Plan gezeigten Eigenschaften wäre die materialisierende Abfrage:

SELECT
    A.A_Var7_FK_LK,
    A.A_Var4_FK_LK,
    A.A_Var6_FK_LK, 
    A.A_Var5_FK_LK,
    A.A_Var1,
    A.A_Var2,
    A.A_Var3_FK_LK
INTO #AnotherTempTable
FROM @MyTableVar AS B
JOIN TableA AS A
    ON A.Job = B.B_Var1_PK
WHERE
    A_Var8_FK_LK = @Param1;

Sie könnten auch INSERTin eine vordefinierte temporäre Tabelle (die richtigen Datentypen werden im Plan nicht angezeigt, daher kann ich diesen Teil nicht ausführen). Die neue temporäre Tabelle kann von gruppierten und nicht gruppierten Indizes profitieren oder nicht.

Paul White Monica wieder einsetzen
quelle
Vielen Dank für diese ausführliche Antwort. Es tut mir leid, dass es eine Woche gedauert hat, bis ich antworte. Ich habe jeden Tag daran gearbeitet und andere Arbeiten erledigt. Ich habe Ihre Vorschläge umgesetzt, um den Join zu TableA zu materialisieren #AnotherTempTable. Dies schien die besten Auswirkungen zu haben - die anderen Vorschläge (die Verwendung einer temporären Tabelle anstelle einer Tabellenvariablen für @MyTableVar und die Verwendung von OPTION (RECOMPILE)hatten keine großen oder gar keine Auswirkungen. Die Optionen "Anonymisieren" und "Auf SQLPerformance.com posten" Die Optionen in SQL Sentry Plan Explorer sind großartig - ich habe sie gerade verwendet: answers.sqlperformance.com/questions/1087
Neo
-6

Ich stelle fest, dass auf @MyTableVar ein PK vorhanden sein sollte, und stimme zu, dass #MyTableVar häufig eine bessere Leistung erbringt (insbesondere bei einer größeren Anzahl von Zeilen).

Die Bedingung innerhalb der where-Klausel

   WHERE A.A_Var8_FK_LK = @Param1

sollte in den inneren Join verschoben werden A AND'ed. Der Optimierer ist meiner Erfahrung nach nicht klug genug, um dies zu tun (ich habe den Plan leider nicht angeschaut), und er kann einen großen Unterschied machen.

Wenn diese Änderungen keine Verbesserung zeigen, würde ich als nächstes eine weitere temporäre Tabelle von A erstellen und all die Dinge, mit denen sie verbunden sind, durch A.A_Var8_FK_LK = @ Param1 einschränken, wenn diese Gruppierung für Sie logisch sinnvoll ist.

Erstellen Sie dann einen Clustered-Index für diese temporäre Tabelle (entweder vor oder nach der Erstellung) für die nächste Join-Bedingung.

Verbinden Sie dann dieses Ergebnis mit den wenigen verbleibenden Tabellen (F und T).

Bam, der einen stinkenden Abfrageplan benötigt, wenn die Zeilenschätzungen deaktiviert sind und manchmal ohnehin nicht leicht zu verbessern sind ). Ich gehe davon aus, dass Sie die richtigen Indizes haben, die ich jedoch zuerst im Plan prüfen würde.

Ein Trace kann die Tempdb-Verschüttungen anzeigen, die möglicherweise drastische Auswirkungen haben oder nicht.

Ein anderer alternativer Ansatz - der zumindest schneller ausprobiert werden kann - besteht darin, die Tabellen von der niedrigsten Anzahl von Zeilen (A) bis zur höchsten anzuordnen und dann die Verknüpfungen mit Zusammenführung, Hash und Schleife zu versehen. Wenn Hinweise vorhanden sind, wird die Verknüpfungsreihenfolge wie angegeben festgelegt. Andere Benutzer vermeiden diesen Ansatz mit Bedacht, da er langfristig schaden kann, wenn sich die relativen Zeilenzahlen dramatisch ändern. Eine Mindestanzahl von Hinweisen ist wünschenswert.

Wenn Sie viele dieser Aufgaben ausführen, ist ein kommerzieller Optimierer möglicherweise einen Versuch (oder eine Testversion) wert und dennoch eine gute Lernerfahrung.

crokusek
quelle
Ja ist es. Es stellt sicher, dass die von A zurückgegebenen Zeilen durch die Einschränkung begrenzt sind. Andernfalls tritt der Optimierer möglicherweise zuerst bei und wendet die Einschränkung später an. Ich beschäftige mich täglich damit.
Crokusek
4
@crokusek Du liegst einfach falsch. Der Optimierer von SQL-Server ist ziemlich gut darin zu wissen, dass die Abfragen äquivalent sind (unabhängig davon, ob sich eine Bedingung in der WHERE- oder der ON-Klausel befindet), wenn es sich um einen INNER-Join handelt.
Ypercubeᵀᴹ
6
Möglicherweise finden Sie die Reihe von Paul White im Query Optimiser nützlich.
Martin Smith
Es ist eine schreckliche Angewohnheit. Vielleicht wird es für diesen speziellen Fall (wo es eine Einschränkung gibt), aber ich komme aus dem Land mehrerer Entwickler, die AND-Bedingungen in der where-Klausel aufbauen. SQL Server funktioniert nicht konsequent „bewegen“ sie auf den Rücken für Sie beitreten.
Crokusek
Stimmen Sie für äußere Verknüpfungen (und rechte Verknüpfungen) nicht zu. Wenn eine where-Klausel jedoch nur UND-Ausdrücke enthält und jeder Begriff nur einem bestimmten inneren Join entspricht, kann dieser Begriff als Optimierungs- und Best-Practice-Methode (imo) sicher und zuverlässig an die Position "on" verschoben werden. Ob es sich um eine "echte" Verbindungsbedingung oder nur um eine feste Einschränkung handelt, hängt von einem großen Leistungszuwachs ab. Dieser Link ist für einen trivialen Fall. Das wirkliche Leben hat mehrere Bedingungen, unter denen convert () und math die besten Kandidaten für die Ableitung von Best Practices sind.
Crokusek