Prestazioni atroci che uniscono tabelle INSERITE e CANCELLATE in un trigger


12

Ho un trigger UPDATE su una tabella che cerca una colonna specifica che cambia da un valore specifico a qualsiasi altro valore. Quando ciò accade, aggiorna alcuni dati correlati in un'altra tabella tramite una singola istruzione UPDATE.

La prima cosa che fa il trigger è verificare se le righe aggiornate hanno cambiato il valore di questa colonna rispetto al valore in questione. Unisce semplicemente INSERTED a DELETED e confronta il valore in quella colonna. Se nulla si qualifica, viene salvato in anticipo quindi l'istruzione UPDATE non viene eseguita.

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 questo caso, CUSTNMBR è la chiave primaria della tabella sottostante. Se eseguo un aggiornamento di grandi dimensioni su questa tabella (diciamo, oltre 5000 righe), questa istruzione prende AGES, anche se non ho toccato la colonna CUSTCLAS. Posso vederlo bloccato su questa affermazione per diversi minuti in Profiler.

Il piano di esecuzione è bizzarro. Mostra una scansione inserita con 3.714 esecuzioni e ~ 18,5 milioni di righe di output. Ciò attraversa un filtro nella colonna CUSTCLAS. Unisce questo (tramite loop nidificato) a una scansione eliminata (filtrata anche su CUSTCLAS), che viene eseguita una sola volta e ha 5000 righe di output.

Che cosa idiota sto facendo qui per causare questo? Si noti che il trigger deve assolutamente gestire correttamente gli aggiornamenti multi-riga.

MODIFICA :

Ho anche provato a scriverlo in questo modo (nel caso EXISTS stesse facendo qualcosa di spiacevole), ma è comunque altrettanto terribile.

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

Riesci a sbarazzarti del "TOP 1"? Penserei che stia causando un certo sovraccarico che potrebbe non essere necessario se stai solo controllando per vedere se c'è un singolo caso ...
JHFB

Risposte:


10

Potresti valutare usando espliciti INNER MERGE JOINo INNER HASH JOINsuggerimenti, ma dato che presumibilmente stai usando di nuovo queste tabelle più tardi nel trigger, probabilmente stai meglio semplicemente inserendo il contenuto insertede le deletedtabelle in #temptabelle indicizzate e facendo ciò.

Non ottengono automaticamente utili indici creati per loro.


