2010-03-26 7 views
7

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 CASCADEWidgetHistory-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?

+0

Schema? U ist sicher kein einfacher, fehlender Index auf der N-Seite der FKs? –

+0

@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

+0

Hinzugefügt in einigen Spalten/Index/FK Definitionen, falls das überhaupt hilft. – Aaronaught

Antwort

8

SQL Server ist am besten bei mengenbasierte Operationen, während CASCADE Deletionen sind, die ihrer Art nach Rekord-basiert.

SQL Server, im Gegensatz zu den anderen Servern, versucht, die sofortige set-basierte Operationen zu optimieren, jedoch funktioniert es nur eine Ebene tief. Die Datensätze müssen in den übergeordneten Tabellen gelöscht werden, um sie in den untergeordneten Tabellen zu löschen.

Mit anderen Worten, die Kaskadierungsoperationen werden von oben nach unten ausgeführt, während Ihre Lösung abwärts arbeitet, was mehr auf dem Set basiert und effizient ist.

Hier ist ein Beispielschema:

CREATE TABLE t_g (id INT NOT NULL PRIMARY KEY) 

CREATE TABLE t_p (id INT NOT NULL PRIMARY KEY, g INT NOT NULL, CONSTRAINT fk_p_g FOREIGN KEY (g) REFERENCES t_g ON DELETE CASCADE) 

CREATE TABLE t_c (id INT NOT NULL PRIMARY KEY, p INT NOT NULL, CONSTRAINT fk_c_p FOREIGN KEY (p) REFERENCES t_p ON DELETE CASCADE) 

CREATE INDEX ix_p_g ON t_p (g) 

CREATE INDEX ix_c_p ON t_c (p) 

, diese Abfrage:

DELETE 
FROM t_g 
WHERE id > 50000 

und seinen Plan:

|--Sequence 
     |--Table Spool 
     | |--Clustered Index Delete(OBJECT:([test].[dbo].[t_g].[PK__t_g__176E4C6B]), WHERE:([test].[dbo].[t_g].[id] > (50000))) 
     |--Index Delete(OBJECT:([test].[dbo].[t_p].[ix_p_g]) WITH ORDERED PREFETCH) 
     | |--Sort(ORDER BY:([test].[dbo].[t_p].[g] ASC, [test].[dbo].[t_p].[id] ASC)) 
     |   |--Table Spool 
     |    |--Clustered Index Delete(OBJECT:([test].[dbo].[t_p].[PK__t_p__195694DD]) WITH ORDERED PREFETCH) 
     |     |--Sort(ORDER BY:([test].[dbo].[t_p].[id] ASC)) 
     |      |--Merge Join(Inner Join, MERGE:([test].[dbo].[t_g].[id])=([test].[dbo].[t_p].[g]), RESIDUAL:([test].[dbo].[t_p].[g]=[test].[dbo].[t_g].[id])) 
     |        |--Table Spool 
     |        |--Index Scan(OBJECT:([test].[dbo].[t_p].[ix_p_g]), ORDERED FORWARD) 
     |--Index Delete(OBJECT:([test].[dbo].[t_c].[ix_c_p]) WITH ORDERED PREFETCH) 
      |--Sort(ORDER BY:([test].[dbo].[t_c].[p] ASC, [test].[dbo].[t_c].[id] ASC)) 
       |--Clustered Index Delete(OBJECT:([test].[dbo].[t_c].[PK__t_c__1C330188]) WITH ORDERED PREFETCH) 
         |--Table Spool 
          |--Sort(ORDER BY:([test].[dbo].[t_c].[id] ASC)) 
           |--Hash Match(Inner Join, HASH:([test].[dbo].[t_p].[id])=([test].[dbo].[t_c].[p])) 
            |--Table Spool 
            |--Index Scan(OBJECT:([test].[dbo].[t_c].[ix_c_p]), ORDERED FORWARD) 

Zuerst SQL Server Datensätze aus t_g löscht, schließt sich dann die Datensätze gelöscht mit t_p und löscht schließlich von letzterem, verbindet Datensätze gelöscht von t_p mit t_c und löscht von t_c.

Eine einzelne Join mit drei Tabellen wäre in diesem Fall viel effizienter, und das ist, was Sie mit Ihrer Problemumgehung tun.

Wenn Sie sich besser fühlen, Oracle nicht optimiert Kaskadenoperationen in keiner Weise: Sie sind immer NESTED LOOPS und Gott hilft Ihnen, wenn Sie vergessen haben, einen Index auf die referenzierende Spalte zu erstellen.

+0

Interessant ... bedeutet das, dass wenn ich mehr Verschachtelungsebenen habe, sagen wir 4 oder 5, jede mit 100-1000 Zeilen-Fanouts, dass es nur "sicher" (leistungsmäßig) ist, CASCADE-Beziehungen eine Stufe über der Blattebene? – Aaronaught

+0

@Aaronaught: richtig. Das Spoolen erfordert zusätzliche Arbeit, und die Indexstatistiken werden nicht immer korrekt an die Abfragen übergeben. – Quassnoi

+0

Also, wenn ich meine Mathematik richtig gemacht habe, ist eine 'CASCADE DELETE' grundsätzlich eine O (N^X-1) Operation, wobei * N * der durchschnittliche Fanout und * X * die Verschachtelungsebene ist. Versetzt den Datenbankentwurf in eine völlig neue Richtung ... Ich muss immer über die Verschachtelung nachdenken, wenn ich eine weitere "CASCADE" hinzufüge. Ist das irgendwo dokumentiert? Ich frage mich nur, ob es etwas ist, was ich hätte wissen müssen, oder ob es eines dieser "versteckten Features" ist. – Aaronaught