Ich habe einen wiederkehrenden "Fehlerbericht" (Perf-Problem) in einem unserer Systeme im Zusammenhang mit einem besonders langsamen Löschvorgang analysiert. Long story short: Es scheint, dass die CASCADE DELETE
Schlüssel weitgehend verantwortlich waren, und ich würde gerne wissen (a) wenn das Sinn macht, und (b) warum es der Fall ist.Ist SQL Server DRI (ON DELETE CASCADE) langsam?
Wir haben ein Schema von, sagen wir mal, Widgets, die am Anfang eines großen Graphen verwandter Tabellen und verwandter Tabellen stehen und so weiter. Um ganz klar zu sein, wird aktiv von der Löschung aus dieser Tabelle abgeraten; es ist die "nukleare Option" und die Nutzer machen sich keine Illusionen über das Gegenteil. Trotzdem muss es manchmal einfach gemacht werden.
Das Schema sieht wie folgt aus:
Widgets
|
+--- Anvils [1:1]
| |
| +--- AnvilTestData [1:N]
|
+--- WidgetHistory (1:N)
|
+--- WidgetHistoryDetails (1:N)
Spaltendefinitionen wie folgt aussehen:
Widgets (WidgetID int PK, WidgetName varchar(50))
Anvils (AnvilID int PK, WidgetID int FK/IX/UNIQUE, ...)
AnvilTestData (AnvilID int FK/IX, TestID int, ...Test Data...)
WidgetHistory (HistoryID int PK, WidgetID int FK/IX, HistoryDate datetime, ...)
WidgetHistoryDetails (HistoryID int FK/IX, DetailType smallint, ...)
Nichts zu gruselig, wirklich. A Widget
kann verschiedene Typen sein, ein Anvil
ist ein spezieller Typ, so dass die Beziehung 1: 1 (oder genauer 1: 0..1) ist. Dann gibt es eine große Menge von Daten - vielleicht Tausende von Reihen von AnvilTestData
pro Anvil
gesammelt im Laufe der Zeit, Umgang mit Härte, Korrosion, genaues Gewicht, Hammer Kompatibilität, Usability-Probleme, und Auswirkungen Tests mit Cartoon-Köpfe.
Dann hat jede Widget
eine lange, langweilige Geschichte der verschiedenen Arten von Transaktionen - Produktion, Lagerumzüge, Verkäufe, Mängeluntersuchungen, RMAs, Reparaturen, Kundenbeschwerden, etc. Es könnte 10-20k Details für ein einzelnes Widget sein, oder gar keine, je nach Alter.
Also, es ist nicht überraschend, gibt es eine CASCADE DELETE
Beziehung auf jeder Ebene hier. Wenn ein Widget
gelöscht werden muss, bedeutet es, dass etwas schrecklich falsch gegangen ist und wir alle Datensätze des Widgets löschen müssen, die jemals existierten, einschließlich seiner Historie, Testdaten usw. Wiederum eine nukleare Option.
Beziehungen sind alle indiziert, Statistiken sind auf dem neuesten Stand. Normale Abfragen sind schnell. Das System tendiert dazu, für alles außer Deletes ziemlich reibungslos zu summen.
bis zu dem Punkt Anfahrt schließlich aus verschiedenen Gründen nur wir zu einer Zeit, zu löschen ein Widget ermöglichen, so dass eine Anweisung delete würde wie folgt aussehen:
DELETE FROM Widgets
WHERE WidgetID = @WidgetID
Ganz einfach, harmlos suchen löschen ... das dauert 2 Minuten zu laufen, für ein Widget mit keine Daten!
Nachdem ich die Ausführungspläne durchgegangen bin, konnte ich endlich die AnvilTestData
und WidgetHistoryDetails
Löschungen als die Unteroperationen mit den höchsten Kosten herauspicken. So experimentierte ich mit den CASCADE
ausgeschaltet (aber die tatsächliche FK halten, nur um es zu NO ACTION
Einstellung) und Umschreiben das Skript als etwas sehr ähnlich wie die folgenden:
DECLARE @AnvilID int
SELECT @AnvilID = AnvilID FROM Anvils WHERE WidgetID = @WidgetID
DELETE FROM AnvilTestData
WHERE AnvilID = @AnvilID
DELETE FROM WidgetHistory
WHERE HistoryID IN (
SELECT HistoryID
FROM WidgetHistory
WHERE WidgetID = @WidgetID)
DELETE FROM Widgets WHERE WidgetID = @WidgetID
Beide „Optimierungen“ führte zu einer signifikanten speedups jeder rasiert sich fast eine ganze Minute von der Ausführungszeit, so dass die ursprüngliche 2-Minuten-Löschung nun ungefähr 5-10 Sekunden dauert - zumindest für neue Widgets, ohne viel Geschichte oder Testdaten.
Nur absolut klar zu sein, gibt es noch ein CASCADE
WidgetHistory
-WidgetHistoryDetails
, wo der Fanout am höchsten ist, ich die man nur aus Widgets
Ursprung entfernt.
Further „Abflachung“ der Kaskade Beziehungen führte zu immer weniger dramatisch, aber immer noch spürbar speedups, bis zu dem Punkt, wo fast augenblicklich eine neue Widget zu löschen war einmal alle der Kaskade zu größeren Tabellen löscht wurden mit explizitem entfernt und ersetzt löscht.
Ich verwende DBCC DROPCLEANBUFFERS
und DBCC FREEPROCCACHE
vor jedem Test. Ich habe alle Auslöser deaktiviert, die weitere Verlangsamungen verursachen könnten (obwohl diese sowieso im Ausführungsplan auftauchen würden). Und ich teste auch gegen ältere Widgets und bemerke auch dort eine deutliche Beschleunigung; Löschungen, die früher 5 Minuten dauerten, dauern nun 20-40 Sekunden.
Jetzt bin ich ein glühender Anhänger der Philosophie "SELECT ist nicht kaputt", aber es scheint einfach keine logische Erklärung für dieses Verhalten zu geben, abgesehen von der erdrückenden, verblüffenden Ineffizienz der CASCADE DELETE
Beziehungen.
Also, meine Fragen sind:
dies in SQL Server mit DRI ein bekanntes Problem ist? (ich konnte nicht auf diese Art der Sache auf Google alle Verweise zu finden scheinen, oder hier in SO, ich vermute, dass die Antwort ist nein.)
Wenn nicht, gibt es eine andere Erklärung für das Verhalten, das ich bin Sehen?
Wenn es ein bekanntes Problem ist, warum ist es ein Problem, und gibt es bessere Workarounds, die ich verwenden könnte?
Schema? U ist sicher kein einfacher, fehlender Index auf der N-Seite der FKs? –
@Remus: Beispiel Schema ist da (wenn irgendwelche Details fehlen, lassen Sie mich wissen, was Sie sehen möchten). Definitiv, 100% positiv es ist kein fehlender Index (selbst wenn es wäre, dann wäre die zweite Version auch langsam, oder?) – Aaronaught
Hinzugefügt in einigen Spalten/Index/FK Definitionen, falls das überhaupt hilft. – Aaronaught