Warum werden alle resultierenden Spalten dieser Abfrage schneller ausgewählt als die eine Spalte, die mir wichtig ist?


13

Ich habe eine Abfrage, bei der mit select *nicht nur viel weniger Lesevorgänge, sondern auch deutlich weniger CPU-Zeit als mit select c.Foo.

Dies ist die Abfrage:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Dies endete mit 2.473.658 logischen Lesevorgängen, hauptsächlich in Tabelle B. Es verwendete 26.562 CPU und hatte eine Dauer von 7.965.

Dies ist der generierte Abfrageplan:

Plan from Auswahl eines einzelnen Spaltenwerts Auf PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Wenn ich zu ändere c.ID, *wurde die Abfrage mit 107.049 logischen Lesevorgängen abgeschlossen, die ziemlich gleichmäßig auf alle drei Tabellen verteilt waren. Es verwendete 4.266 CPU und hatte eine Laufzeit von 1.147.

Dies ist der generierte Abfrageplan:

Planen von Alle Werte auswählen Auf PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Ich habe versucht, die von Joe Obbish vorgeschlagenen Abfragehinweise mit den folgenden Ergebnissen zu verwenden:
select c.IDohne Hinweis: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID mit Hinweis: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * ohne Hinweis: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * mit Hinweis: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

Durch die Verwendung des OPTION(LOOP JOIN)Hinweises mit wurde select c.IDdie Anzahl der Lesevorgänge im Vergleich zur Version ohne Hinweis drastisch reduziert, es wird jedoch immer noch etwa die vierfache Anzahl der Lesevorgänge für die select *Abfrage ohne Hinweise ausgeführt. Das Hinzufügen OPTION(RECOMPILE, HASH JOIN)zu der select *Abfrage hat die Leistung wesentlich verschlechtert als alles, was ich bisher versucht habe.

Nach dem Aktualisieren der Statistiken für die Tabellen und ihre Indizes mithilfe von WITH FULLSCANwird die select c.IDAbfrage viel schneller ausgeführt:
select c.IDVor dem Update: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * Vor dem Update: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID nach dem Update: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * nach dem Update: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *Immer noch besser als die select c.IDGesamtdauer und die Gesamtanzahl der Lesevorgänge ( select *hat etwa die Hälfte der Lesevorgänge), verbraucht jedoch mehr CPU. Insgesamt sind sie viel näher als vor dem Update, jedoch unterscheiden sich die Pläne immer noch.

Das gleiche Verhalten ist für 2016 im Kompatibilitätsmodus 2014 und für 2014 zu beobachten. Was könnte die Diskrepanz zwischen den beiden Plänen erklären? Könnte es sein, dass die "richtigen" Indizes nicht erstellt wurden? Könnten leicht veraltete Statistiken dazu führen?

Ich habe versucht, ONdie Vergleichselemente auf mehrere Arten in den Teil des Joins zu verschieben, aber der Abfrageplan ist jedes Mal derselbe.

Nach Index-Neuerstellungen

Ich habe alle Indizes für die drei an der Abfrage beteiligten Tabellen neu erstellt. c.IDführt immer noch die meisten Lesevorgänge aus (mehr als doppelt so viele *), die CPU-Auslastung beträgt jedoch etwa die Hälfte der *Version. Die c.IDVersion auch verschüttete in tempdb auf die Sortierung von ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Ich habe auch versucht, den Betrieb ohne Parallelität zu erzwingen. Dies ergab die beste Abfrage: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Ich bemerke die Ausführungsanzahl der Operatoren NACH dem großen Index-Suchvorgang, bei dem die Bestellung in der Singlethread-Version nur 1.000 Mal ausgeführt wird, in der parallelisierten Version jedoch deutlich mehr, und zwar zwischen 2.622 und 4.315 Ausführungen verschiedener Operatoren.

Antworten:


4

