Sie sollten sich in Ausführungsplänen nicht zu sehr auf die prozentualen Kosten verlassen. Dies sind immer geschätzte Kosten , auch in Post-Execution-Plänen mit 'tatsächlichen' Zahlen für Dinge wie Zeilenanzahl. Die geschätzten Kosten basieren auf einem Modell, das für den beabsichtigten Zweck ziemlich gut funktioniert: dem Optimierer zu ermöglichen, zwischen verschiedenen Kandidatenausführungsplänen für dieselbe Abfrage zu wählen. Die Kosteninformationen sind interessant und ein zu berücksichtigender Faktor, sollten jedoch selten eine primäre Metrik für die Abfrageoptimierung sein. Das Interpretieren von Ausführungsplaninformationen erfordert eine breitere Sicht auf die präsentierten Daten.
ItemTran Clustered Index Suchoperator
Dieser Operator ist wirklich zwei Operationen in einer. Zuerst findet eine Indexsuchoperation alle Zeilen, die mit dem Prädikat übereinstimmen product_code_v42 = 'M10BOLT'
, dann wird auf jede Zeile das verbleibende Prädikat bit_field_v41 & 4 = 0
angewendet. Es gibt eine implizite Konvertierung von bit_field_v41
von seinem Basistyp ( tinyint
oder smallint
) nach integer
.
Die Konvertierung erfolgt, weil der bitweise AND-Operator (&) erfordert, dass beide Operanden vom gleichen Typ sind. Der implizite Typ des Konstantenwerts '4' ist eine Ganzzahl, und die Vorrangregeln für Datentypen bedeuten, dass der Feldwert mit der niedrigeren Priorität bit_field_v41
konvertiert wird.
Das Problem (wie es ist) kann leicht behoben werden, indem das Prädikat wie folgt geschrieben wird: bit_field_v41 & CONVERT(tinyint, 4) = 0
- Der konstante Wert hat die niedrigere Priorität und wird (während der konstanten Faltung) anstelle des Spaltenwerts konvertiert. Wenn das bit_field_v41
ist tinyint
auftreten keine Wandlungen überhaupt. Ebenso CONVERT(smallint, 4)
könnte verwendet werden, wenn bit_field_v41
ist smallint
. Allerdings ist die Konvertierung in diesem Fall kein Leistungsproblem , es ist jedoch nach wie vor empfehlenswert, Typen zuzuordnen und implizite Konvertierungen nach Möglichkeit zu vermeiden.
Der größte Teil der geschätzten Kosten für diese Suche hängt von der Größe der Basistabelle ab. Während der gruppierte Indexschlüssel selbst ziemlich schmal ist, ist die Größe jeder Zeile groß. Eine Definition für die Tabelle wird nicht angegeben, sondern nur die in der Ansicht verwendeten Spalten summieren sich zu einer signifikanten Zeilenbreite. Da der gruppierte Index alle Spalten enthält, entspricht der Abstand zwischen gruppierten Indexschlüsseln der Breite der Zeile und nicht der Breite der Indexschlüssel . Die Verwendung von Versionssuffixen für einige Spalten legt nahe, dass die reale Tabelle für frühere Versionen noch mehr Spalten enthält.
Betrachtet man die Such-, Residuenprädikat- und Ausgabespalten, so kann die Leistung dieses Operators isoliert überprüft werden, indem eine entsprechende Abfrage erstellt wird (dies 1 <> 2
ist ein Trick, um die automatische Parametrisierung zu verhindern. Der Widerspruch wird vom Optimierer entfernt und erscheint nicht in der Abfrageplan):
SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
Die Leistung dieser Abfrage mit einem kalten Datencache ist von Interesse, da das Vorauslesen durch die Fragmentierung von Tabellen (Clustered-Index) beeinträchtigt wird. Der Clustering-Schlüssel für diese Tabelle fordert zur Fragmentierung auf. Daher kann es wichtig sein, diesen Index regelmäßig zu verwalten (neu zu organisieren oder zu erstellen) und einen geeigneten FILLFACTOR
Platz für neue Zeilen zwischen den Indexverwaltungsfenstern zu verwenden.
Ich habe den Effekt der Fragmentierung auf das Vorauslesen anhand von Beispieldaten getestet, die mit SQL Data Generator generiert wurden . Bei Verwendung der gleichen Anzahl von Tabellenzeilen wie im Abfrageplan der Frage dauerte ein stark fragmentierter Clustered-Index SELECT * FROM view
15 Sekunden DBCC DROPCLEANBUFFERS
. Dieselbe Prüfung unter denselben Bedingungen mit einem neu erstellten Clustered-Index für die ItemTrans-Tabelle wurde in 3 Sekunden abgeschlossen.
Wenn sich die Tabellendaten normalerweise vollständig im Cache befinden, ist das Fragmentierungsproblem sehr viel weniger wichtig. Aber selbst bei geringer Fragmentierung können die breiten Tabellenzeilen bedeuten, dass die Anzahl der logischen und physischen Lesevorgänge viel höher ist als erwartet. Sie können auch mit dem Hinzufügen und Entfernen des Explicit experimentieren CONVERT
, um meine Erwartung zu bestätigen, dass das implizite Konvertierungsproblem hier nicht wichtig ist, außer als Best-Practice-Verstoß.
Wesentlicher ist die geschätzte Anzahl der Zeilen, die den Suchoperator verlassen. Die Optimierungszeitschätzung beträgt 165 Zeilen, aber zur Ausführungszeit wurden 4.226 Zeilen erstellt. Ich werde später auf diesen Punkt zurückkommen, aber der Hauptgrund für die Diskrepanz ist, dass die Selektivität des verbleibenden Prädikats (einschließlich des bitweisen UND) für den Optimierer sehr schwer vorherzusagen ist - in der Tat greift er auf Vermutungen zurück.
Filter Operator
Ich zeige das Filter-Prädikat hier hauptsächlich, um zu veranschaulichen, wie die beiden NOT IN
Listen kombiniert, vereinfacht und dann erweitert werden, und um eine Referenz für die folgende Hash-Match-Diskussion bereitzustellen. Die Testabfrage aus der Suche kann erweitert werden, um ihre Auswirkungen einzubeziehen und die Auswirkung des Filteroperators auf die Leistung zu bestimmen:
SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
Der Compute Scalar-Operator im Plan definiert den folgenden Ausdruck (die Berechnung selbst wird zurückgestellt, bis das Ergebnis von einem späteren Operator angefordert wird):
[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
Der Hash-Match-Operator
Das Durchführen eines Joins für Zeichendatentypen ist nicht der Grund für die hohen geschätzten Kosten dieses Operators. Die SSMS-QuickInfo zeigt nur einen Hash Keys Probe-Eintrag an, die wichtigen Details befinden sich jedoch im Fenster SSMS-Eigenschaften.
Der Hash-Match-Operator erstellt eine Hash-Tabelle mit den Werten der booking_no_v32
Spalte (Hash Keys Build) aus der ItemTran-Tabelle und sucht dann nach Übereinstimmungen mit der booking_no
Spalte (Hash Keys Probe) aus der Bookings-Tabelle. Der SSMS-Tooltip zeigt normalerweise auch einen Test-Residuum an, aber der Text ist viel zu lang für einen Tooltip und wird einfach weggelassen.
Ein Test-Residuum ähnelt dem Residuum, das nach der früheren Indexsuche angezeigt wird. Das verbleibende Vergleichselement wird für alle Zeilen ausgewertet, die mit dem Hash übereinstimmen, um zu bestimmen, ob die Zeile an den übergeordneten Operator übergeben werden soll. Das Finden von Hash-Übereinstimmungen in einer ausgewogenen Hash-Tabelle ist extrem schnell, aber das Anwenden eines komplexen Residuenprädikats auf jede übereinstimmende Zeile ist im Vergleich dazu recht langsam. In der QuickInfo "Hash Match" im Plan-Explorer werden die Details einschließlich des Ausdrucks "Probe Residual" angezeigt:
Das verbleibende Vergleichselement ist komplex und enthält die Überprüfung des Buchungsfortschritts, sobald die Spalte in der Buchungstabelle verfügbar ist. Der Tooltip zeigt auch die gleiche Diskrepanz zwischen der geschätzten und der tatsächlichen Zeilenanzahl, die zuvor bei der Indexsuche festgestellt wurde. Es mag seltsam erscheinen, dass ein Großteil der Filterung zweimal durchgeführt wird, aber dies ist nur der Optimierer, der optimistisch ist. Es wird nicht erwartet, dass die Teile des Filters, die vom Sondenrest im Plan nach unten verschoben werden können, alle Zeilen entfernen (die Schätzungen der Zeilenanzahl sind vor und nach dem Filter gleich), aber der Optimierer weiß, dass dies möglicherweise falsch ist. Die Chance, Zeilen frühzeitig zu filtern (wodurch die Kosten für den Hash-Join gesenkt werden), ist die geringen Kosten des zusätzlichen Filters wert. Der gesamte Filter kann nicht heruntergedrückt werden, da er einen Test für eine Spalte aus der Buchungstabelle enthält, der größte Teil jedoch.
Die Unterschätzung der Zeilenanzahl ist ein Problem für den Hash-Match-Operator, da die für die Hash-Tabelle reservierte Speicherkapazität von der geschätzten Anzahl der Zeilen abhängt. Wenn der Speicher für die zur Laufzeit erforderliche Größe der Hash-Tabelle zu klein ist (aufgrund der größeren Anzahl von Zeilen), wird die Hash-Tabelle rekursiv in den physischen Tempdb- Speicher verschoben , was häufig zu einer sehr schlechten Leistung führt. Im schlimmsten Fall stoppt die Ausführungs-Engine das rekursive Verschütten von Hash-Buckets und greift auf einen sehr langsamen zurückRettungsalgorithmus. Hash-Spilling (rekursiv oder Bailout) ist die wahrscheinlichste Ursache für die in der Frage beschriebenen Leistungsprobleme (keine zeichenartigen Join-Spalten oder impliziten Konvertierungen). Die Hauptursache ist, dass der Server aufgrund einer falschen Schätzung der Zeilenanzahl (Kardinalität) zu wenig Speicher für die Abfrage reserviert.
Leider gibt es vor SQL Server 2012 im Ausführungsplan keinen Hinweis darauf, dass ein Hashvorgang seine Speicherzuordnung überschritten hat (die nach der Reservierung vor dem Start der Ausführung nicht dynamisch anwachsen kann, selbst wenn der Server über eine große Menge an freiem Speicher verfügt) und überflüssig werden musste tempdb. Es ist möglich, die Hash Warning Event Class mit dem Profiler zu überwachen , es kann jedoch schwierig sein, die Warnungen mit einer bestimmten Abfrage zu korrelieren.
Behebung der Probleme
Die drei Probleme sind die Fragmentierung, der komplexe Sondenrest im Hash-Match-Operator und die falsche Kardinalitätsschätzung, die sich aus dem Erraten der Indexsuche ergibt.
Empfohlene Lösung
Überprüfen Sie die Fragmentierung und korrigieren Sie sie gegebenenfalls. Planen Sie die Wartung, um sicherzustellen, dass der Index akzeptabel organisiert bleibt. Die übliche Methode zur Korrektur der Kardinalitätsschätzung besteht in der Bereitstellung von Statistiken. In diesem Fall benötigt der Optimierer Statistiken für die Kombination ( product_code_v42
, bitfield_v41 & 4 = 0
). Wir können keine Statistiken für einen Ausdruck direkt erstellen, daher müssen wir zuerst eine berechnete Spalte für den Bitfeldausdruck erstellen und dann die manuelle mehrspaltige Statistik erstellen:
ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
Die berechnete Spaltentextdefinition muss mit dem Text in der Ansichtsdefinition ziemlich genau übereinstimmen, damit die Statistiken verwendet werden. Korrigieren Sie daher gleichzeitig die Ansicht, um die implizite Konvertierung zu beseitigen, und achten Sie darauf, dass der Text übereinstimmt.
Die mehrspaltigen Statistiken sollten zu viel besseren Schätzungen führen und die Wahrscheinlichkeit, dass der Hash-Match-Operator rekursives Verschütten oder den Rettungsalgorithmus verwendet, erheblich verringern. Das Hinzufügen der berechneten Spalte (bei der es sich nur um eine Metadatenoperation handelt und die keinen Platz in der Tabelle beansprucht, da sie nicht markiert ist PERSISTED
) und der mehrspaltigen Statistik ist meine beste Vermutung für eine erste Lösung.
Bei der Lösung von Problemen mit der Abfrageleistung ist es wichtig, die verstrichene Zeit, die CPU-Auslastung, die logischen Lesevorgänge, die physischen Lesevorgänge, die Wartezeiten und -dauern usw. zu messen. Es kann auch nützlich sein, Teile der Abfrage separat auszuführen, um vermutete Ursachen wie oben dargestellt zu überprüfen.
In einigen Umgebungen, in denen eine sekundengenaue Ansicht der Daten nicht wichtig ist, kann es nützlich sein, gelegentlich einen Hintergrundprozess auszuführen, der die gesamte Ansicht in eine Momentaufnahmetabelle umwandelt. Diese Tabelle ist nur eine normale Basistabelle und kann für Leseabfragen indiziert werden, ohne dass die Aktualisierungsleistung beeinträchtigt werden muss.
Indexierung anzeigen
Versuchen Sie nicht, die ursprüngliche Ansicht direkt zu indizieren. Die Leseleistung ist erstaunlich schnell (eine einzelne Suche in einem Ansichtsindex), aber (in diesem Fall) werden alle Leistungsprobleme in den vorhandenen Abfrageplänen auf Abfragen übertragen, die eine der Tabellenspalten ändern, auf die in der Ansicht verwiesen wird. Abfragen, die die Zeilen der Basistabelle ändern, sind in der Tat sehr stark betroffen.
Erweiterte Lösung mit einer teilweise indizierten Ansicht
Es gibt eine teilweise indizierte Ansichtslösung für diese spezielle Abfrage, die Kardinalitätsschätzungen korrigiert und die Filter- und Sondenreste entfernt. Sie basiert jedoch auf einigen Annahmen zu den Daten (meistens meine Vermutung des Schemas) und erfordert eine fachmännische Implementierung, insbesondere in Bezug auf die Eignung Indizes zur Unterstützung der Wartungspläne für die indizierte Ansicht. Ich teile den folgenden Code aus Interesse, ich schlage nicht vor, ihn ohne sehr sorgfältige Analyse und Prüfung zu implementieren .
-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
Die vorhandene Ansicht wurde optimiert, um die oben angegebene indizierte Ansicht zu verwenden:
CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
Beispielabfrage und Ausführungsplan:
SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
Im neuen Plan hat die Hash-Übereinstimmung kein Residuum-Prädikat , es gibt keinen komplexen Filter , kein Residuum-Prädikat für die Suche in der indizierten Ansicht und die Kardinalitätsschätzungen sind genau richtig.
Beispiel für die Auswirkung des Einfügens / Aktualisierens / Löschens von Plänen: Dies ist der Plan für ein Einfügen in die ItemTrans-Tabelle:
Der hervorgehobene Abschnitt ist neu und für die Pflege der indizierten Ansicht erforderlich. Der Tabellenspool gibt eingefügte Basistabellenzeilen für die Pflege indizierter Sichten wieder. Jede Zeile wird mithilfe einer Clustered-Index-Suche mit der Buchungstabelle verbunden. Anschließend wendet ein Filter die WHERE
Prädikate für komplexe Klauseln an, um festzustellen, ob die Zeile der Ansicht hinzugefügt werden muss. In diesem Fall wird eine Einfügung in den Clustered-Index der Ansicht vorgenommen.
Derselbe SELECT * FROM view
zuvor durchgeführte Test wurde in 150 ms durchgeführt, wobei die indizierte Ansicht vorhanden war.
Abschließend stelle ich fest, dass sich Ihr 2008 R2-Server noch bei RTM befindet. Es wird Ihre Leistungsprobleme nicht beheben, aber Service Pack 2 für 2008 R2 ist seit Juli 2012 verfügbar, und es gibt viele gute Gründe, mit Service Packs so aktuell wie möglich zu bleiben.