Grausame Leistung beim Verbinden von INSERTED- und DELETED-Tabellen in einem Trigger

12

Ich habe einen UPDATE-Trigger für eine Tabelle, die nach einer bestimmten Spalte sucht, die von einem bestimmten Wert zu einem anderen Wert wechselt. In diesem Fall werden einige verwandte Daten in einer anderen Tabelle über eine einzelne UPDATE-Anweisung aktualisiert.

Der Trigger überprüft zunächst, ob bei aktualisierten Zeilen der Wert dieser Spalte gegenüber dem betreffenden Wert geändert wurde. Es verbindet einfach INSERTED mit DELETED und vergleicht den Wert in dieser Spalte. Wenn nichts qualifiziert ist, wird es vorzeitig beendet, sodass die UPDATE-Anweisung nicht ausgeführt wird.

IF NOT EXISTS (
    SELECT TOP 1 i.CUSTNMBR
    FROM INSERTED i
        INNER JOIN DELETED d
            ON i.CUSTNMBR = d.CUSTNMBR
    WHERE d.CUSTCLAS = 'Misc'
        AND i.CUSTCLAS != 'Misc'
)
    RETURN

In diesem Fall ist CUSTNMBR der Primärschlüssel der zugrunde liegenden Tabelle. Wenn ich eine große Aktualisierung dieser Tabelle durchführe (z. B. mehr als 5000 Zeilen), benötigt diese Anweisung AGES, auch wenn ich die Spalte CUSTCLAS nicht berührt habe. Ich kann beobachten, wie diese Aussage für einige Minuten in Profiler zum Stillstand kommt.

Der Ausführungsplan ist bizarr. Es zeigt einen eingefügten Scan mit 3.714 Ausführungen und ~ 18,5 Millionen Ausgabezeilen. Das läuft durch einen Filter in der CUSTCLAS-Spalte. Es verbindet dies (über eine verschachtelte Schleife) mit einem gelöschten Scan (ebenfalls nach CUSTCLAS gefiltert), der nur einmal ausgeführt wird und 5000 Ausgabezeilen hat.

Was für eine idiotische Sache mache ich hier, um das zu verursachen? Beachten Sie, dass der Trigger mehrzeilige Aktualisierungen unbedingt ordnungsgemäß verarbeiten muss.

EDIT :

Ich habe auch versucht, es so zu schreiben (falls EXISTS etwas Unangenehmes tat), aber es ist immer noch genauso schrecklich.

DECLARE @CUSTNMBR varchar(31)
SELECT TOP 1 @CUSTNMBR = i.CUSTNMBR
FROM INSERTED i
    INNER JOIN DELETED d
        ON i.CUSTNMBR = d.CUSTNMBR
WHERE d.CUSTCLAS = 'Misc'
    AND i.CUSTCLAS != 'Misc'

IF @CUSTNMBR IS NULL
    RETURN
db2
quelle
Können Sie die "TOP 1" loswerden? Ich würde denken, dass dies einen gewissen Overhead verursacht, der möglicherweise nicht erforderlich ist, wenn Sie nur prüfen, ob es einen Einzelfall gibt ...
JHFB

Antworten:

10

Sie könnten mit expliziten INNER MERGE JOINoder INNER HASH JOINHinweisen auswerten, aber da Sie diese Tabellen vermutlich später im Trigger wieder verwenden, ist es wahrscheinlich besser, nur den Inhalt insertedund die deletedTabellen in indizierte #tempTabellen einzufügen und damit fertig zu sein.

Sie erhalten keine nützlichen Indizes, die automatisch für sie erstellt werden.