Wenn Sie mehr Spalten auswählen, muss SQL Server möglicherweise härter arbeiten, um die angeforderten Ergebnisse der Abfrage zu erhalten. Wenn das Abfrageoptimierungsprogramm den perfekten Abfrageplan für beide Abfragen erstellen könnte, wäre es vernünftig, das zu erwartenSELECT *query wird länger ausgeführt als die Abfrage, mit der alle Spalten aus allen Tabellen ausgewählt werden. Sie haben das Gegenteil für Ihr Abfragepaar beobachtet. Sie müssen beim Vergleichen der Kosten vorsichtig sein, aber die langsame Abfrage hat geschätzte Gesamtkosten von 1090,08 Optimierungseinheiten und die schnelle Abfrage hat geschätzte Gesamtkosten von 6823,11 Optimierungseinheiten. In diesem Fall kann man sagen, dass der Optimierer mit der Schätzung der gesamten Abfragekosten einen schlechten Job macht. Es wurde ein anderer Plan für Ihre SELECT * -Abfrage ausgewählt, und es wurde erwartet, dass dieser Plan teurer wird, aber das war hier nicht der Fall. Diese Art der Nichtübereinstimmung kann aus vielen Gründen auftreten, und eine der häufigsten Ursachen sind Probleme mit der Kardinalitätsschätzung. Die Betreiberkosten werden weitgehend durch Schätzungen der Kardinalität bestimmt. Wenn eine Kardinalitätsschätzung an einem wichtigen Punkt eines Plans ungenau ist, spiegeln die Gesamtkosten des Plans möglicherweise nicht die Realität wider. Dies ist eine grobe Vereinfachung, aber ich hoffe, dass es hilfreich ist, um zu verstehen, was hier vor sich geht.

Lassen Sie uns zunächst erläutern, warum eine SELECT *Abfrage möglicherweise teurer ist als die Auswahl einer einzelnen Spalte. Die SELECT *Abfrage kann einige abdeckende Indizes in nicht abdeckende Indizes umwandeln. Dies kann bedeuten, dass das Optimierungsprogramm zusätzliche Arbeiten ausführen muss, um alle benötigten Spalten abzurufen, oder aus einem größeren Index lesen muss.SELECT *kann auch zu größeren Zwischenergebnissen führen, die während der Ausführung der Abfrage verarbeitet werden müssen. Sie können dies in Aktion sehen, indem Sie sich die geschätzten Zeilengrößen in beiden Abfragen ansehen. In der Schnellabfrage reichen Ihre Zeilengrößen von 664 Byte bis 3019 Byte. In der langsamen Abfrage liegen Ihre Zeilengrößen zwischen 19 und 36 Byte. Das Blockieren von Operatoren wie Sortierungen oder Hash-Builds verursacht höhere Kosten für Daten mit einer größeren Zeilengröße, da SQL Server weiß, dass es teurer ist, größere Datenmengen zu sortieren oder sie in eine Hash-Tabelle umzuwandeln.

Bei der schnellen Abfrage schätzt das Optimierungsprogramm, dass 2,4 Millionen Index-Suchvorgänge erforderlich sind Database1.Schema1.Object5.Index3. Von dort kommen die meisten Kosten des Plans. Der tatsächliche Plan zeigt jedoch, dass nur 1332 Index-Suchvorgänge für diesen Operator durchgeführt wurden. Wenn Sie die tatsächlichen mit den geschätzten Zeilen für die äußeren Teile dieser Schleifenverknüpfungen vergleichen, werden Sie große Unterschiede feststellen. Das Optimierungsprogramm geht davon aus, dass wesentlich mehr Indexsuchen erforderlich sind, um die ersten 1000 Zeilen zu finden, die für die Abfrageergebnisse benötigt werden. Aus diesem Grund hat die Abfrage einen relativ hohen Kostenplan, wird aber so schnell ausgeführt: Der Betreiber, von dem vorhergesagt wurde, dass er der teuerste ist, hat weniger als 0,1% seiner erwarteten Arbeit geleistet.

Betrachtet man die langsame Abfrage, so erhält man einen Plan mit den meisten Hash-Joins (ich glaube, der Loop-Join dient nur dazu, die lokale Variable zu behandeln). Kardinalitätsschätzungen sind definitiv nicht perfekt, aber das einzige wirkliche Schätzungsproblem liegt am Ende bei der Sortierung. Ich vermute, dass die meiste Zeit für das Scannen der Tabellen mit Hunderten von Millionen Zeilen aufgewendet wird.

Es kann hilfreich sein, Abfragehinweise zu beiden Versionen der Abfrage hinzuzufügen, um den Abfrageplan zu erzwingen, der der anderen Version zugeordnet ist. Abfragetipps können ein gutes Werkzeug sein, um herauszufinden, warum das Optimierungsprogramm einige seiner Entscheidungen getroffen hat. Wenn Sie OPTION (RECOMPILE, HASH JOIN)der SELECT *Abfrage hinzufügen , wird vermutlich ein ähnlicher Abfrageplan wie bei der Hash-Join-Abfrage angezeigt. Ich erwarte auch, dass die Abfragekosten für den Hash-Join-Plan viel höher sind, da Ihre Zeilen viel größer sind. Das könnte der Grund sein, warum die Hash-Join-Abfrage nicht für die SELECT *Abfrage ausgewählt wurde. Wenn Sie OPTION (LOOP JOIN)der Abfrage hinzufügen , die nur eine Spalte auswählt, wird erwartungsgemäß ein Abfrageplan angezeigt, der dem für das ähneltSELECT *Abfrage. In diesem Fall sollte die Reduzierung der Zeilengröße keinen großen Einfluss auf die Gesamtkosten der Abfrage haben. Sie können die Schlüsselsuche überspringen, aber das ist ein kleiner Prozentsatz der geschätzten Kosten.

