Database
 sql >> Database >  >> RDS >> Database

T-SQL Martedì #64:un trigger o molti?

È quel martedì del mese, sai, quello in cui si tiene il block party dei blogger noto come T-SQL Tuesday. Questo mese è ospitato da Russ Thomas (@SQLJudo) e l'argomento è "Calling All Tuners and Gear Heads". Tratterò qui un problema relativo alle prestazioni, anche se mi scuso per il fatto che potrebbe non essere completamente in linea con le linee guida che Russ ha stabilito nel suo invito (non userò suggerimenti, tracce di flag o guide di pianificazione) .

A SQLBits la scorsa settimana, ho tenuto una presentazione sui trigger e il mio buon amico e collega MVP Erland Sommarskog ha partecipato. A un certo punto ho suggerito che prima di creare un nuovo trigger su una tabella, dovresti verificare se esistono già trigger e considerare di combinare la logica invece di aggiungere un trigger aggiuntivo. Le mie ragioni erano principalmente per la manutenibilità del codice, ma anche per le prestazioni. Erland ha chiesto se avessi mai testato per vedere se c'era un sovraccarico aggiuntivo nell'avere più trigger sparati per la stessa azione, e ho dovuto ammettere che, no, non avevo fatto nulla di esteso. Quindi lo farò ora.

