2009-03-16 21 views
20

Il nostro sistema gira su SQL Server 2000 e stiamo preparando per un aggiornamento a SQL Server 2008. Abbiamo un sacco di codice di trigger dove dobbiamo rilevare una modifica in una determinata colonna e quindi operare su quella colonna se è cambiata.Il metodo più efficace per rilevare il cambio di colonna in MS SQL Server

Ovviamente SQL Server fornisce la UPDATE() e COLUMNS_UPDATED() funzioni, ma queste funzioni solo dirvi che le colonne sono stati implicati nell'istruzione SQL, non cui colonne hanno effettivamente cambiato.

per determinare le colonne sono cambiate, è necessario il codice simile al seguente (per una colonna che supporta valori null):

IF UPDATE(Col1) 
    SELECT @col1_changed = COUNT(*) 
    FROM Inserted i 
     INNER JOIN Deleted d ON i.Table_ID = d.Table_ID 
    WHERE ISNULL(i.Col1, '<unique null value>') 
      != ISNULL(i.Col1, '<unique null value>') 

Questo codice deve essere ripetuta per ogni colonna che vi interessa in fase di test. È quindi possibile controllare il valore 'cambiato' per determinare se eseguire o meno operazioni costose. Naturalmente, questo codice è di per sé problematico, in quanto ti dice solo che almeno un valore nella colonna è cambiato su tutte le righe che sono state modificate.

È possibile verificare le singole istruzioni UPDATE con qualcosa di simile:

UPDATE Table SET Col1 = CASE WHEN i.Col1 = d.Col1 
      THEN Col1 
      ELSE dbo.fnTransform(Col1) END 
FROM Inserted i 
    INNER JOIN Deleted d ON i.Table_ID = d.Table_ID 

... ma questo non funziona bene quando si ha bisogno di invocare una stored procedure. In questi casi, per quanto posso dire, devi ricorrere ad altri approcci.

La mia domanda è se qualcuno ha approfondimenti (o, meglio ancora, dati rigidi) su quale sia l'approccio migliore/più economico per il problema di prevedere un'operazione di database in un trigger sul fatto che un particolare valore di colonna in una riga modificata è effettivamente cambiato o no. Nessuno dei due metodi sopra sembra ideale e mi chiedevo se esistesse un metodo migliore.

+1

Ho aggiunto una nuova risposta a questa vecchia domanda che è rilevante: http://stackoverflow.com/questions/1254787/sql-server-update-trigger -get-only-modified-fields/8020461 # 8020461 –

+0

Questo è molto interessante, grazie per l'heads-up! – mwigdahl

risposta

7

Sebbene HLGEM abbia dato qualche buon consiglio sopra, non era esattamente quello di cui avevo bisogno. Ho fatto un bel po 'di test negli ultimi giorni, e ho pensato che avrei almeno condiviso i risultati qui dato che sembra che non ci saranno più informazioni in arrivo.

Ho impostato una tabella che era effettivamente un sottoinsieme più ristretto (9 colonne) di una delle tabelle principali del nostro sistema e l'ho popolata con i dati di produzione in modo che fosse profonda quanto la nostra versione di produzione della tabella.

Ho quindi duplicato quella tabella, e sul primo ho scritto un trigger che ha tentato di rilevare ogni singola modifica di colonna e quindi ha predicato ogni aggiornamento di colonna sul fatto che i dati in quella colonna fossero effettivamente cambiati o meno.

Per la seconda tabella, ho scritto un trigger che utilizzava una logica CASE condizionale estesa per eseguire tutti gli aggiornamenti di tutte le colonne in una singola istruzione.

Ho poi eseguito 4 test:

  1. un aggiornamento singola colonna in una singola riga
  2. Una colonna singola aggiornamento a 10000 righe
  3. Un aggiornamento nove colonne di una singola riga
  4. Un aggiornamento a nove colonne su 10000 righe

Ho ripetuto questo test per entrambe le versioni indicizzate e non indicizzate delle tabelle, quindi ripetuto Tutto su server SQL 2000 e SQL 2008.

I risultati che ho ottenuto erano piuttosto interessante:

Il secondo metodo (un'istruzione aggiornamento singola con logica CASE pelosa nella clausola SET) è uniformemente migliore rendimento rispetto al rilevamento cambiamento individuale (in misura maggiore o minore, a seconda del test) con la sola eccezione di una modifica a singola colonna che interessa molte righe in cui la colonna è stata indicizzata, in esecuzione su SQL 2000. Nel nostro caso particolare non facciamo molti aggiornamenti stretti e profondi come questo, quindi per i miei scopi l'approccio a singola affermazione è sicuramente la strada da seguire.


Sarei interessato a sentire i risultati di simili tipi di test, di altre persone per vedere se le mie conclusioni sono così universale come ho il sospetto che siano o se sono specifici per la particolare configurazione.

Per iniziare, qui è lo script di test che ho usato - avrete ovviamente bisogno di venire con altri dati per popolarlo con:

create table test1 
( 
    t_id int NOT NULL PRIMARY KEY, 
    i1 int NULL, 
    i2 int NULL, 
    i3 int NULL, 
    v1 varchar(500) NULL, 
    v2 varchar(500) NULL, 
    v3 varchar(500) NULL, 
    d1 datetime NULL, 
    d2 datetime NULL, 
    d3 datetime NULL 
) 

create table test2 
( 
    t_id int NOT NULL PRIMARY KEY, 
    i1 int NULL, 
    i2 int NULL, 
    i3 int NULL, 
    v1 varchar(500) NULL, 
    v2 varchar(500) NULL, 
    v3 varchar(500) NULL, 
    d1 datetime NULL, 
    d2 datetime NULL, 
    d3 datetime NULL 
) 

-- optional indexing here, test with it on and off... 
CREATE INDEX [IX_test1_i1] ON [dbo].[test1] ([i1]) 
CREATE INDEX [IX_test1_i2] ON [dbo].[test1] ([i2]) 
CREATE INDEX [IX_test1_i3] ON [dbo].[test1] ([i3]) 
CREATE INDEX [IX_test1_v1] ON [dbo].[test1] ([v1]) 
CREATE INDEX [IX_test1_v2] ON [dbo].[test1] ([v2]) 
CREATE INDEX [IX_test1_v3] ON [dbo].[test1] ([v3]) 
CREATE INDEX [IX_test1_d1] ON [dbo].[test1] ([d1]) 
CREATE INDEX [IX_test1_d2] ON [dbo].[test1] ([d2]) 
CREATE INDEX [IX_test1_d3] ON [dbo].[test1] ([d3]) 

CREATE INDEX [IX_test2_i1] ON [dbo].[test2] ([i1]) 
CREATE INDEX [IX_test2_i2] ON [dbo].[test2] ([i2]) 
CREATE INDEX [IX_test2_i3] ON [dbo].[test2] ([i3]) 
CREATE INDEX [IX_test2_v1] ON [dbo].[test2] ([v1]) 
CREATE INDEX [IX_test2_v2] ON [dbo].[test2] ([v2]) 
CREATE INDEX [IX_test2_v3] ON [dbo].[test2] ([v3]) 
CREATE INDEX [IX_test2_d1] ON [dbo].[test2] ([d1]) 
CREATE INDEX [IX_test2_d2] ON [dbo].[test2] ([d2]) 
CREATE INDEX [IX_test2_d3] ON [dbo].[test2] ([d3]) 

insert into test1 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3) 
-- add data population here... 

insert into test2 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3) 
select t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3 from test1 

go 

create trigger test1_update on test1 for update 
as 
begin 

declare @i1_changed int, 
    @i2_changed int, 
    @i3_changed int, 
    @v1_changed int, 
    @v2_changed int, 
    @v3_changed int, 
    @d1_changed int, 
    @d2_changed int, 
    @d3_changed int 

IF UPDATE(i1) 
    SELECT @i1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i1,0) != ISNULL(d.i1,0) 
IF UPDATE(i2) 
    SELECT @i2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i2,0) != ISNULL(d.i2,0) 
IF UPDATE(i3) 
    SELECT @i3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i3,0) != ISNULL(d.i3,0) 
IF UPDATE(v1) 
    SELECT @v1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v1,'') != ISNULL(d.v1,'') 
IF UPDATE(v2) 
    SELECT @v2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v2,'') != ISNULL(d.v2,'') 
IF UPDATE(v3) 
    SELECT @v3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v3,'') != ISNULL(d.v3,'') 
IF UPDATE(d1) 
    SELECT @d1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d1,'1/1/1980') != ISNULL(d.d1,'1/1/1980') 
IF UPDATE(d2) 
    SELECT @d2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d2,'1/1/1980') != ISNULL(d.d2,'1/1/1980') 
IF UPDATE(d3) 
    SELECT @d3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d3,'1/1/1980') != ISNULL(d.d3,'1/1/1980') 

if (@i1_changed > 0) 
begin 
    UPDATE test1 SET i1 = CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i1 != d.i1 
end 

if (@i2_changed > 0) 
begin 
    UPDATE test1 SET i2 = CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i2 != d.i2 
end 

if (@i3_changed > 0) 
begin 
    UPDATE test1 SET i3 = i.i3^d.i3 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i3 != d.i3 
end 

if (@v1_changed > 0) 
begin 
    UPDATE test1 SET v1 = i.v1 + 'a' 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.v1 != d.v1 
