sql >> Database >  >> RDS >> Database

T-SQL dinsdag #64:één trigger of veel?

Het is die dinsdag van de maand – je weet wel, die ene waarop het blogger block-feest, bekend als T-SQL Tuesday, plaatsvindt. Deze maand wordt het gehost door Russ Thomas (@SQLJudo), en het onderwerp is, "Calling All Tuners and Gear Heads." Ik ga hier een prestatiegerelateerd probleem behandelen, hoewel mijn excuses dat het misschien niet volledig in overeenstemming is met de richtlijnen die Russ in zijn uitnodiging heeft uiteengezet (ik ga geen hints, traceervlaggen of planhandleidingen gebruiken) .

Bij SQLBits vorige week gaf ik een presentatie over triggers, en mijn goede vriend en mede-MVP Erland Sommarskog was toevallig aanwezig. Op een gegeven moment stelde ik voor dat je, voordat je een nieuwe trigger op een tafel maakt, moet controleren of er al triggers bestaan, en overwegen om de logica te combineren in plaats van een extra trigger toe te voegen. Mijn redenen waren voornamelijk voor de onderhoudbaarheid van de code, maar ook voor de prestaties. Erland vroeg of ik ooit had getest of er extra overhead was als meerdere triggers voor dezelfde actie werden geactiveerd, en ik moest toegeven dat, nee, ik had niets uitgebreids gedaan. Dus dat ga ik nu doen.

In AdventureWorks2014 heb ik een eenvoudige set tabellen gemaakt die in feite sys.all_objects vertegenwoordigen (~ 2.700 rijen) en sys.all_columns (~ 9.500 rijen). Ik wilde het effect meten op de werklast van verschillende benaderingen voor het bijwerken van beide tabellen - in wezen heb je gebruikers die de kolommentabel bijwerken en je gebruikt een trigger om een ​​andere kolom in dezelfde tabel en een paar kolommen in de objectentabel bij te werken.

  • T1:basislijn :Stel dat u alle gegevenstoegang kunt regelen via een opgeslagen procedure; in dit geval kunnen de updates voor beide tabellen direct worden uitgevoerd, zonder dat er triggers nodig zijn. (Dit is niet praktisch in de echte wereld, omdat je directe toegang tot de tabellen niet op betrouwbare wijze kunt verbieden.)
  • T2:enkele trigger tegen andere tafel :Stel dat u de update-instructie voor de betreffende tabel kunt beheren en andere kolommen kunt toevoegen, maar de updates voor de secundaire tabel moeten worden geïmplementeerd met een trigger. We werken alle drie de kolommen bij met één verklaring.
  • T3:enkele trigger voor beide tafels :In dit geval hebben we een trigger met twee instructies, een die de andere kolom in de betreffende tabel bijwerkt en een die alle drie de kolommen in de secundaire tabel bijwerkt.
  • T4:enkele trigger voor beide tafels :Net als T3, maar deze keer hebben we een trigger met vier instructies, een die de andere kolom in de betreffende tabel bijwerkt en een instructie voor elke kolom die is bijgewerkt in de secundaire tabel. Dit kan de manier zijn waarop het wordt afgehandeld als de vereisten in de loop van de tijd worden toegevoegd en een afzonderlijke verklaring als veiliger wordt beschouwd in termen van regressietesten.
  • T5:twee triggers :Eén trigger werkt alleen de betreffende tabel bij; de andere gebruikt een enkele instructie om de drie kolommen in de secundaire tabel bij te werken. Dit kan de manier zijn waarop het wordt gedaan als de andere triggers niet worden opgemerkt of als het wijzigen ervan verboden is.
  • T6:vier triggers :Eén trigger werkt alleen de betreffende tabel bij; de andere drie werken elke kolom in de secundaire tabel bij. Nogmaals, dit kan de manier zijn waarop het wordt gedaan als je niet weet dat de andere triggers bestaan, of als je bang bent om de andere triggers aan te raken vanwege regressieproblemen.

Dit zijn de brongegevens waarmee we te maken hebben:

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