Zusammenfassend erwarte ich, dass die größeren Zeilengrößen, die zur Erfüllung der SELECT *Abfrage erforderlich sind , das Optimierungsprogramm in Richtung eines Loop-Join-Plans anstelle eines Hash-Join-Plans verschieben. Der Loop-Join-Plan ist aufgrund von Problemen mit der Kardinalitätsschätzung teurer als erwartet. Das Reduzieren der Zeilengröße durch Auswahl nur einer Spalte senkt die Kosten für einen Hash-Join-Plan erheblich, hat jedoch wahrscheinlich keine großen Auswirkungen auf die Kosten für einen Loop-Join-Plan, sodass Sie den weniger effizienten Hash-Join-Plan erhalten. Für einen anonymisierten Plan ist es schwer, mehr als das zu sagen.


Vielen Dank für Ihre ausführliche und informative Antwort. Ich habe versucht, die von Ihnen vorgeschlagenen Hinweise hinzuzufügen. Die select c.IDAbfrage wurde dadurch viel schneller, es wird jedoch noch zusätzliche Arbeit geleistet, die die select *Abfrage ohne Hinweise leistet.
L. Miller

2

Veraltete Statistiken können sicher dazu führen, dass der Optimierer eine schlechte Methode zum Auffinden der Daten auswählt. Haben Sie versucht, einen Index zu erstellen UPDATE STATISTICS ... WITH FULLSCANoder einen vollständigen REBUILDIndex zu erstellen? Versuchen Sie das und sehen Sie, ob es hilft.

AKTUALISIEREN

Nach einem Update aus dem OP:

Nach dem Aktualisieren der Statistiken für die Tabellen und ihre Indizes mithilfe von WITH FULLSCANwird die select c.IDAbfrage viel schneller ausgeführt

Wenn die einzige Maßnahme nun darin bestand UPDATE STATISTICS, einen Index zu erstellen REBUILD(nicht REORGANIZE), da ich gesehen habe, dass dies bei geschätzten Zeilenzahlen hilft, bei denen sowohl der UPDATE STATISTICSIndex als REORGANIZEauch der Index nicht erfolgreich waren.


Ich konnte alle Indizes für die drei beteiligten Tabellen über das Wochenende neu erstellen und habe meinen Beitrag aktualisiert, um diese Ergebnisse widerzuspiegeln.
L. Miller

-1
  1. Können Sie bitte die Index-Skripte einbinden?
  2. Haben Sie mögliche Probleme mit "Parameter-Sniffing" beseitigt? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Ich habe festgestellt, dass diese Technik in einigen Fällen hilfreich ist:
    a) Schreiben Sie jede Tabelle als Unterabfrage neu, und beachten Sie dabei die folgenden Regeln:
    b) SELECT - Join-Spalten zuerst setzen
    c) PREDICATES - In die entsprechenden Unterabfragen verschieben
    d) ORDER BY - In die entsprechenden Unterabfragen verschieben entsprechende Unterabfragen sortieren nach JOIN COLUMNS FIRST
    e) Fügen Sie eine Wrapper-Abfrage für Ihre endgültige Sortierung und SELECT hinzu.

Die Idee ist, Verknüpfungsspalten in jeder Unterauswahl vorzuordnen, wobei Verknüpfungsspalten in jeder Auswahlliste an erster Stelle stehen.

Hier ist was ich meine ...

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1. Ich werde versuchen, Index-DDL-Skripte zum ursprünglichen Beitrag hinzuzufügen. Das "Scrubben" kann eine Weile dauern. 2. Ich habe diese Möglichkeit getestet, indem ich sowohl den Plan-Cache geleert als auch den Bind-Parameter durch einen tatsächlichen Wert ersetzt habe. 3. Ich habe es versucht, aberORDER BY in einer Unterabfrage ohne TOP, FORXML usw. ist es ungültig. Ich habe es ohne die ORDER BYKlauseln versucht , aber es war der gleiche Plan.
L. Miller
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.