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

T-SQL tirsdag #64:Én trigger eller mange?

Det er den tirsdag i måneden – du ved, den, hvor bloggerblokfesten kendt som T-SQL Tuesday finder sted. I denne måned er den vært af Russ Thomas (@SQLJudo), og emnet er "Calling All Tuners and Gear Heads." Jeg har tænkt mig at behandle et præstationsrelateret problem her, selvom jeg undskylder, at det måske ikke er helt i overensstemmelse med de retningslinjer, Russ har angivet i hans invitation (jeg vil ikke bruge tip, sporingsflag eller planvejledninger) .

På SQLBits i sidste uge holdt jeg et oplæg om triggere, og min gode ven og kollega MVP Erland Sommarskog deltog tilfældigvis. På et tidspunkt foreslog jeg, at før du opretter en ny trigger på en tabel, skal du tjekke om der allerede findes nogen triggere, og overveje at kombinere logikken i stedet for at tilføje en ekstra trigger. Mine årsager var primært for kodevedligeholdelse, men også for ydeevne. Erland spurgte, om jeg nogensinde havde testet for at se, om der var ekstra overhead i at få flere triggere i gang for den samme handling, og jeg måtte indrømme, at nej, jeg havde ikke gjort noget omfattende. Så det vil jeg gøre nu.

I AdventureWorks2014 oprettede jeg et simpelt sæt tabeller, der grundlæggende repræsenterer sys.all_objects (~2.700 rækker) og sys.all_columns (~9.500 rækker). Jeg ønskede at måle effekten på arbejdsbyrden af ​​forskellige tilgange til opdatering af begge tabeller - i det væsentlige har du brugere, der opdaterer kolonnetabellen, og du bruger en trigger til at opdatere en anden kolonne i den samme tabel og et par kolonner i objekttabellen.

  • T1:Grundlinje :Antag, at du kan kontrollere al dataadgang gennem en lagret procedure; i dette tilfælde kan opdateringerne til begge tabeller udføres direkte uden behov for triggere. (Dette er ikke praktisk i den virkelige verden, fordi du ikke pålideligt kan forbyde direkte adgang til bordene.)
  • T2:Enkelt trigger mod anden tabel :Antag, at du kan styre opdateringssætningen mod den berørte tabel og tilføje andre kolonner, men opdateringerne til den sekundære tabel skal implementeres med en trigger. Vi opdaterer alle tre kolonner med én erklæring.
  • T3:Enkelt trigger mod begge tabeller :I dette tilfælde har vi en trigger med to sætninger, en der opdaterer den anden kolonne i den berørte tabel, og en der opdaterer alle tre kolonner i den sekundære tabel.
  • T4:Enkelt trigger mod begge tabeller :Ligesom T3, men denne gang har vi en trigger med fire sætninger, en der opdaterer den anden kolonne i den berørte tabel, og en sætning for hver kolonne opdateret i den sekundære tabel. Dette kan være måden, det håndteres på, hvis kravene tilføjes over tid, og en separat erklæring anses for at være mere sikker med hensyn til regressionstestning.
  • T5:To udløsere :En trigger opdaterer kun den berørte tabel; den anden bruger en enkelt sætning til at opdatere de tre kolonner i den sekundære tabel. Dette kan være måden, det gøres på, hvis de andre udløsere ikke bemærkes, eller hvis det er forbudt at ændre dem.
  • T6:Fire udløsere :En trigger opdaterer kun den berørte tabel; de tre andre opdaterer hver kolonne i den sekundære tabel. Igen, dette kan være måden, det gøres på, hvis du ikke ved, at de andre triggere eksisterer, eller hvis du er bange for at røre ved de andre triggere på grund af regressionsproblemer.

Her er de kildedata, vi har med at gøre:

-- 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, for hver af de 6 tests, vil vi køre vores opdateringer 1.000 gange og måle længden af ​​tid