Martin Smith
quelle
Okay, dies beschleunigt es enorm, es besteht jedoch die Möglichkeit einer kaskadierenden Triggerausführung. Wenn ich in jedem Trigger dieselben temporären Tabellennamen (#i, #d) verwende, treten Konflikte auf. Gibt es eine bessere / sicherere Lösung, als in jedem Trigger nur einen anderen temporären Tabellennamen zu verwenden?
db2
Könnte mithilfe von Tabellenvariablen ausgewertet werden (mit einem Primärschlüssel CUSTNMBR, der zum Erstellen des eindeutigen Clustered-Index definiert ist) und den OPTION (RECOMPILE)Hinweis verwenden, um die Anzahl der Zeilen zu berücksichtigen, oder einfach eine bestimmte Namenskonvention verwenden, z. B.#i_dbo_YourTable
Martin Smith,
Ich denke, ich werde mich damit zufrieden geben, sie so zu benennen #trigger_name_i. Wenn ich mit Tabellenvariablen arbeite, muss ich den Code mit expliziten CREATE TABLEs noch mehr überladen. Wir haben kaskadierende Trigger, aber keine rekursiven Trigger, also denke ich, dass ich in Sicherheit bin ...
db2
Ich empfehle zu diesem Zweck eine Tabellenvariable anstelle einer temporären Tabelle. Tabellenvariablen können weiterhin primäre und sekundäre (eindeutige) Indizes haben. Sie werden automatisch bereinigt, wenn der Trigger beendet wird, und Tabellenvariablen werden auf genau diese Triggerausführung beschränkt (es wird kein Konflikt mit anderen gleichnamigen Tabellenvariablen mit höherem oder niedrigerem Wert auftreten der Aufrufstapel). Um den Overhead des Tabellendefinitionscodes zu sparen, definieren Sie jeweils einen Tabellentyp und deklarieren Sie die Tabellenvariablen anhand des Typnamens.
Chris Smith
@ChrisSmith würde man oft auch brauchen, OPTION (RECOMPILE)damit die Kardinalität berücksichtigt wird.
Martin Smith
10

Ich weiß, dass dies beantwortet wurde, aber es ist erst kürzlich als aktiv aufgetaucht, und ich bin auch bei Tabellen mit vielen Millionen Zeilen darauf gestoßen. Obwohl die akzeptierte Antwort nicht abgezinst wird, kann ich zumindest hinzufügen, dass meine Erfahrung zeigt, dass ein Schlüsselfaktor für die Triggerleistung bei ähnlichen Tests (ob eine oder mehrere Spalten tatsächlich ihre Werte geändert haben) darin besteht, ob die Spalte (n) geändert wurden oder nicht. getestet zu werden waren eigentlich Teil der UPDATEAussage. Ich stellte fest, dass der Vergleich von Spalten zwischen insertedund und deletedTabellen, die tatsächlich nicht Teil der UPDATEAnweisung waren, die Leistung erheblich beeinträchtigte, die sonst nicht vorhanden wäre, wenn diese Felder Teil von wärenUPDATEAnweisung (unabhängig davon, ob ihr Wert tatsächlich geändert wird). Warum funktioniert das alles (dh eine Abfrage zum Vergleichen von N Feldern über X Zeilen), um festzustellen, ob sich etwas geändert hat, wenn Sie logisch ausschließen können, dass eine dieser Spalten geändert wird, was offensichtlich nicht möglich ist, wenn sie nicht vorhanden sind in der SETKlausel der UPDATEErklärung.

Die Lösung, die ich verwendet habe, war die Verwendung der UPDATE () -Funktion, die nur innerhalb von Triggern funktioniert. Diese integrierte Funktion teilt Ihnen mit, ob in der UPDATEAnweisung eine Spalte angegeben wurde , und kann zum Beenden des Triggers verwendet werden, wenn die Spalten, um die Sie sich kümmern, nicht Teil der sind UPDATE. Dies kann in Verbindung mit a verwendet werden, SELECTum festzustellen, ob diese Spalten unter der Annahme, dass sie in der vorhanden UPDATEsind, tatsächliche Änderungen aufweisen. Ich habe Code oben in mehreren Audit-Triggern, der wie folgt aussieht:

-- exit on updates that do not update the only 3 columns we ETL
IF (
     EXISTS(SELECT 1 FROM DELETED) -- this is an UPDATE (Trigger is AFTER INSERT, UPDATE)
     AND (
            NOT (UPDATE(Column3) OR UPDATE(Column7)
                 OR UPDATE(Column11)) -- the columns we care about are not being updated
            OR NOT EXISTS(
                        SELECT 1
                        FROM INSERTED ins
                        INNER JOIN DELETED del
                                ON del.KeyField1 = ins.KeyField1
                                AND del.KeyField2 = ins.KeyField2
                        WHERE ins.Column3 <> del.Column3
                                 COLLATE Latin1_General_100_CS_AS -- case-sensitive compare
                        OR    ISNULL(ins.Column7, -99) <> 
                                 ISNULL(del.Column7, -99) -- NULLable INT field
                        OR    ins.[Column11] <> del.[Column11] -- NOT NULL INT field
                      )
          )
    )
BEGIN
    RETURN;
END;

Diese Logik fährt mit dem Rest des Triggers fort, wenn:

  1. Die Operation ist eine INSERT
  2. Mindestens eines der relevanten Felder befindet sich in der SETKlausel einer UPDATE und mindestens eine dieser Spalten in einer Zeile hat sich geändert

Das NOT (UPDATE...) OR NOT EXISTS()mag seltsam oder rückwärts aussehen, aber es soll verhindern, dass SELECTdie Tabellen on insertedund ausgeführt deletedwerden, wenn keine der relevanten Spalten Teil der Tabelle ist UPDATE.

Abhängig von Ihren Anforderungen ist die Funktion COLUMNS_UPDATED () eine weitere Option, um zu bestimmen, welche Spalten Teil der UPDATEAnweisung sind.

Solomon Rutzky
quelle
1
Guter Punkt, dass sie überprüfen UPDATE(CUSTCLAS)und einfach das Ganze überspringen sollten , wenn falsch (+1). Ich glaube nicht, dass Sie richtig liegen, dass nicht aktualisierte Spalten in den Zeilenversionen nicht so leicht verfügbar sind wie aktualisierte.
Martin Smith
@MartinSmith, wie gehen wir vor, um es auf die eine oder andere Weise zu beweisen? Es spielt jedoch möglicherweise keine Rolle, ob das Verhalten in der von mir gefundenen Weise vorhersehbar ist. Ich weiß nur, dass es sich um einen drastischen Leistungsunterschied handelt, der das gleiche SELECT-Verfahren zwischen INSERTED und DELETED ausführt und Felder auf tatsächliche Unterschiede überprüft, je nachdem, ob sich die Felder in WHERE im SET des UPDATE befanden oder nicht. Das Verhalten, das ich gesehen habe, ist konsistent, daher meine Theorie, aber es wäre gut / interessant, den wahren Grund zu kennen. Ich vermutete, dass Felder, die nicht im SET enthalten waren, für ihren Wert zur Basistabelle zurückkehren mussten.
Solomon Rutzky
Ich habe mir die Struktur davon schon einmal angesehen. Ich kann mich nicht erinnern, ob ich einen guten Weg gefunden habe, oder ich habe einfach eine leicht zu findende Zeichenfolge und eine erschöpfende Suche tempdbmitDBCC PAGE
Martin Smith
IN ORDNUNG. Bei einer Instanz mit einer einzelnen Datei mit minimaler Größe habe tempdbich gerade dieses Skript ausprobiert , die Ausgabe in den Editor eingefügt und nach "EEEEEE" gesucht. Ich sehe die Ausgabe im Screenshot hier . Beachten Sie vor und nach Versionen beider Spalten in beiden Zeilen. Es mag viel einfachere Wege geben, aber für meine Zwecke hier ausreichend!
Martin Smith
Obwohl es tatsächlich andere lange EEEEEE-Zeichenfolgen auf den tempdbSeiten gibt, die nicht neben BBBBBBoder stehen DDDDDD. Möglicherweise müssen noch weitere Untersuchungen durchgeführt werden! Vielleicht liegt das aber am REPLICATEAnruf.
Martin Smith
2

Ich könnte versuchen, mit zu schreiben, wenn vorhanden

IF EXISTS (SELECT TOP 1 i.CUSTNMBR     
            FROM INSERTED i         
            INNER JOIN DELETED d             
            ON i.CUSTNMBR = d.CUSTNMBR and d.custclass = 'Misc'  
            WHERE d.CUSTCLAS <>i.CUSTCLAS)    
BEGIN

--do your triggerstuff here
END
HLGEM
quelle
1

http://dave.brittens.org/blog/writing-well-behaved-triggers.html

Laut Dave sollten Sie temporäre Tabellen oder Tabellenvariablen mit Indizes verwenden, da die virtuellen INSERTED / DELETED-Tabellen keine haben. Wenn Sie die Möglichkeit rekursiver Trigger haben, sollten Sie Tabellenvariablen verwenden, um Namenskollisionen zu vermeiden.

Hoffe, jemand findet das hilfreich, da der ursprüngliche Beitrag vor einiger Zeit war ...

Keith
quelle
-1

Der folgende Code kann die Leistung dieses Triggers erhöhen. Ich kannte den richtigen Datentyp der Spalte [custclass] nicht , daher müssen Sie ihn anpassen.

DECLARE @i AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
DECLARE @d AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
INSERT INTO @i SELECT CUSTNMBR, custclass FROM inserted
INSERT INTO @d SELECT CUSTNMBR, custclass FROM deleted
IF NOT EXISTS
  (SELECT * FROM @i AS i INNER JOIN @d AS d ON d.CUSTNMBR = i.CUSTNMBR
   WHERE i.custclass <> d.custclass) RETURN

Beachten Sie, dass Sie zusätzliche Spalten in diese in Speicherkopien der eingefügten und gelöschten Tabellen aufnehmen können, wenn Sie diese in Ihrem Triggercode benötigen. Die Primärschlüssel in diesen Tabellen erhöhen die Join-Leistung erheblich, wenn mehr als einige Zeilen gleichzeitig aktualisiert werden. Viel Glück!

Dony
quelle