Ich habe eine Frage wie die folgende:
DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)
tblFEStatsBrowsers hat 553 Zeilen.
tblFEStatsPaperHits hat 47.974.301 Zeilen.
tblFEStatsBrowsers:
CREATE TABLE [dbo].[tblFEStatsBrowsers](
[BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
[Browser] [varchar](50) NOT NULL,
[Name] [varchar](40) NOT NULL,
[Version] [varchar](10) NOT NULL,
CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)
tblFEStatsPaperHits:
CREATE TABLE [dbo].[tblFEStatsPaperHits](
[PaperID] [int] NOT NULL,
[Created] [smalldatetime] NOT NULL,
[IP] [binary](4) NULL,
[PlatformID] [tinyint] NULL,
[BrowserID] [smallint] NULL,
[ReferrerID] [int] NULL,
[UserLanguage] [char](2) NULL
)
Es gibt einen Clustered-Index für tblFEStatsPaperHits, der keine BrowserID enthält. Das Ausführen der inneren Abfrage erfordert daher einen vollständigen Tabellenscan von tblFEStatsPaperHits - was völlig in Ordnung ist.
Derzeit wird in tblFEStatsBrowsers für jede Zeile ein vollständiger Scan ausgeführt. Das bedeutet, dass ich 553 vollständige Tabellenscans von tblFEStatsPaperHits habe.
Das Umschreiben auf einen WHERE EXISTS ändert den Plan nicht:
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)
Wie von Adam Machanic vorgeschlagen, führt das Hinzufügen einer HASH JOIN-Option jedoch zu einem optimalen Ausführungsplan (nur ein einziger Scan von tblFEStatsPaperHits):
DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)
Nun ist dies nicht so sehr eine Frage, wie man das behebt - ich kann entweder die OPTION (HASH JOIN) verwenden oder manuell eine temporäre Tabelle erstellen. Ich frage mich eher, warum das Abfrageoptimierungsprogramm jemals den Plan verwenden würde, den es derzeit ausführt.
Da die QO keine Statistiken in der BrowserID-Spalte hat, wird davon ausgegangen, dass es sich um die schlechtesten Werte handelt - 50 Millionen verschiedene Werte, weshalb eine ziemlich große In-Memory / Tempdb-Arbeitstabelle erforderlich ist. Daher ist es am sichersten, in tblFEStatsBrowsers für jede Zeile einen Scan durchzuführen. Es gibt keine Fremdschlüsselbeziehung zwischen den BrowserID-Spalten in den beiden Tabellen, sodass der QO keine Informationen von tblFEStatsBrowsers ableiten kann.
Ist das so einfach, wie es sich anhört, der Grund?
Update 1
Um ein paar Statistiken zu geben: OPTION (HASH JOIN):
208.711 logische Lesevorgänge (12 Scans)
OPTION (LOOP JOIN, HASH GROUP):
11.008.698 logische Lesevorgänge (~ Scan per BrowserID (339))
Keine Optionen:
11.008.775 logische Lesevorgänge (~ Scan per BrowserID (339))
Update 2
Hervorragende Antworten, ihr alle - danke! Schwierig, nur einen auszuwählen. Obwohl Martin der erste war und Remus eine ausgezeichnete Lösung bietet, muss ich es der Kiwi geben, um die Details im Kopf zu behalten :)
quelle
Antworten:
Anders ausgedrückt ist die Frage, warum der folgende Plan für den Optimierer im Vergleich zu den Alternativen (von denen es viele gibt ) am billigsten erscheint .
Auf der Innenseite des Joins wird für jeden korrelierten Wert von im Wesentlichen eine Abfrage der folgenden Form ausgeführt
BrowserID
:Beachten Sie, dass die geschätzte Anzahl der Zeilen 185.220 (nicht 289.013 ) beträgt, da der Gleichheitsvergleich implizit ausschließt
NULL
(sofern dies nicht der FallANSI_NULLS
istOFF
). Die geschätzten Kosten des oben genannten Plans betragen 206,8 Einheiten.Fügen wir nun eine
TOP (1)
Klausel hinzu:Die geschätzten Kosten betragen jetzt 0,00452 Einheiten. Durch Hinzufügen des Operators "Top" wird beim Operator "Top" ein Zeilenziel von 1 Zeile festgelegt. Die Frage lautet dann, wie ein Zeilenziel für den Clustered Index Scan abgeleitet werden kann. Das heißt, wie viele Zeilen soll der Scan verarbeiten, bevor eine Zeile mit dem
BrowserID
Prädikat übereinstimmt ?Die verfügbaren statistischen Daten zeigen 166 verschiedene
BrowserID
Werte (1 / [All Density] = 1 / 0.006024096 = 166). In der Kalkulation wird davon ausgegangen, dass die unterschiedlichen Werte gleichmäßig auf die physischen Zeilen verteilt sind, sodass das Zeilenziel beim Clustered Index Scan auf 166.302 festgelegt ist (unter Berücksichtigung der Änderung der Tabellenkardinalität seit der Erfassung der Stichprobenstatistik).Die geschätzten Kosten für das Scannen der erwarteten 166 Zeilen sind nicht sehr hoch (sogar 339- maliges Ausführen bei jeder Änderung
BrowserID
). Der Clustered-Index-Scan weist geschätzte Kosten von 1,3219 Einheiten auf und zeigt den Skalierungseffekt des Zeilenziels . Die nicht skalierten Bedienerkosten für E / A und CPU werden mit 153,931 bzw. 52,8698 angegeben:In der Praxis ist es sehr unwahrscheinlich, dass die ersten 166 vom Index durchsuchten Zeilen (in welcher Reihenfolge auch immer sie zurückgegeben werden) jeweils einen der möglichen
BrowserID
Werte enthalten. DerDELETE
Plan kostet jedoch insgesamt 1.40921 Einheiten und wird aus diesem Grund vom Optimierer ausgewählt. Bart Duncan zeigt ein weiteres Beispiel dieses Typs in einem kürzlich veröffentlichten Beitrag mit dem Titel Row Goals Gone Rogue .Es ist auch interessant festzustellen, dass der Top-Operator im Ausführungsplan nicht mit dem Antisemit verbunden ist (insbesondere die Erwähnungen von Martin zum Kurzschließen). Wir können herausfinden, woher das Top stammt, indem wir zuerst eine Erkundungsregel mit dem Namen GbAggToConstScanOrTop deaktivieren :
Dieser Plan hat geschätzte Kosten von 364,912 und zeigt, dass der obere eine Gruppe nach Aggregat (Gruppierung nach der korrelierten Spalte
BrowserID
) ersetzt hat. Das Aggregat ist nicht auf die RedundanzDISTINCT
im Abfragetext zurückzuführen: Es handelt sich um eine Optimierung, die durch die beiden Explorationsregeln LASJNtoLASJNonDist und LASJOnLclDist eingeführt werden kann . Durch Deaktivieren dieser beiden Optionen wird der folgende Plan erstellt:Dieser Plan hat geschätzte Kosten von 40729,3 Einheiten.
Ohne die Umwandlung von Group By nach Top wählt der Optimierer "natürlich" einen Hash-Join-Plan mit
BrowserID
Aggregation vor dem Anti-Semi-Join:Und ohne die MAXDOP 1-Einschränkung ein paralleler Plan:
Eine andere Möglichkeit, die ursprüngliche Abfrage zu "reparieren", besteht darin, den fehlenden Index für
BrowserID
den Ausführungsplan zu erstellen. Verschachtelte Schleifen funktionieren am besten, wenn die Innenseite indiziert ist. Das Schätzen der Kardinalität für Semi-Joins ist im besten Fall eine Herausforderung. Wenn Sie keine ordnungsgemäße Indizierung haben (die große Tabelle hat nicht einmal einen eindeutigen Schlüssel!), Hilft dies überhaupt nicht.Ich habe mehr darüber in Row Goals, Part 4: The Anti Join Anti Pattern geschrieben .
quelle
Wenn ich Ihr Skript ausführe, um nur eine Statistikdatenbank und die Abfrage in der Frage zu erstellen, erhalte ich den folgenden Plan.
Die im Plan angegebenen Tabellenkardinalitäten sind
tblFEStatsPaperHits
:48063400
tblFEStatsBrowsers
:339
Daher wird geschätzt, dass der Scan
tblFEStatsPaperHits
339 Mal ausgeführt werden muss. Jeder Scan hat das korrelierte PrädikattblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULL
, das in den Scanoperator gedrückt wird.Der Plan bedeutet jedoch nicht, dass es 339 vollständige Scans geben wird. Da es sich um einen Anti-Semi-Join-Operator handelt, kann der Rest kurzgeschlossen werden, sobald die erste übereinstimmende Zeile bei jedem Scan gefunden wird. Die geschätzten Teilbaumkosten für diesen Knoten betragen
1.32603
und der gesamte Plan wird mit berechnet1.41337
.Für den Hash Join gibt es den unten stehenden Plan
Der Gesamtplan kostet
418.415
etwa 300-mal mehr als der Plan für verschachtelte Schleifen. Der einzelne vollständige Clustered-Index-ScantblFEStatsPaperHits
kostet nur 300-mal mehr206.8
. Vergleichen Sie dies mit der1.32603
Schätzung für 339 Teilscans, die zuvor angegeben wurde (Durchschnittliche geschätzte Kosten für Teilscans =0.003911592
).Dies würde also bedeuten, dass jeder Teil-Scan 53.000-mal billiger ist als ein vollständiger Scan. Wenn die Kosten linear mit der Zeilenzahl skaliert würden, würde dies bedeuten, dass angenommen wird, dass durchschnittlich nur 900 Zeilen bei jeder Iteration verarbeitet werden müssten, bevor eine übereinstimmende Zeile gefunden wird und ein Kurzschluss auftreten kann.
Ich denke jedoch nicht, dass die Kosten auf diese lineare Weise skalieren. Ich denke, sie beinhalten auch einen Teil der festen Startkosten.
TOP
In der folgenden Abfrage werden verschiedene Werte von ausprobiert147
gibt die nächsten geschätzten Teilbaumkosten0.003911592
an0.0039113
. Wie auch immer, es ist klar, dass die Kosten auf der Annahme basieren, dass jeder Scan nur einen winzigen Teil der Tabelle verarbeiten muss, in der Größenordnung von Hunderten von Zeilen anstatt von Millionen.Ich bin mir nicht sicher, auf welcher Mathematik diese Annahme basiert, und sie stimmt nicht wirklich mit den Zeilenanzahlschätzungen im Rest des Plans überein Fälle, in denen überhaupt keine übereinstimmende Zeile gefunden wurde und ein vollständiger Scan erforderlich war). Ich gehe davon aus, dass dies nur ein Fall ist, in dem die getroffenen Modellierungsannahmen etwas nachlassen und den Plan für verschachtelte Schleifen erheblich unter den Kosten belassen.
quelle
In meinem Buch ist sogar ein Scan von 50 Millionen Zeilen inakzeptabel ... Mein üblicher Trick besteht darin, die unterschiedlichen Werte zu materialisieren und die Engine zu delegieren, um sie auf dem neuesten Stand zu halten:
Auf diese Weise erhalten Sie einen materialisierten Index mit einer Zeile pro Browser-ID, sodass 50 Millionen Zeilen nicht mehr durchsucht werden müssen. Die Engine verwaltet es für Sie und der QO verwendet es in der von Ihnen veröffentlichten Anweisung "wie es ist" (ohne Hinweis oder Umschreiben der Abfrage).
Der Nachteil ist natürlich Streit. Jeder Einfüge- oder Löschvorgang in
tblFEStatsPaperHits
(und ich denke, es handelt sich um eine Protokollierungstabelle mit umfangreichen Einfügungen) muss den Zugriff auf eine bestimmte Browser-ID serialisieren. Es gibt Möglichkeiten, die dies möglich machen (verzögerte Updates, 2-stufige Protokollierung usw.), wenn Sie bereit sind, sich darauf einzulassen.quelle