In AdventureWorks2014, ho creato un semplice insieme di tabelle che sostanzialmente rappresentano sys.all_objects (~2.700 righe) e sys.all_columns (~9.500 righe). Volevo misurare l'effetto sul carico di lavoro di vari approcci all'aggiornamento di entrambe le tabelle:in pratica hai utenti che aggiornano la tabella delle colonne e usi un trigger per aggiornare una colonna diversa nella stessa tabella e alcune colonne nella tabella degli oggetti.

  • T1:riferimento :si supponga di poter controllare tutti gli accessi ai dati tramite una procedura memorizzata; in questo caso, gli aggiornamenti su entrambe le tabelle possono essere eseguiti direttamente, senza necessità di trigger. (Questo non è pratico nel mondo reale, perché non puoi vietare in modo affidabile l'accesso diretto ai tavoli.)
  • T2:Singolo trigger contro un altro tavolo :si supponga di poter controllare l'istruzione di aggiornamento rispetto alla tabella interessata e aggiungere altre colonne, ma gli aggiornamenti alla tabella secondaria devono essere implementati con un trigger. Aggiorneremo tutte e tre le colonne con un'unica istruzione.
  • T3:Singolo trigger contro entrambi i tavoli :in questo caso, abbiamo un trigger con due istruzioni, una che aggiorna l'altra colonna nella tabella interessata e una che aggiorna tutte e tre le colonne nella tabella secondaria.
  • T4:Singolo trigger contro entrambi i tavoli :come T3, ma questa volta abbiamo un trigger con quattro istruzioni, una che aggiorna l'altra colonna nella tabella interessata e un'istruzione per ogni colonna aggiornata nella tabella secondaria. Questo potrebbe essere il modo in cui viene gestito se i requisiti vengono aggiunti nel tempo e una dichiarazione separata viene considerata più sicura in termini di test di regressione.
  • T5:due trigger :un trigger aggiorna solo la tabella interessata; l'altro usa una singola istruzione per aggiornare le tre colonne nella tabella secondaria. Questo potrebbe essere il modo in cui si fa se gli altri trigger non vengono notati o se è vietato modificarli.
  • T6:Quattro trigger :un trigger aggiorna solo la tabella interessata; gli altri tre aggiornano ogni colonna nella tabella secondaria. Anche in questo caso, potrebbe essere così se non sai che esistono altri trigger o se hai paura di toccare gli altri trigger a causa di problemi di regressione.

Ecco i dati di origine con cui abbiamo a che fare:

-- sys.all_objects:
SELECT * INTO dbo.src FROM sys.all_objects;
CREATE UNIQUE CLUSTERED INDEX x ON dbo.src([object_id]);
GO
 
-- sys.all_columns:
SELECT * INTO dbo.tr1 FROM sys.all_columns;
CREATE UNIQUE CLUSTERED INDEX x ON dbo.tr1([object_id], column_id);
-- repeat 5 times: tr2, tr3, tr4, tr5, tr6

Ora, per ciascuno dei 6 test, eseguiremo i nostri aggiornamenti 1000 volte e misureremo la durata

T1:riferimento

Questo è lo scenario in cui siamo abbastanza fortunati da evitare i trigger (di nuovo, non molto realistico). In questo caso, misureremo le letture e la durata di questo batch. Metto /*real*/ nel testo della query in modo che io possa facilmente estrarre le statistiche solo per queste istruzioni e non per qualsiasi istruzione dall'interno dei trigger, poiché alla fine le metriche si accumulano nelle istruzioni che invocano i trigger. Tieni inoltre presente che gli aggiornamenti effettivi che sto apportando non hanno alcun senso, quindi ignora che sto impostando le regole di confronto sul nome del server/istanza e del principal_id dell'oggetto al session_id della sessione corrente .

UPDATE /*real*/ dbo.tr1 SET name += N'',
  collation_name = @@SERVERNAME
  WHERE name LIKE '%s%';
 
UPDATE /*real*/ s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
  FROM dbo.src AS s
  INNER JOIN dbo.tr1 AS t
  ON s.[object_id] = t.[object_id]
  WHERE t.name LIKE '%s%';
 
GO 1000

T2:trigger singolo

Per questo abbiamo bisogno del seguente semplice trigger, che aggiorna solo dbo.src :

CREATE TRIGGER dbo.tr_tr2
ON dbo.tr2
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = SUSER_ID()
    FROM dbo.src AS s 
	INNER JOIN inserted AS i
	ON s.[object_id] = i.[object_id];
END
GO

Quindi il nostro batch deve solo aggiornare le due colonne nella tabella principale:

UPDATE /*real*/ dbo.tr2 SET name += N'', collation_name = @@SERVERNAME
  WHERE name LIKE '%s%';
GO 1000

T3:Singolo trigger contro entrambi i tavoli

Per questo test, il nostro trigger si presenta così:

CREATE TRIGGER dbo.tr_tr3
ON dbo.tr3
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr3 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
 
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

E ora il batch che stiamo testando deve semplicemente aggiornare la colonna originale nella tabella principale; l'altro è gestito dal trigger:

UPDATE /*real*/ dbo.tr3 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T4:Attivazione singola contro entrambi i tavoli

È proprio come T3, ma ora il trigger ha quattro istruzioni:

CREATE TRIGGER dbo.tr_tr4
ON dbo.tr4
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr4 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
 
  UPDATE s SET modify_date = GETDATE()
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
 
  UPDATE s SET is_ms_shipped = 0
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
 
  UPDATE s SET principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

Il lotto di prova è invariato:

UPDATE /*real*/ dbo.tr4 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T5:due trigger

Qui abbiamo un trigger per aggiornare la tabella primaria e un trigger per aggiornare la tabella secondaria:

CREATE TRIGGER dbo.tr_tr5_1
ON dbo.tr5
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr5 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr5_2
ON dbo.tr5
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

Il lotto di prova è ancora una volta molto semplice:

UPDATE /*real*/ dbo.tr5 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T6:Quattro trigger

Questa volta abbiamo un trigger per ogni colonna interessata; uno nella tabella principale e tre nelle tabelle secondarie.

CREATE TRIGGER dbo.tr_tr6_1
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr6 AS t
    INNER JOIN inserted AS i
    ON t.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_2
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE()
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_3
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET is_ms_shipped = 0
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_4
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

E il lotto di prova:

UPDATE /*real*/ dbo.tr6 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

Misurare l'impatto del carico di lavoro

Infine, ho scritto una semplice query su sys.dm_exec_query_stats per misurare le letture e la durata di ogni test:

SELECT 
  [cmd] = SUBSTRING(t.text, CHARINDEX(N'U', t.text), 23), 
  avg_elapsed_time = total_elapsed_time / execution_count * 1.0,
  total_logical_reads
FROM sys.dm_exec_query_stats AS s 
CROSS APPLY sys.dm_exec_sql_text(s.sql_handle) AS t
WHERE t.text LIKE N'%UPDATE /*real*/%'
ORDER BY cmd;

Risultati

Ho eseguito i test 10 volte, raccolto i risultati e fatto la media di tutto. Ecco come si è rotto:

Test/Lotto Durata media
(microsecondi)
Letture totali
(8.000 pagine)
T1 :AGGIORNAMENTO /*real*/ dbo.tr1 … 22.608 205.134
T2 :AGGIORNAMENTO /*real*/ dbo.tr2 … 32.749 11.331.628
T3 :AGGIORNAMENTO /*real*/ dbo.tr3 … 72.899 22.838.308
T4 :AGGIORNAMENTO /*real*/ dbo.tr4 … 78.372 44.463.275
T5 :AGGIORNAMENTO /*real*/ dbo.tr5 … 88.563 41.514.778
T6 :AGGIORNAMENTO /*real*/ dbo.tr6 … 127.079 100.330.753


Ed ecco una rappresentazione grafica della durata:

Conclusione

È chiaro che, in questo caso, c'è un sovraccarico sostanziale per ogni trigger che viene richiamato:tutti questi batch hanno in definitiva influenzato lo stesso numero di righe, ma in alcuni casi le stesse righe sono state toccate più volte. Probabilmente eseguirò ulteriori test successivi per misurare la differenza quando la stessa riga non viene mai toccata più di una volta – uno schema più complicato, forse, in cui 5 o 10 altre tabelle devono essere toccate ogni volta e queste diverse affermazioni potrebbero essere in un solo trigger o in più. La mia ipotesi è che le differenze di overhead saranno guidate più da cose come la concorrenza e il numero di righe interessate che dall'overhead del trigger stesso, ma vedremo.

Vuoi provare tu stesso la demo? Scarica lo script qui.