Nu gaan we voor elk van de 6 tests onze updates 1000 keer uitvoeren en de tijdsduur meten

T1:basislijn

Dit is het scenario waarin we het geluk hebben om triggers te vermijden (wederom, niet erg realistisch). In dit geval meten we de uitlezingen en duur van deze batch. Ik plaats /*real*/ in de querytekst, zodat ik eenvoudig de statistieken voor alleen deze verklaringen kan trekken, en niet alle verklaringen van binnen de triggers, omdat uiteindelijk de statistieken oprollen naar de verklaringen die de triggers oproepen. Houd er ook rekening mee dat de daadwerkelijke updates die ik aan het maken ben niet echt logisch zijn, dus negeer dat ik de sortering instel op de naam van de server / instantie en de principal_id van het object naar de session_id . van de huidige sessie .

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:enkele trigger

Hiervoor hebben we de volgende eenvoudige trigger nodig, die alleen dbo.src . bijwerkt :

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

Dan hoeft onze batch alleen de twee kolommen in de primaire tabel bij te werken:

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

T3:enkele trigger tegen beide tafels

Voor deze test ziet onze trigger er als volgt uit:

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

En nu hoeft de batch die we testen alleen de originele kolom in de primaire tabel bij te werken; de andere wordt afgehandeld door de trigger:

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

T4:enkele trigger tegen beide tafels

Dit is net als T3, maar nu heeft de trigger vier statements:

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

De testbatch is ongewijzigd:

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

T5:Twee triggers

Hier hebben we één trigger om de primaire tabel bij te werken en één trigger om de secundaire tabel bij te werken:

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

De testbatch is weer erg basic:

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

T6:vier triggers

Deze keer hebben we een trigger voor elke kolom die wordt beïnvloed; één in de primaire tabel en drie in de secundaire tabellen.

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

En de testbatch:

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

De impact van de werkdruk meten

Ten slotte schreef ik een eenvoudige query tegen sys.dm_exec_query_stats meetwaarden en duur voor elke test meten:

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;

Resultaten

Ik heb de tests 10 keer uitgevoerd, de resultaten verzameld en alles gemiddeld. Hier is hoe het kapot ging:

Test/Batch Gemiddelde duur
(microseconden)
Totaal aantal gelezen
(8K pagina's)
T1 :UPDATE /*real*/ dbo.tr1 … 22.608 205.134
T2 :UPDATE /*real*/ dbo.tr2 … 32.749 11.331.628
T3 :UPDATE /*real*/ dbo.tr3 … 72.899 22.838.308
T4 :UPDATE /*real*/ dbo.tr4 … 78,372 44.463.275
T5 :UPDATE /*real*/ dbo.tr5 … 88,563 41.514.778
T6 :UPDATE /*real*/ dbo.tr6 … 127.079 100.330.753


En hier is een grafische weergave van de duur:

Conclusie

Het is duidelijk dat er in dit geval een aanzienlijke overhead is voor elke trigger die wordt aangeroepen - al deze batches hadden uiteindelijk invloed op hetzelfde aantal rijen, maar in sommige gevallen werden dezelfde rijen meerdere keren aangeraakt. Ik zal waarschijnlijk verdere vervolgtesten uitvoeren om het verschil te meten wanneer dezelfde rij nooit meer dan één keer wordt aangeraakt - een ingewikkelder schema misschien, waarbij elke keer 5 of 10 andere tabellen moeten worden aangeraakt, en deze verschillende uitspraken kunnen worden in een enkele trigger of in meerdere. Ik vermoed dat de overheadverschillen meer worden veroorzaakt door zaken als gelijktijdigheid en het aantal getroffen rijen dan door de overhead van de trigger zelf, maar we zullen zien.

Zelf de demo proberen? Download het script hier.


  1. Waarvoorwaarde voor samengevoegde tabel in Sequelize ORM

  2. Hoe u uw SQL Server-database documenteert?

  3. Fout:Kolom bestaat niet

  4. MariaDB FOUND_ROWS() uitgelegd