T1:Baseline

Dette er scenariet, hvor vi er heldige nok til at undgå triggere (igen, ikke særlig realistisk). I dette tilfælde vil vi måle aflæsningerne og varigheden af ​​denne batch. Jeg sætter /*real*/ ind i forespørgselsteksten, så jeg nemt kan trække statistikken for netop disse udsagn, og ikke nogen udsagn inde fra triggerne, da metrics i sidste ende ruller op til de udsagn, der påkalder triggerne. Bemærk også, at de faktiske opdateringer, jeg laver, ikke rigtig giver nogen mening, så ignorer, at jeg indstiller sorteringen til server-/instansnavnet og objektets principal_id til den aktuelle sessions session_id .

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:Enkelt udløser

Til dette har vi brug for følgende simple trigger, som kun opdaterer 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

Så mangler vores batch kun at opdatere de to kolonner i den primære tabel:

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

T3:Enkelt trigger mod begge tabeller

Til denne test ser vores trigger sådan ud:

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

Og nu skal den batch, vi tester, blot opdatere den oprindelige kolonne i den primære tabel; den anden håndteres af triggeren:

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

T4:Enkelt trigger mod begge tabeller

Dette er ligesom T3, men nu har triggeren fire udsagn:

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

Testbatchen er uændret:

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

T5:To triggere

Her har vi en trigger til at opdatere den primære tabel, og en trigger til at opdatere den sekundære tabel:

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

Testbatchen er igen meget grundlæggende:

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

T6:Fire udløsere

Denne gang har vi en trigger for hver kolonne, der er påvirket; en i den primære tabel og tre i de sekundære tabeller.

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

Og testbatchen:

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

Måling af arbejdsbelastningspåvirkning

Til sidst skrev jeg en simpel forespørgsel mod sys.dm_exec_query_stats for at måle aflæsninger og varighed for hver 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;

Resultater

Jeg kørte testene 10 gange, indsamlede resultaterne og tog gennemsnittet af alt. Sådan gik det i stykker:

Test/batch Gennemsnitlig varighed
(mikrosekunder)
Læsninger i alt
(8.000 sider)
T1 :OPDATERING /*real*/ dbo.tr1 … 22.608 205.134
T2 :OPDATERING /*real*/ dbo.tr2 … 32.749 11.331.628
T3 :OPDATERING /*real*/ dbo.tr3 … 72.899 22.838.308
T4 :OPDATERING /*real*/ dbo.tr4 … 78.372 44.463.275
T5 :OPDATERING /*real*/ dbo.tr5 … 88.563 41.514.778
T6 :OPDATERING /*real*/ dbo.tr6 … 127.079 100.330.753


Og her er en grafisk fremstilling af varigheden:

Konklusion

Det er klart, at der i dette tilfælde er nogle betydelige overhead for hver trigger, der bliver påkaldt – alle disse batches påvirkede i sidste ende det samme antal rækker, men i nogle tilfælde blev de samme rækker berørt flere gange. Jeg vil sandsynligvis udføre yderligere opfølgningstest for at måle forskellen, når den samme række aldrig berøres mere end én gang – et mere kompliceret skema, måske, hvor 5 eller 10 andre tabeller skal røres hver gang, og disse forskellige udsagn kunne være i en enkelt trigger eller i flere. Mit gæt er, at overhead-forskellene i højere grad vil blive drevet af ting som samtidighed og antallet af rækker, der påvirkes, end af selve udløserens overhead - men vi må se.

Vil du selv prøve demoen? Download scriptet her.


  1. *ADVARSEL* Kan ikke åbne flere databasefejl med Microsoft Office build 2201

  2. 7 gratis værktøjer til databasediagram for travle datafolk

  3. SQL Server ROUND() Funktion:Hvad er det til, og hvorfor skal du bekymre dig?

  4. ListView Kontrol Træk-Drop Sorter begivenheder