Ändern Sie die Abfrage, um die Schätzungen des Bedieners zu verbessern

14

Ich habe eine Abfrage, die in akzeptabler Zeit ausgeführt wird, aber ich möchte die bestmögliche Leistung erzielen.

Die Operation, die ich zu verbessern versuche, ist die "Indexsuche" auf der rechten Seite des Plans von Knoten 17.

Bildbeschreibung hier eingeben

Ich habe entsprechende Indizes hinzugefügt, aber die Schätzungen, die ich für diesen Vorgang erhalte, entsprechen der Hälfte der erwarteten Werte.

Ich habe versucht, meine Indizes zu ändern, eine temporäre Tabelle hinzuzufügen und die Abfrage neu zu schreiben, konnte sie jedoch nicht weiter vereinfachen, um die richtigen Schätzungen zu erhalten.

Hat jemand irgendwelche Vorschläge, was ich sonst noch probieren kann?

Den vollständigen Plan und seine Details finden Sie hier .

Den nicht anonymisierten Plan finden Sie hier.

Aktualisieren:

Ich habe das Gefühl, dass die ursprüngliche Version der Frage viel Verwirrung stiftete, daher werde ich den ursprünglichen Code mit einigen Erklärungen ergänzen.

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

Antworten:

  1. Warum die ungerade anfängliche Benennung im pasteThePlan-Link?

    Antwort : Da ich den Anonymisierungsplan aus dem SQL Sentry Plan Explorer verwendet habe.

  2. Warum OPTION RECOMPILE?

    Antwort : Weil ich mir Neukompilierungen leisten kann, um Parameter-Sniffing zu vermeiden (die Daten sind / könnten verzerrt sein). Ich habe getestet und bin mit dem Plan, den das Optimierungsprogramm während der Verwendung erstellt, zufrieden OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Antwort : Ich möchte das wirklich vermeiden und würde es nur verwenden, wenn ich eine indizierte Ansicht habe. Wie auch immer, dies ist eine Systemfunktion ( COUNT()), die hier nicht verwendet SCHEMABINDINGwerden kann.