end 

UPDATE test1 SET v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5) 
FROM test1 
    INNER JOIN inserted i ON test1.t_id = i.t_id 
    INNER JOIN deleted d ON i.t_id = d.t_id 

if (@v3_changed > 0) 
begin 
    UPDATE test1 SET v3 = LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.v3 != d.v3 
end 

if (@d1_changed > 0) 
begin 
    UPDATE test1 SET d1 = DATEADD(dd, 1, i.d1) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.d1 != d.d1 
end 

if (@d2_changed > 0) 
begin 
    UPDATE test1 SET d2 = DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.d2 != d.d2 
end 

UPDATE test1 SET d3 = DATEADD(dd, 15, i.d3) 
FROM test1 
    INNER JOIN inserted i ON test1.t_id = i.t_id 
    INNER JOIN deleted d ON i.t_id = d.t_id 

end 

go 

create trigger test2_update on test2 for update 
as 
begin 

    UPDATE test2 SET 
     i1 = 
      CASE 
      WHEN ISNULL(i.i1, 0) != ISNULL(d.i1, 0) 
      THEN CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END 
      ELSE test2.i1 END, 
     i2 = 
      CASE 
      WHEN ISNULL(i.i2, 0) != ISNULL(d.i2, 0) 
      THEN CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END 
      ELSE test2.i2 END, 
     i3 = 
      CASE 
      WHEN ISNULL(i.i3, 0) != ISNULL(d.i3, 0) 
      THEN i.i3^d.i3 
      ELSE test2.i3 END, 
     v1 = 
      CASE 
      WHEN ISNULL(i.v1, '') != ISNULL(d.v1, '') 
      THEN i.v1 + 'a' 
      ELSE test2.v1 END, 
     v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5), 
     v3 = 
      CASE 
      WHEN ISNULL(i.v3, '') != ISNULL(d.v3, '') 
      THEN LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5) 
      ELSE test2.v3 END, 
     d1 = 
      CASE 
      WHEN ISNULL(i.d1, '1/1/1980') != ISNULL(d.d1, '1/1/1980') 
      THEN DATEADD(dd, 1, i.d1) 
      ELSE test2.d1 END, 
     d2 = 
      CASE 
      WHEN ISNULL(i.d2, '1/1/1980') != ISNULL(d.d2, '1/1/1980') 
      THEN DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2) 
      ELSE test2.d2 END, 
     d3 = DATEADD(dd, 15, i.d3) 
    FROM test2 
     INNER JOIN inserted i ON test2.t_id = i.t_id 
     INNER JOIN deleted d ON test2.t_id = d.t_id 

end 

go 

----- 
-- the below code can be used to confirm that the triggers operated identically over both tables after a test 
select top 10 test1.i1, test2.i1, test1.i2, test2.i2, test1.i3, test2.i3, test1.v1, test2.v1, test1.v2, test2.v2, test1.v3, test2.v3, test1.d1, test1.d1, test1.d2, test2.d2, test1.d3, test2.d3 
from test1 inner join test2 on test1.t_id = test2.t_id 
where 
    test1.i1 != test2.i1 or 
    test1.i2 != test2.i2 or 
    test1.i3 != test2.i3 or 
    test1.v1 != test2.v1 or 
    test1.v2 != test2.v2 or 
    test1.v3 != test2.v3 or 
    test1.d1 != test2.d1 or 
    test1.d2 != test2.d2 or 
    test1.d3 != test2.d3 

-- test 1 -- one column, one row 
update test1 set i3 = 64 where t_id = 1000 
go 
update test2 set i3 = 64 where t_id = 1000 
go 

update test1 set i3 = 64 where t_id = 1001 
go 
update test2 set i3 = 64 where t_id = 1001 
go 

-- test 2 -- one column, 10000 rows 
update test1 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000 
go 
update test2 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000 
go 

-- test 3 -- all columns, 1 row, non-self-referential 
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id = 3000 
go 
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id = 3000 
go 

-- test 4 -- all columns, 10000 rows, non-self-referential 
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id between 30000 and 40000 
go 
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id between 30000 and 40000 
go 

----- 

drop table test1 
drop table test2 
16

Iniziamo con non vorrei mai e non intendo mai invocare un processo memorizzato in un trigger. Per tenere conto di un inserimento di più righe, devi passare attraverso il proc. Ciò significa che le 200.000 righe appena caricate, anche se una query basata su set (ad esempio aggiornando tutti i prezzi del 10%), potrebbero bloccare la tabella per ore mentre il trigger tenta valorosamente di gestire il carico. Inoltre, se qualcosa cambia nel proc, è possibile interrompere qualsiasi inserimento nel tavolo o addirittura riagganciare completamente il tavolo. Sono fermamente convinto che il codice di attivazione non debba chiamare nient'altro al di fuori del trigger.

