Das Einbeziehen von ORDER BY in Abfragen, die keine Zeilen zurückgeben, wirkt sich drastisch auf die Leistung aus

15

Bei einem einfachen Join mit drei Tabellen ändert sich die Abfrageleistung drastisch, wenn ORDER BY eingeschlossen wird, auch wenn keine Zeilen zurückgegeben werden. Das tatsächliche Problemszenario benötigt 30 Sekunden, um Nullzeilen zurückzugeben, ist jedoch sofort verfügbar, wenn ORDER BY nicht enthalten ist. Warum?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Ich verstehe, dass ich einen Index für bigtable.smallGuidId haben könnte, aber ich glaube, das würde es in diesem Fall noch schlimmer machen.

Hier ist ein Skript zum Erstellen / Auffüllen der zu testenden Tabellen. Interessanterweise scheint es wichtig zu sein, dass smalltable ein nvarchar (max) -Feld hat. Es scheint auch von Bedeutung zu sein, dass ich mich mit einem Guid auf dem Bigtable anmelde (was meiner Meinung nach dazu führt, dass Hash-Matching verwendet werden soll).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Ich habe auf SQL 2005, 2008 und 2008R2 mit den gleichen Ergebnissen getestet.

Hafthor
quelle

Antworten:

32

Ich stimme der Antwort von Martin Smith zu, aber das Problem ist nicht einfach nur eine Statistik. Die Statistiken für die foreignId-Spalte (unter der Annahme, dass die automatische Statistik aktiviert ist) zeigen genau, dass für einen Wert von 3 keine Zeilen vorhanden sind (es gibt nur eine mit einem Wert von 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

Statistikausgabe

SQL Server weiß, dass sich seit der Erfassung der Statistiken möglicherweise Änderungen ergeben haben. Bei der Ausführung des Plans wird daher möglicherweise eine Zeile für den Wert 3 angezeigt . Außerdem kann zwischen der Kompilierung und Ausführung des Plans eine beliebige Zeit vergehen (die Pläne werden schließlich zur Wiederverwendung zwischengespeichert). Wie Martin sagt, enthält SQL Server Logik, um zu erkennen, wann aus Gründen der Optimalität ausreichende Änderungen vorgenommen wurden, um das Neukompilieren eines zwischengespeicherten Plans zu rechtfertigen.

Nichts davon ist jedoch letztendlich von Bedeutung. Mit einer Ausnahme für den Fall einer Kante schätzt der Optimierer die Anzahl der von einer Tabellenoperation erzeugten Zeilen niemals auf Null. Wenn statisch festgestellt werden kann, dass die Ausgabe immer Nullzeilen sein muss, ist der Vorgang redundant und wird vollständig entfernt.

Das Modell des Optimierers schätzt stattdessen mindestens eine Zeile. Die Verwendung dieser Heuristik führt im Durchschnitt tendenziell zu besseren Plänen, als dies der Fall wäre, wenn eine niedrigere Schätzung möglich wäre. Ein Plan, der zu einem bestimmten Zeitpunkt eine Nullzeilenschätzung erstellt, wäre von diesem Zeitpunkt an im Verarbeitungsstrom unbrauchbar, da es keine Grundlage für kostenbasierte Entscheidungen gibt (Nullzeilen sind Nullzeilen, egal was passiert). Wenn sich die Schätzung als falsch herausstellt, hat die Planform über der Nullzeilenschätzung fast keine Chance, vernünftig zu sein.

Der zweite Faktor ist eine weitere Modellannahme, die als Containment-Annahme bezeichnet wird. Dies besagt im Wesentlichen, dass sich die Bereiche überlappen, wenn eine Abfrage einen Wertebereich mit einem anderen Wertebereich verknüpft. Eine andere Möglichkeit, dies zu formulieren, besteht darin, dass der Join angegeben wird, da erwartet wird, dass Zeilen zurückgegeben werden. Ohne diese Begründung würden die Kosten im Allgemeinen unterschätzt, was zu schlechten Plänen für ein breites Spektrum gängiger Fragen führen würde.

Was Sie hier haben, ist im Wesentlichen eine Abfrage, die nicht zum Modell des Optimierers passt. Es gibt nichts, was wir tun können, um Schätzungen mit mehrspaltigen oder gefilterten Indizes zu verbessern. Es gibt keine Möglichkeit, hier eine Schätzung von weniger als einer Zeile zu erhalten. Eine echte Datenbank verfügt möglicherweise über Fremdschlüssel, um sicherzustellen, dass dies nicht der Fall ist. Unter der Annahme, dass dies hier nicht zutrifft, müssen wir Hinweise verwenden, um den Zustand außerhalb des Modells zu korrigieren. Beliebig viele verschiedene Hinweisansätze funktionieren mit dieser Abfrage. OPTION (FORCE ORDER)ist eine, die mit der Abfrage wie geschrieben gut funktioniert.

Paul White Monica wieder einsetzen
quelle
21

Das Grundproblem ist hier die Statistik.

Bei beiden Abfragen zeigt die geschätzte SELECTZeilenzahl , dass das endgültige Ergebnis 1.048.580 Zeilen (die gleiche Anzahl von Zeilen, für die geschätzt wird, dass sie vorhanden sind bigtable) und nicht die tatsächliche Null zurückgibt .

Beide JOINBedingungen stimmen überein und würden alle Zeilen beibehalten. Sie werden eliminiert, weil die einzelne Zeile in tinytablenicht mit dem t.foreignId=3Prädikat übereinstimmt .

Wenn du läufst

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

und Blick auf die geschätzte Anzahl von Zeilen ist es 1nicht 0und dieser Fehler pflanzt sich durch den Plan. tinytableDerzeit enthält 1 Zeile. Die Statistiken für diese Tabelle werden erst nach 500 Zeilenänderungen neu kompiliert , sodass eine übereinstimmende Zeile hinzugefügt werden kann und keine Neukompilierung ausgelöst wird.

Der Grund, warum sich die Verknüpfungsreihenfolge ändert, wenn Sie die ORDER BYKlausel hinzufügen und eine varchar(max)Spalte hinzugefügt wird, smalltablebesteht darin, dass varchar(max)die Spaltengröße im Durchschnitt um 4.000 Byte erhöht wird. Multiplizieren Sie dies mit 1048580 Zeilen und es bedeutet, dass die Sortieroperation geschätzte 4 GB benötigen würde, sodass es sinnvoll ist, die SORTOperation vor dem auszuführen JOIN.

Sie können die ORDER BYAbfrage dazu zwingen, die Nicht- ORDER BYJoin-Strategie unter Verwendung der folgenden Hinweise zu übernehmen.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

Der Plan zeigt einen Sortieroperator mit geschätzten Teilbaumkosten von nahezu 12,000und fehlerhaft geschätzten Zeilenzahlen und geschätzter Datengröße.

Planen

Übrigens habe ich UNIQUEIDENTIFIERin meinem Test nicht gefunden, dass ich die Spalten durch ganzzahlige ersetzt habe.

Martin Smith
quelle
2

Aktivieren Sie die Schaltfläche Ausführungsplan anzeigen, und Sie können sehen, was passiert. Hier ist der Plan für die "langsame" Abfrage: Bildbeschreibung hier eingeben

Und hier ist die "schnelle" Abfrage: Bildbeschreibung hier eingeben

Schauen Sie sich das an - laufen Sie zusammen, die erste Abfrage ist ~ 33x "teurer" (Verhältnis 97: 3). SQL optimiert die erste Abfrage, um die BigTable nach Datum und Uhrzeit zu ordnen, und führt dann eine kleine "Such" -Schleife über SmallTable & TinyTable aus, die jeweils 1 Million Mal ausgeführt wird (Sie können den Mauszeiger über das Symbol "Clustered Index Seek" bewegen, um weitere Statistiken abzurufen). Die Sortierung (27%) und 2 x 1 Million "Suchvorgänge" an kleinen Tabellen (23% und 46%) machen den größten Teil der teuren Abfrage aus. Im Vergleich dazu führt die Nicht- ORDER BYAbfrage insgesamt 3 Scans durch.

Grundsätzlich haben Sie eine Lücke in der SQL-Optimierungslogik für Ihr bestimmtes Szenario gefunden. Wenn Sie jedoch, wie in TysHTTP angegeben, einen Index hinzufügen (der Ihre Einfügung / Aktualisierung etwas verlangsamt), wird das Scannen schnell verrückt.

jklemmack
quelle
2

Was passiert, ist, dass SQL beschließt, die Reihenfolge vor der Einschränkung auszuführen.

Versuche dies:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Auf diese Weise erhalten Sie eine verbesserte Leistung (in diesem Fall ist die Anzahl der zurückgegebenen Ergebnisse sehr gering), ohne dass die Leistung durch das Hinzufügen eines weiteren Index beeinträchtigt wird. Obwohl es seltsam ist, wenn das SQL-Optimierungsprogramm die Reihenfolge vor dem Join ausführt, liegt es wahrscheinlich daran, dass das Sortieren nach den Joins länger dauern würde als das Sortieren ohne.

Führen Sie zum Schluss das folgende Skript aus und prüfen Sie, ob die aktualisierten Statistiken und Indizes das Problem beheben, das Sie haben:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "
Seph
quelle