Antworten auf weitere mögliche Fragen:

  1. Warum benutze ich INSERT INTO #temp FROM @customAttrributeValues?

    Antwort : Weil mir aufgefallen ist und ich jetzt weiß, dass bei Verwendung von in eine Abfrage eingebundenen Variablen alle Schätzungen, die aus der Arbeit mit einer Variablen resultieren, immer 1 sind. Und ich habe getestet, wie die Daten in eine temporäre Tabelle gestellt werden, und die Schätzung ist dann gleich den tatsächlichen Zeilen .

  2. Warum habe ich verwendet and acav.CustomAttributeValue_Id in (select id from #temp)?

    Antwort : Ich hätte es bei #temp durch JOIN ersetzen können, aber die Entwickler waren sehr verwirrt und haben die INOption angeboten. Ich denke nicht wirklich, dass es einen Unterschied geben würde, selbst wenn man ersetzt, und so oder so gibt es kein Problem damit.

Radu Gheorghiu
quelle
Ich würde vermuten, dass die #tempErstellung und Verwendung ein Problem für die Leistung sein würde, kein Gewinn. Sie speichern in einer nicht indizierten Tabelle, die nur einmal verwendet werden darf. Versuchen Sie, es vollständig zu entfernen (und ändern Sie es möglicherweise in (select id from #temp)in eine existsUnterabfrage.
ypercubeᵀᴹ
@ ypercubeᵀᴹ Richtig, nur ein paar Seiten weniger werden gelesen, wenn die Variable anstelle einer temporären Tabelle verwendet wird.
Radu Gheorghiu
Übrigens liefert eine Tabellenvariable die richtige Schätzung der Zeilenanzahl, wenn sie mit Option (Neu kompilieren) verwendet wird - aber immer noch keine detaillierten Statistiken, Kardinalität usw.
TH
@TH Nun, ich habe mir im tatsächlichen Ausführungsplan die Schätzungen angesehen, bei denen select id from @customAttrValIdsanstelle von select id from #tempund die geschätzte Anzahl der Zeilen 1für die Variable und 3für #temp (die mit der tatsächlichen Anzahl der Zeilen übereinstimmten) verwendet wurde. Deshalb habe ich ersetzt @mit #. Und ich DO einen Vortrag erinnern (von Brent O oder Aaron Bertrand) , wo sie sagte , dass , wenn eine Tabl Variable die Schätzungen für die Verwendung von immer 1 sein werden und als eine Verbesserung bessere Schätzungen erhalten würden sie eine temporäre Tabelle verwenden.
Radu Gheorghiu
@RaduGheorghiu Ja, aber in der Welt dieser Typen ist die Option (Neukompilieren) selten eine Option, und sie bevorzugen auch temporäre Tabellen aus anderen gültigen Gründen. Möglicherweise wird die Schätzung einfach immer fälschlicherweise als 1 angezeigt
TH

Antworten:

12

Der Plan wurde auf einer SQL Server 2008 R2-RTM-Instanz (Build 10.50.1600) kompiliert. Sie sollten Service Pack 3 (Build 10.50.6000) installieren , gefolgt von den neuesten Patches, um es auf den (aktuellen) neuesten Build 10.50.6542 zu bringen. Dies ist aus einer Reihe von Gründen wichtig, darunter Sicherheit, Fehlerkorrekturen und neue Funktionen.

Die Parameter-Einbettungsoptimierung

In Bezug auf die vorliegende Frage hat SQL Server 2008 R2 RTM die Parameter Embedding Optimization (PEO) für nicht unterstützt OPTION (RECOMPILE). Im Moment zahlen Sie die Kosten für die Neukompilierung, ohne einen der Hauptvorteile zu erkennen.

Wenn PEO verfügbar ist, kann SQL Server die in lokalen Variablen und Parametern gespeicherten Literalwerte direkt im Abfrageplan verwenden. Dies kann zu dramatischen Vereinfachungen und Leistungssteigerungen führen. Weitere Informationen dazu finden Sie in meinem Artikel Parameter Sniffing, Embedding und den RECOMPILE-Optionen .

Hash, Sortieren und Austauschen von verschüttetem Material

Diese werden nur in Ausführungsplänen angezeigt, wenn die Abfrage unter SQL Server 2012 oder höher kompiliert wurde. In früheren Versionen mussten wir Überläufe überwachen, während die Abfrage mit dem Profiler oder Extended Events ausgeführt wurde. Verschüttete Daten führen immer zu physischen E / A- Vorgängen in (und aus) dem permanenten Speicher-Backing- Tempdb , was schwerwiegende Auswirkungen auf die Leistung haben kann, insbesondere wenn die Verschüttung groß ist oder der E / A-Pfad unter Druck steht.

In Ihrem Ausführungsplan gibt es zwei Hash-Match-Operatoren (aggregiert). Der für die Hash-Tabelle reservierte Speicher basiert auf der Schätzung für Ausgabezeilen (dh er ist proportional zur Anzahl der zur Laufzeit gefundenen Gruppen). Der zugewiesene Speicher wird unmittelbar vor Beginn der Ausführung festgelegt und kann während der Ausführung nicht vergrößert werden, unabhängig davon, wie viel freier Speicher in der Instanz vorhanden ist. Im bereitgestellten Plan erzeugen beide Hash-Match-Operatoren (Aggregatoperatoren) mehr Zeilen als vom Optimierer erwartet. Daher kann es zur Laufzeit zu einem Überlauf auf tempdb kommen .

Der Plan enthält auch einen Hash-Match-Operator (Inner Join). Der für die Hash-Tabelle reservierte Speicher basiert auf der Schätzung für probenseitige Eingabezeilen . Die Probe-Eingabe schätzt 847.399 Zeilen, aber 1.223.636 werden zur Laufzeit angetroffen. Dieser Überschuss kann auch ein Überlaufen von Hasch verursachen.

Redundantes Aggregat

Die Hash-Übereinstimmung (Aggregat) an Knoten 8 führt eine Gruppierungsoperation für aus (Assortment_Id, CustomAttrID), aber die Eingabezeilen sind gleich den Ausgabezeilen:

Knoten 8 Hash-Übereinstimmung (aggregiert)

Dies deutet darauf hin, dass die Spaltenkombination ein Schlüssel ist (daher ist die Gruppierung semantisch nicht erforderlich). Die Kosten für die Ausführung des redundanten Aggregats werden durch die Notwendigkeit erhöht, die 1,4 Millionen Zeilen zweimal über Hash-Partitionierungs-Börsen (die Parallelismus-Operatoren auf beiden Seiten) zu übertragen.

Da die beteiligten Spalten aus unterschiedlichen Tabellen stammen, ist es schwieriger als üblich, diese Eindeutigkeitsinformationen an das Optimierungsprogramm zu übermitteln, sodass redundante Gruppierungsvorgänge und unnötiger Austausch vermieden werden.

Ineffiziente Threadverteilung

Wie in der Antwort von Joe Obbish angemerkt , verwendet der Austausch am Knoten 14 eine Hash-Partitionierung, um Zeilen unter Threads zu verteilen. Aufgrund der geringen Anzahl von Zeilen und verfügbaren Schedulern landen leider alle drei Zeilen in einem einzigen Thread. Der scheinbar parallele Plan läuft seriell (mit parallelem Overhead) bis zur Vermittlung am Knoten 9.

Sie können dieses Problem beheben (um eine Round-Robin- oder Broadcast-Partitionierung zu erhalten), indem Sie die Option "Distinct Sort" auf Knoten 13 entfernen. Am einfachsten erstellen Sie dazu einen gruppierten Primärschlüssel für die #tempTabelle und führen die eindeutige Operation aus, wenn Sie die Tabelle laden:

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

Temporäres Caching von Tabellenstatistiken

Trotz der Verwendung von OPTION (RECOMPILE)kann SQL Server das temporäre Tabellenobjekt und die zugehörigen Statistiken zwischen den Prozeduraufrufen zwischenspeichern. Dies ist im Allgemeinen eine willkommene Leistungsoptimierung. Wenn die temporäre Tabelle jedoch bei benachbarten Prozeduraufrufen mit einer ähnlichen Datenmenge gefüllt ist, basiert der neu kompilierte Plan möglicherweise auf falschen Statistiken (die aus einer früheren Ausführung zwischengespeichert wurden). Dies wird in meinen Artikeln, Temporäre Tabellen in gespeicherten Prozeduren und Zwischenspeichern von temporären Tabellen, erläutert .

Um dies zu vermeiden, verwenden Sie diese Option OPTION (RECOMPILE)zusammen mit einer expliziten, UPDATE STATISTICS #TempTablenachdem die temporäre Tabelle ausgefüllt wurde und bevor in einer Abfrage auf sie verwiesen wird.

Abfrage umschreiben

In diesem Teil wird davon ausgegangen, dass die Änderungen an der Erstellung der #TempTabelle bereits vorgenommen wurden.

Angesichts der Kosten möglicher Hash-Verschüttungen und des redundanten Aggregats (und der umgebenden Börsen) kann es sich lohnen, die Menge an Knoten 10 zu materialisieren:

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

Das PRIMARY KEYwird in einem separaten Schritt hinzugefügt, um sicherzustellen, dass die Indexerstellung genaue Informationen zur Kardinalität enthält, und um das Problem der Zwischenspeicherung temporärer Tabellenstatistiken zu vermeiden.

Es ist sehr wahrscheinlich, dass diese Materialisierung im Arbeitsspeicher auftritt (Vermeidung von Tempdb- E / A), wenn die Instanz über genügend Arbeitsspeicher verfügt. Dies ist noch wahrscheinlicher, wenn Sie ein Upgrade auf SQL Server 2012 (SP1 CU10 / SP2 CU1 oder höher) durchführen, wodurch das Eager Write-Verhalten verbessert wurde .

Durch diese Aktion erhält der Optimierer genaue Informationen zur Kardinalität des Zwischensatzes, kann Statistiken erstellen und (Assortment_Id, CustomAttrID)als Schlüssel deklarieren .

Der Plan für die Grundgesamtheit von #Temp2sollte folgendermaßen aussehen (beachten Sie den Clustered-Index-Scan von #Temp, no Distinct Sort, und der Austausch verwendet jetzt eine Round-Robin-Zeilenpartitionierung):

# Temp2 Bevölkerung

Wenn dieses Set verfügbar ist, lautet die endgültige Abfrage:

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

Wir könnten das COUNT_BIG(DISTINCT...als einfaches manuell umschreiben COUNT_BIG(*), aber mit den neuen Schlüsselinformationen erledigt der Optimierer das für uns:

Endgültiger Plan

Der endgültige Plan verwendet möglicherweise einen Loop / Hash / Merge-Join, abhängig von statistischen Informationen zu den Daten, auf die ich keinen Zugriff habe. Noch eine kleine Anmerkung: Ich habe angenommen, dass ein Index wie CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);existiert.

Das Wichtigste an den endgültigen Plänen ist jedoch, dass die Schätzungen viel besser sein sollten und die komplexe Abfolge der Gruppierungsvorgänge auf ein einziges Stream-Aggregat reduziert wurde (das keinen Speicher benötigt und daher nicht auf die Festplatte übertragen werden kann).

Es ist schwer zu sagen, dass die Leistung in diesem Fall mit der zusätzlichen temporären Tabelle tatsächlich besser sein wird, aber die Schätzungen und Planoptionen sind gegenüber Änderungen des Datenvolumens und der Verteilung im Laufe der Zeit viel widerstandsfähiger. Das kann langfristig wertvoller sein als eine kleine Leistungssteigerung heute. In jedem Fall haben Sie jetzt viel mehr Informationen, auf die Sie Ihre endgültige Entscheidung stützen können.

Paul White Monica wieder einsetzen
quelle
9

Die Kardinalitätsschätzungen für Ihre Anfrage sind tatsächlich sehr gut. Es ist selten, dass die Anzahl der geschätzten Zeilen genau mit der Anzahl der tatsächlichen Zeilen übereinstimmt, insbesondere wenn Sie über so viele Verknüpfungen verfügen. Schätzungen der Join-Kardinalität sind für das Optimierungsprogramm schwierig. Wichtig ist, dass die Anzahl der geschätzten Zeilen für den inneren Teil der verschachtelten Schleife pro Ausführung dieser Schleife angegeben wird. Wenn SQL Server angibt, dass 463869 Zeilen mit der Indexsuche abgerufen werden, ist die tatsächliche Schätzung in diesem Fall die Anzahl der Ausführungen (2). Die Anzahl der geschätzten Zeilen ist unmittelbar nach dem Join mit verschachtelten Schleifen an Knoten-ID 10 nahezu perfekt.

Schlechte Schätzungen der Kardinalität sind meist ein Problem, wenn das Abfrageoptimierungsprogramm den falschen Plan auswählt oder dem Plan nicht genügend Speicherplatz gewährt. Ich sehe für diesen Plan keine Zeitverschwendung, daher sieht das Gedächtnis in Ordnung aus. Für den Join mit verschachtelten Schleifen, den Sie aufrufen, verfügen Sie über eine kleine äußere Tabelle und eine indizierte innere Tabelle. Was stimmt damit nicht? Um genau zu sein, was würde das Abfrageoptimierungsprogramm hier anders machen?

In Bezug auf die Verbesserung der Leistung fällt mir auf, dass SQL Server einen Hashing-Algorithmus verwendet, um parallele Zeilen zu verteilen, was dazu führt, dass sich alle auf demselben Thread befinden:

Thread-Ungleichgewicht

Infolgedessen führt ein Thread die gesamte Arbeit mit der Indexsuche aus:

Thread Ungleichgewicht suchen

Das bedeutet, dass Ihre Abfrage effektiv erst dann parallel ausgeführt wird, wenn der Operator für erneute Partitionierungsströme auf Knoten-ID 9 ausgeführt wird. Sie möchten wahrscheinlich eine Round-Robin-Partitionierung, sodass jede Zeile auf einem eigenen Thread endet. Auf diese Weise können zwei Threads die Indexsuche für Knoten-ID 17 durchführen. Wenn Sie einen überflüssigen TOPOperator hinzufügen, erhalten Sie möglicherweise eine Round-Robin-Partitionierung. Ich kann hier Details hinzufügen, wenn Sie möchten.

Wenn Sie sich wirklich auf Kardinalitätsschätzungen konzentrieren möchten, können Sie die Zeilen nach dem ersten Join in eine temporäre Tabelle einfügen. Wenn Sie Statistiken zur temporären Tabelle erfassen, die dem Optimierer weitere Informationen zur äußeren Tabelle für den von Ihnen aufgerufenen Nested-Loop-Join liefert. Dies kann auch zu einer Round-Robin-Partitionierung führen.

Wenn Sie die Ablaufverfolgungsflags 4199 oder 2301 nicht verwenden, können Sie sie in Betracht ziehen. Das Ablaufverfolgungsflag 4199 bietet eine Vielzahl von Optimierungskorrekturen, die jedoch einige Arbeitslasten beeinträchtigen können. Das Ablaufverfolgungsflag 2301 ändert einige der Join-Kardinalitätsannahmen des Abfrageoptimierers und macht es schwieriger. In beiden Fällen sollten Sie den Test sorgfältig durchführen, bevor Sie ihn aktivieren.

Joe Obbish
quelle
-2

Ich glaube, eine bessere Schätzung dieses Joins wird den Plan nicht ändern, es sei denn, 1,4 Mill ist ein ausreichender Teil der Tabelle, damit das Optimierungsprogramm einen Index- (nicht Cluster-) Scan mit Hash- oder Merge-Join auswählt. Ich vermute, dass dies hier weder der Fall noch hilfreich wäre, aber Sie können die Auswirkungen testen, indem Sie den inneren Join gegen den CustomAttributeValues ​​durch den inneren Hash-Join und den inneren Merge-Join ersetzen .

Ich habe mir den Code auch genauer angesehen und sehe keine Möglichkeit, ihn zu verbessern - ich wäre interessiert, wenn ich mich natürlich als falsch erweisen könnte. Und wenn Sie Lust haben, die vollständige Logik des Ziels zu veröffentlichen, wäre ich an einem anderen Look interessiert.

TH
quelle
3
Es gibt sehr viele Pläne für diese Abfrage, mit vielen Optionen für die Reihenfolge und Verschachtelung von Verknüpfungen, Parallelität, lokale / globale Aggregation usw. usw., von denen die meisten durch Änderungen der abgeleiteten Statistiken (Verteilung sowie unformatierte Kardinalität) beeinflusst würden. Beachten Sie auch, dass Verknüpfungshinweise im Allgemeinen vermieden werden sollten, da sie mit einer Stille geliefert werden OPTION(FORCE ORDER), die verhindert, dass der Optimierer Verknüpfungen aus der Textsequenz und vielen anderen Optimierungen neu anordnet.
Paul White setzt Monica wieder ein
-12

Sie werden sich von einem [nicht gruppierten] Index-Suchvorgang nicht verbessern. Das einzige, was besser ist als eine nicht gruppierte Indexsuche, ist eine gruppierte Indexsuche.

Außerdem bin ich seit zehn Jahren SQL-Datenbankadministrator und davor seit fünf Jahren SQL-Entwickler, und meiner Erfahrung nach ist es äußerst selten, eine Verbesserung einer SQL-Abfrage zu finden, wenn Sie den Ausführungsplan untersuchen, den Sie nicht finden konnten. ' nicht mit anderen Mitteln finden. Der Hauptgrund für das Generieren des Ausführungsplans besteht darin, dass häufig fehlende Indizes vorgeschlagen werden, die Sie zur Verbesserung der Leistung hinzufügen können.

Der Hauptvorteil der Leistung besteht darin, die SQL-Abfrage selbst anzupassen, falls dort Ineffizienzen auftreten. Zum Beispiel habe ich vor ein paar Monaten eine SQL-Funktion erhalten, die 160-mal schneller ausgeführt werden kann, indem eine SELECT UNION SELECTStil-Pivot-Tabelle neu geschrieben wurde, um den Standard-SQL- PIVOTOperator zu verwenden.

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Mal sehen, SELECT * INTOist im Allgemeinen weniger effizient als ein Standard INSERT Object1 (column list) SELECT column list. Also würde ich das umschreiben. Wenn als Nächstes Funktion1 ohne a definiert wurde WITH SCHEMABINDING, wird a hinzugefügtWITH SCHEMABINDING Klausel eine schnellere Ausführung ermöglichen.

Sie haben viele Aliase ausgewählt, die keinen Sinn ergeben, z. B. das Aliasing von Object2 als Object3. Sie sollten bessere Aliase auswählen, die den Code nicht verschleiern. Sie haben "Object7.Column5 in (wählen Sie Column1 von Object1 aus)".

INKlauseln dieser Art sind immer effizienter geschrieben als EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Vielleicht hätte ich das andersherum schreiben sollen. EXISTSwird immer mindestens so gut sein wieIN . Es ist nicht immer besser, ist es aber normalerweise.

Ich bezweifle auch, dass dies option(recompile)die Abfrageleistung hier verbessert. Ich würde es testen, um es zu entfernen.

Matthew Sontum
quelle
6
Wenn eine nicht gruppierte Indexsuche die Abfrage abdeckt, ist sie fast immer besser als eine gruppierte Indexsuche, da der gruppierte Index definitionsgemäß alle Spalten enthält und der nicht gruppierte Index weniger Spalten enthält, sodass weniger Seitensuchen erforderlich sind (und weniger Stufen in den B-Baum), um die Daten abzurufen. Es ist also nicht richtig zu sagen, dass eine Clustered-Index-Suche immer besser ist.
ErikE