Personalmente preferisco semplicemente svolgere il mio compito. Se ho scritto le azioni che voglio fare correttamente nel trigger, aggiornerà, cancellerà o inserirà solo dove le colonne sono cambiate.

Esempio: si supponga di voler aggiornare il campo last_name che si sta memorizzando in due punti a causa di una denormalizzazione posta lì per motivi di prestazioni.

update t 
set lname = i.lname 
from table2 t 
join inserted i on t.fkfield = i.pkfield 
where t.lname <>i.lname 

Come si può vedere che sarebbe aggiornare solo le lnames che sono diverso da quello che è attualmente nella tabella sto aggiornando.

Se si vuole fare auditing e registrare solo le righe che hanno cambiato poi fare il confronto con tutti i campi in cui qualcosa come i.field1 <> d.field1 o i.field2 <> d.field3 (ecc attraverso tutte le campi)

+0

Nel caso in cui si presenti, si finirebbe per bloccare table2 per l'aggiornamento per ogni modifica apportata alla tabella originale, anche se non si è mai modificato affatto il nome. Questo è parte di ciò che sto cercando di evitare. Grazie per il consiglio, però! – mwigdahl

+1

Ho votato perché ho scoperto il modo difficile per non chiamare SP da trigger ... Mai più! – RolandTumble

9

penso che si potrebbe voler indagare utilizzando l'operatore ad eccezione. È un operatore basato su set che può estirpare le righe che non sono state modificate. La cosa bella è che considera i valori nulli come uguali come sembra per le righe nel primo set elencati prima l'operatore Tranne e non nella seconda elencato dopo except

WITH ChangedData AS (
SELECT d.Table_ID , d.Col1 FROM deleted d 
EXCEPT 
SELECT i.Table_ID , i.Col1 FROM inserted i 
) 
/*Do Something with the ChangedData */ 

Questo gestisce l'emissione di colonne che permettono Null senza l'uso di ISNULL() nel trigger e restituisce solo gli ID delle righe con le modifiche a col1 per un approccio basato su set piacevole per rilevare le modifiche. Non ho testato l'approccio, ma potrebbe valere la pena. Penso che EXCEPT sia stato introdotto con SQL Server 2005.

+0

Investigherò su questo, grazie! – mwigdahl

+0

Sto usando questo metodo abbastanza nel mio database e sebbene non abbia misurato le prestazioni sembra abbastanza veloce. Non ho misurato perché non vedo un successo in termini di prestazioni. A proposito, la mia logica inserisce l'INSERT prima di EXCEPT e gestisce il rilevamento delle modifiche INSERT e UPDATE. A proposito, non ho usato la frase "WITH" ma sembra interessante. Vedi la mia risposta leggermente diversa qui sotto. –

5

Si consiglia di utilizzare l'operatore di selezione EXCEPT come indicato da Todd/arghtype sopra.

Ho aggiunto questa risposta perché inserisco "inserito" prima di "eliminato" in modo che vengano rilevati INSERT e UPDATE. Quindi di solito posso avere un trigger per coprire sia gli inserti che gli aggiornamenti. È inoltre possibile rilevare le eliminazioni aggiungendo OR (NON ESISTE (SELECT * FROM inserito) E EXISTS (SELECT * FROM cancellato))

Determina se un valore è stato modificato solo nelle colonne specificate. Non ho studiato le sue prestazioni rispetto alle altre soluzioni, ma sta funzionando bene nel mio database.

Utilizza l'operatore set EXCEPT per restituire qualsiasi riga dalla query di sinistra che non si trova anche nella query corretta. Questo codice può essere utilizzato nei trigger INSERT, UPDATE e DELETE.

La colonna "PKID" è la chiave primaria. È necessario abilitare la corrispondenza tra i due set. Se si dispone di più colonne per la chiave primaria, sarà necessario includere tutte le colonne per fare la corrispondenza corretta tra i set inseriti e cancellati.

-- Only do trigger logic if specific field values change. 
IF EXISTS(SELECT PKID 
       ,Column1 
       ,Column7 
       ,Column10 
      FROM inserted 
      EXCEPT 
      SELECT PKID 
       ,Column1 
       ,Column7 
       ,Column10 
      FROM deleted) -- Tests for modifications to fields that we are interested in 
OR (NOT EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)) -- Have a deletion 
BEGIN 
      -- Put code here that does the work in the trigger 

END 

Se si desidera utilizzare le righe modificate nella logica di trigger successivo, Io di solito mettere i risultati della query eccetto in una variabile di tabella che è possibile fare riferimento in seguito.

Spero che questo è di interesse :-)

Problemi correlati