Va bene, questo accelera enormemente, tuttavia c'è il potenziale per l'esecuzione a cascata del trigger. Se uso gli stessi nomi delle tabelle temporanee (#i, #d) in ogni trigger, sono in conflitto. Esiste una soluzione migliore / più sicura del semplice utilizzo di un nome di tabella temporanea diverso in ogni trigger?
db2,

Potrebbe valutare utilizzando le variabili di tabella (con una chiave primaria definita CUSTNMBRper creare l'indice cluster univoco) e utilizzare il OPTION (RECOMPILE)suggerimento per farlo in modo da tenere conto del numero di righe o forse semplicemente utilizzare una convenzione di denominazione particolare come#i_dbo_YourTable
Martin Smith

Penso che mi accontenterò di nominarli come #trigger_name_i. Se vado con le variabili di tabella, dovrò ingombrare ancora di più il codice con esplicite CREATE TABLE. Abbiamo trigger a cascata, ma non trigger ricorsivi, quindi penso che sarò al sicuro ...
db2

A questo scopo, consiglio una variabile table anziché una tabella temporanea; le variabili di tabella possono ancora avere indici primari e secondari (univoci), vengono automaticamente ripuliti quando il trigger viene chiuso e le variabili di tabella vengono impostate solo sull'esecuzione di quel trigger (non entrerà in conflitto con altre variabili di tabella con lo stesso nome più in alto o più in basso lo stack di chiamate). Per risparmiare sull'overhead del codice di definizione della tabella, definire un tipo di tabella per ciascuno e utilizzare il nome del tipo per dichiarare le variabili della tabella.
Chris Smith,

@ChrisSmith spesso ti servirà anche OPTION (RECOMPILE)per tenere conto della cardinalità.
Martin Smith,

10

So che è stata data una risposta, ma è apparso di recente attivo e mi sono imbattuto anche in questo per le tabelle con milioni di righe. Pur non scontando la risposta accettata, posso almeno aggiungere che la mia esperienza dimostra che un fattore chiave nelle prestazioni di trigger quando si eseguono test simili (vedere se una o più colonne hanno effettivamente cambiato i loro valori) è se le colonne sono o meno in fase di test facevano effettivamente parte UPDATEdell'affermazione. Ho scoperto che il confronto di colonne tra le tabelle insertede deletedche in realtà non facevano parte dell'istruzione ha comportatoUPDATE un enorme trascinamento delle prestazioni che altrimenti non sarebbero state presenti se quei campi facessero parte dellaUPDATEistruzione (indipendentemente dal fatto che il loro valore venga effettivamente modificato). Perché tutto ciò funziona (ad esempio una query per confrontare N campi su X righe) per determinare se qualcosa è cambiato se è possibile escludere logicamente la possibilità che una di quelle colonne venga modificata, il che ovviamente non è possibile se non fossero presenti nella SETclausola della UPDATEdichiarazione.

La soluzione che ho usato è stata quella di utilizzare la funzione UPDATE () che funziona solo all'interno di Trigger. Questa funzione integrata indica se è stata specificata una colonna UPDATEnell'istruzione e può essere utilizzata per uscire dal trigger se le colonne di cui si è preoccupati non fanno parte di UPDATE. Questo può essere usato insieme a a SELECTper determinare se quelle colonne, supponendo che siano presenti nel UPDATE, hanno delle modifiche effettive. Ho il codice nella parte superiore di diversi trigger di controllo che assomigliano a:

-- 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;

Questa logica procederà al resto del trigger se:

  1. L'operazione è un INSERT
  2. Almeno uno dei campi rilevanti è nella SETclausola di una UPDATE e almeno una di quelle colonne in una riga è cambiata

Il NOT (UPDATE...) OR NOT EXISTS()potrebbe apparire strano o indietro, ma è progettato per evitare di fare il SELECTsui insertede deletedtavoli se nessuna delle colonne rilevanti fanno parte del UPDATE.

A seconda delle esigenze, la funzione COLUMNS_UPDATED () è un'altra opzione per determinare quali colonne fanno parte dell'istruzioneUPDATE .


1
È bene che debbano controllare UPDATE(CUSTCLAS)e saltare il tutto se falso (+1). Non penso che tu abbia ragione nel dire che le colonne non aggiornate non sono così prontamente disponibili nelle versioni di riga come quelle aggiornate.
Martin Smith,

@MartinSmith, come possiamo provarlo in un modo o nell'altro? Anche se, potrebbe non importare se il comportamento è prevedibile nel modo che ho trovato. So solo che è una drastica differenza nelle prestazioni che fa lo stesso SELEZIONA, UNendosi tra INSERTO e CANCELLATO, controllando i campi per le differenze effettive, a seconda che i campi in WHERE fossero nel SET dell'aggiornamento o meno. Il comportamento che ho visto è coerente, quindi la mia teoria, ma sarebbe bello / interessante conoscere la vera ragione. Sospettavo che i campi non presenti nel SET dovessero tornare alla tabella di base per il loro valore.
Solomon Rutzky,

Ho già esaminato la struttura di questo prima. Non ricordo se ho trovato un buon modo di farlo o ho appena usato una stringa di trovare facilmente in grado e una ricerca esaustiva attraverso tempdbconDBCC PAGE
Martin Smith

OK. In un'istanza con un singolo file di dimensioni minime tempdbho appena provato questo script , incollato l'output nel blocco note e cercato "EEEEEE". Vedo l'output nello screenshot qui . Nota prima e dopo le versioni di entrambe le colonne in entrambe le righe. Potrebbero esserci modi molto più semplici ma sufficienti per i miei scopi qui!
Martin Smith,

Sebbene in realtà ci siano altre stringhe EEEEEE lunghe nelle tempdbpagine non accanto a BBBBBBo DDDDDD. Potrebbe essere necessario fare qualche indagine in più! Anche se forse questo è dovuto alla REPLICATEchiamata.
Martin Smith,

2

Potrei provare a riscrivere usando se esiste

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

1

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

Secondo Dave, è necessario utilizzare tabelle temporanee o variabili di tabella con indici, poiché le tabelle INSERTED / DELETED virtuali non ne hanno. Se hai la possibilità di trigger ricorsivi, allora dovresti usare le variabili di tabella per evitare collisioni di nomi.

Spero che qualcuno lo trovi utile dato che il post originale era un po 'di tempo fa ...


-1

Il codice seguente potrebbe aumentare le prestazioni di questo trigger. Non conoscevo il tipo di dati corretto della colonna [custclass] , quindi è necessario modificarlo.

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

Si noti che è possibile includere colonne aggiuntive in queste copie di memoria delle tabelle inserite ed eliminate se sono necessarie nel codice di attivazione. Le chiavi primarie su queste tabelle aumenteranno notevolmente le prestazioni dei join quando si aggiornano più righe contemporaneamente. In bocca al lupo!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.