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

Begrundelse for I STEDET FOR triggere – del 1

Sidste år postede jeg et tip kaldet Forbedre SQL Server Efficiency ved at skifte til I STEDET FOR Triggere.

Den store grund til, at jeg har en tendens til at foretrække en ISTEDEN FOR trigger, især i tilfælde, hvor jeg forventer en masse forretningslogikovertrædelser, er, at det virker intuitivt, at det ville være billigere at forhindre en handling helt, end at gå videre og udføre den (og log det!), kun for at bruge en AFTER-trigger til at slette de stødende rækker (eller rulle hele handlingen tilbage). Resultaterne vist i det tip viste, at dette faktisk var tilfældet – og jeg formoder, at de ville være endnu mere udtalte med flere ikke-klyngede indekser påvirket af operationen.

Det var dog på en langsom disk og på en tidlig CTP af SQL Server 2014. Da jeg forberedte et dias til en ny præsentation, jeg vil lave i år på triggere, fandt jeg ud af, at på en nyere version af SQL Server 2014 – kombineret med opdateret hardware – det var lidt sværere at demonstrere det samme delta i ydeevne mellem en EFTER og I STEDET FOR trigger. Så jeg satte mig for at finde ud af hvorfor, selvom jeg med det samme vidste, at dette ville blive mere arbejde, end jeg nogensinde har gjort for et enkelt dias.

En ting, jeg vil nævne, er, at triggere kan bruge tempdb på forskellige måder, og dette kan forklare nogle af disse forskelle. En AFTER-trigger bruger versionslagret til de indsatte og slettede pseudo-tabeller, mens en INSTEAD OF-trigger laver en kopi af disse data i en intern arbejdstabel. Forskellen er subtil, men værd at påpege.

Variablerne

Jeg vil teste forskellige scenarier, herunder:

  • Tre forskellige udløsere:
    • En AFTER-udløser, der sletter bestemte rækker, der fejler
    • En AFTER-trigger, der ruller hele transaktionen tilbage, hvis en række mislykkes
    • En I STEDET FOR trigger, der kun indsætter rækker, der passerer
  • Forskellige gendannelsesmodeller og indstillinger for snapshot-isolering:
    • FULD med SNAPSHOT aktiveret
    • FULD med SNAPSHOT deaktiveret
    • ENKELT med SNAPSHOT aktiveret
    • SIMPEL med SNAPSHOT deaktiveret
  • Forskellige disklayouts*:
    • Data på SSD, log på 7200 RPM HDD
    • Data på SSD, log på SSD
    • Data på 7200 RPM HDD, log på SSD
    • Data på 7200 RPM HDD, log på 7200 RPM HDD
  • Forskellige fejlfrekvenser:
    • 10 %, 25 % og 50 % fejlrate på tværs af:
      • Enkelt batchindlæg på 20.000 rækker
      • 10 batches af 2.000 rækker
      • 100 batches af 200 rækker
      • 1.000 batches af 20 rækker
      • 20.000 singleton skær

    * tempdb er en enkelt datafil på en langsom 7200 RPM disk. Dette er bevidst og beregnet til at forstærke eventuelle flaskehalse forårsaget af de forskellige anvendelser af tempdb . Jeg planlægger at gense denne test på et tidspunkt, når tempdb er på en hurtigere SSD.

Okay, TL;DR Allerede!

Hvis du bare vil vide resultaterne, så spring ned. Alt i midten er kun baggrund og en forklaring på, hvordan jeg satte op og kørte testene. Jeg er ikke knust over, at ikke alle vil være interesserede i alle detaljerne.

Scenariet

For dette særlige sæt af tests er det virkelige scenarie et, hvor en bruger vælger et skærmnavn, og triggeren er designet til at fange tilfælde, hvor det valgte navn overtræder nogle regler. Det kan for eksempel ikke være nogen variant af "ninny-muggins" (du kan bestemt bruge din fantasi her).

Jeg oprettede en tabel med 20.000 unikke brugernavne:

USE model;
GO
 
-- 20,000 distinct, good Names
;WITH distinct_Names AS
(
  SELECT Name FROM sys.all_columns
  UNION 
  SELECT Name FROM sys.all_objects
)
SELECT TOP (20000) Name 
INTO dbo.GoodNamesSource
FROM
(
  SELECT Name FROM distinct_Names
  UNION 
  SELECT Name + 'x' FROM distinct_Names
  UNION 
  SELECT Name + 'y' FROM distinct_Names
  UNION 
  SELECT Name + 'z' FROM distinct_Names
) AS x;
 
CREATE UNIQUE CLUSTERED INDEX x ON dbo.GoodNamesSource(Name);

Så lavede jeg en tabel, der ville være kilden for mine "frække navne" at tjekke op imod. I dette tilfælde er det bare ninny-muggins-00001 gennem ninny-muggins-10000 :

USE model;
GO
 
CREATE TABLE dbo.NaughtyUserNames
(
  Name NVARCHAR(255) PRIMARY KEY
);
GO
 
-- 10,000 "bad" names
INSERT dbo.NaughtyUserNames(Name)
  SELECT N'ninny-muggins-' + RIGHT(N'0000' + RTRIM(n),5)
  FROM
  (
    SELECT TOP (10000) n = ROW_NUMBER() OVER (ORDER BY Name)
	FROM dbo.GoodNamesSource
  ) AS x;

Jeg oprettede disse tabeller i model database, så hver gang jeg opretter en database, ville den eksistere lokalt, og jeg planlægger at oprette en masse databaser for at teste scenariematricen ovenfor (i stedet for blot at ændre databaseindstillinger, rydde loggen osv.). Bemærk venligst, at hvis du opretter objekter i modellen til testformål, skal du sørge for at slette disse objekter, når du er færdig.

Som en sidebemærkning vil jeg med vilje lade nøgleovertrædelser og anden fejlhåndtering ude af dette, hvilket gør den naive antagelse, at det valgte navn kontrolleres for unikhed længe før indsættelsen nogensinde forsøges, men inden for den samme transaktion (ligesom check mod den frække navnetabel kunne være lavet på forhånd).

For at understøtte dette oprettede jeg også følgende tre næsten identiske tabeller i model , til testisoleringsformål:

USE model;
GO
 
 
-- AFTER (rollback)
CREATE TABLE dbo.UserNames_After_Rollback
(
  UserID INT IDENTITY(1,1) PRIMARY KEY,
  Name NVARCHAR(255) NOT NULL UNIQUE,
  DateCreated DATE NOT NULL DEFAULT SYSDATETIME()
);
CREATE INDEX x ON dbo.UserNames_After_Rollback(DateCreated) INCLUDE(Name);
 
 
-- AFTER (delete)
CREATE TABLE dbo.UserNames_After_Delete
(
  UserID INT IDENTITY(1,1) PRIMARY KEY,
  Name NVARCHAR(255) NOT NULL UNIQUE,
  DateCreated DATE NOT NULL DEFAULT SYSDATETIME()
);
CREATE INDEX x ON dbo.UserNames_After_Delete(DateCreated) INCLUDE(Name);
 
 
-- INSTEAD
CREATE TABLE dbo.UserNames_Instead
(
  UserID INT IDENTITY(1,1) PRIMARY KEY,
  Name NVARCHAR(255) NOT NULL UNIQUE,
  DateCreated DATE NOT NULL DEFAULT SYSDATETIME()
);
CREATE INDEX x ON dbo.UserNames_Instead(DateCreated) INCLUDE(Name);
GO

Og de følgende tre udløsere, en for hver tabel:

USE model;
GO
 
 
-- AFTER (rollback)
CREATE TRIGGER dbo.trUserNames_After_Rollback
ON dbo.UserNames_After_Rollback
AFTER INSERT
AS
BEGIN
  SET NOCOUNT ON;
 
  IF EXISTS 
  (
   SELECT 1 FROM inserted AS i
    WHERE EXISTS
    (
      SELECT 1 FROM dbo.NaughtyUserNames
      WHERE Name = i.Name
    )
  )
  BEGIN
    ROLLBACK TRANSACTION;
  END
END
GO
 
 
-- AFTER (delete)
CREATE TRIGGER dbo.trUserNames_After_Delete
ON dbo.UserNames_After_Delete
AFTER INSERT
AS
BEGIN
  SET NOCOUNT ON;
 
  DELETE d
    FROM inserted AS i
    INNER JOIN dbo.NaughtyUserNames AS n
    ON i.Name = n.Name
    INNER JOIN dbo.UserNames_After_Delete AS d
    ON i.UserID = d.UserID;
END
GO
 
 
-- INSTEAD
CREATE TRIGGER dbo.trUserNames_Instead
ON dbo.UserNames_Instead
INSTEAD OF INSERT
AS
BEGIN
  SET NOCOUNT ON;
 
  INSERT dbo.UserNames_Instead(Name)
    SELECT i.Name
      FROM inserted AS i
      WHERE NOT EXISTS
      (
        SELECT 1 FROM dbo.NaughtyUserNames
        WHERE Name = i.Name
      );
END
GO

Du vil sandsynligvis overveje yderligere håndtering for at underrette brugeren om, at deres valg blev rullet tilbage eller ignoreret – men dette er også udeladt for nemheds skyld.

Testopsætningen

Jeg oprettede eksempeldata, der repræsenterede de tre fejlrater, jeg ville teste, ændrede 10 procent til 25 og derefter 50, og tilføjede også disse tabeller til model :

USE model;
GO
 
DECLARE @pct INT = 10, @cap INT = 20000;
-- change this ----^^ to 25 and 50
 
DECLARE @good INT = @cap - (@cap*(@pct/100.0));
 
SELECT Name, rn = ROW_NUMBER() OVER (ORDER BY NEWID()) 
INTO dbo.Source10Percent FROM 
-- change this ^^ to 25 and 50
(
  SELECT Name FROM 
  (
    SELECT TOP (@good) Name FROM dbo.GoodNamesSource ORDER BY NEWID()
  ) AS g
  UNION ALL
  SELECT Name FROM 
  (
    SELECT TOP (@cap-@good) Name FROM dbo.NaughtyUserNames ORDER BY NEWID()
  ) AS b
) AS x;
 
CREATE UNIQUE CLUSTERED INDEX x ON dbo.Source10Percent(rn);
-- and here as well -------------------------^^

Hver tabel har 20.000 rækker, med en forskellig blanding af navne, der vil bestå og fejle, og rækkenummerkolonnen gør det nemt at opdele dataene i forskellige batchstørrelser for forskellige tests, men med gentagelige fejlfrekvenser for alle testene.

Selvfølgelig har vi brug for et sted at fange resultaterne. Jeg valgte at bruge en separat database til dette, hvor jeg kørte hver test flere gange, blot registrerede varigheden.

CREATE DATABASE ControlDB;
GO
 
USE ControlDB;
GO
 
CREATE TABLE dbo.Tests
(
  TestID        INT, 
  DiskLayout    VARCHAR(15),
  RecoveryModel VARCHAR(6),
  TriggerType   VARCHAR(14),
  [snapshot]    VARCHAR(3),
  FailureRate   INT,
  [sql]         NVARCHAR(MAX)
);
 
CREATE TABLE dbo.TestResults
(
  TestID INT,
  BatchDescription VARCHAR(15),
  Duration INT
);

Jeg udfyldte dbo.Tests tabel med følgende script, så jeg kunne udføre forskellige dele for at sætte de fire databaser op, så de matcher de aktuelle testparametre. Bemærk, at D:\ er en SSD, mens G:\ er en 7200 RPM disk:

TRUNCATE TABLE dbo.Tests;
TRUNCATE TABLE dbo.TestResults;
 
;WITH d AS 
(
  SELECT DiskLayout FROM (VALUES
    ('DataSSD_LogHDD'),
    ('DataSSD_LogSSD'),
    ('DataHDD_LogHDD'),
    ('DataHDD_LogSSD')) AS d(DiskLayout)
),
t AS 
(
  SELECT TriggerType FROM (VALUES
  ('After_Delete'),
  ('After_Rollback'),
  ('Instead')) AS t(TriggerType)
),
m AS 
(
  SELECT RecoveryModel = 'FULL' 
      UNION ALL SELECT 'SIMPLE'
),
s AS 
(
  SELECT IsSnapshot = 0 
      UNION ALL SELECT 1
),
p AS 
(
  SELECT FailureRate = 10 
      UNION ALL SELECT 25 
	  UNION ALL SELECT 50
)
INSERT ControlDB.dbo.Tests
(
  TestID, 
  DiskLayout, 
  RecoveryModel, 
  TriggerType, 
  IsSnapshot, 
  FailureRate, 
  Command
)
SELECT 
  TestID = ROW_NUMBER() OVER 
  (
    ORDER BY d.DiskLayout, t.TriggerType, m.RecoveryModel, s.IsSnapshot, p.FailureRate
  ),
  d.DiskLayout, 
  m.RecoveryModel, 
  t.TriggerType, 
  s.IsSnapshot, 
  p.FailureRate, 
  [sql]= N'SET NOCOUNT ON;
 
CREATE DATABASE ' + QUOTENAME(d.DiskLayout) 
 + N' ON (name = N''data'', filename = N''' + CASE d.DiskLayout 
WHEN 'DataSSD_LogHDD' THEN N'D:\data\data1.mdf'') 
  LOG ON (name = N''log'', filename = N''G:\log\data1.ldf'');'
WHEN 'DataSSD_LogSSD' THEN N'D:\data\data2.mdf'') 
  LOG ON (name = N''log'', filename = N''D:\log\data2.ldf'');'
WHEN 'DataHDD_LogHDD' THEN N'G:\data\data3.mdf'') 
  LOG ON (name = N''log'', filename = N''G:\log\data3.ldf'');'
WHEN 'DataHDD_LogSSD' THEN N'G:\data\data4.mdf'') 
  LOG ON (name = N''log'', filename = N''D:\log\data4.ldf'');' END
+ '
EXEC sp_executesql N''ALTER DATABASE ' + QUOTENAME(d.DiskLayout) 
  + ' SET RECOVERY ' + m.RecoveryModel + ';'';'
+ CASE WHEN s.IsSnapshot = 1 THEN 
'
EXEC sp_executesql N''ALTER DATABASE ' + QUOTENAME(d.DiskLayout) 
  + ' SET ALLOW_SNAPSHOT_ISOLATION ON;'';
EXEC sp_executesql N''ALTER DATABASE ' + QUOTENAME(d.DiskLayout) 
  + ' SET READ_COMMITTED_SNAPSHOT ON;'';' 
ELSE '' END
+ '
 
DECLARE @d DATETIME2(7), @i INT, @LoopID INT, @loops INT, @perloop INT;
 
DECLARE c CURSOR LOCAL FAST_FORWARD FOR
  SELECT LoopID, loops, perloop FROM dbo.Loops; 
 
OPEN c;
 
FETCH c INTO @LoopID, @loops, @perloop;
 
WHILE @@FETCH_STATUS <> -1
BEGIN
  EXEC sp_executesql N''TRUNCATE TABLE ' 
    + QUOTENAME(d.DiskLayout) + '.dbo.UserNames_' + t.TriggerType + ';'';
 
  SELECT @d = SYSDATETIME(), @i = 1;
 
  WHILE @i <= @loops
  BEGIN
    BEGIN TRY
      INSERT ' + QUOTENAME(d.DiskLayout) + '.dbo.UserNames_' + t.TriggerType + '(Name)
        SELECT Name FROM ' + QUOTENAME(d.DiskLayout) + '.dbo.Source' + RTRIM(p.FailureRate) + 'Percent
	    WHERE rn > (@i-1)*@perloop AND rn <= @i*@perloop;
    END TRY
    BEGIN CATCH
      SET @TestID = @TestID;
    END CATCH
 
    SET @i += 1;
  END
 
  INSERT ControlDB.dbo.TestResults(TestID, LoopID, Duration)
    SELECT @TestID, @LoopID, DATEDIFF(MILLISECOND, @d, SYSDATETIME());
 
  FETCH c INTO @LoopID, @loops, @perloop;
END
 
CLOSE c;
DEALLOCATE c;
 
DROP DATABASE ' + QUOTENAME(d.DiskLayout) + ';'
FROM d, t, m, s, p;  -- implicit CROSS JOIN! Do as I say, not as I do! :-)

Så var det nemt at køre alle testene flere gange:

USE ControlDB;
GO
 
SET NOCOUNT ON;
 
DECLARE @TestID INT, @Command NVARCHAR(MAX), @msg VARCHAR(32);
 
DECLARE d CURSOR LOCAL FAST_FORWARD FOR 
  SELECT TestID, Command
    FROM ControlDB.dbo.Tests ORDER BY TestID;
 
OPEN d;
 
FETCH d INTO @TestID, @Command;
 
WHILE @@FETCH_STATUS <> -1
BEGIN
  SET @msg = 'Starting ' + RTRIM(@TestID);
  RAISERROR(@msg, 0, 1) WITH NOWAIT;
 
  EXEC sp_executesql @Command, N'@TestID INT', @TestID;
 
  SET @msg = 'Finished ' + RTRIM(@TestID);
  RAISERROR(@msg, 0, 1) WITH NOWAIT;
 
  FETCH d INTO @TestID, @Command;
END
 
CLOSE d;
DEALLOCATE d;
 
GO 10

På mit system tog dette tæt på 6 timer, så vær forberedt på at lade dette køre uafbrudt. Sørg også for, at du ikke har nogen aktive forbindelser eller forespørgselsvinduer åbne mod model database, ellers kan du få denne fejl, når scriptet forsøger at oprette en database:

Msg 1807, Level 16, State 3
Kunne ikke opnå eksklusiv lås på database-'model'. Prøv handlingen igen senere.

Resultater

Der er mange datapunkter at se på (og alle forespørgsler, der bruges til at udlede dataene, henvises til i appendiks). Husk, at hver gennemsnitsvarighed, der er angivet her, er over 10 tests og indsætter i alt 100.000 rækker i destinationstabellen.

Graf 1 – Samlede aggregater

Den første graf viser overordnede aggregater (gennemsnitlig varighed) for de forskellige variable isoleret (så *alle* tests ved hjælp af en AFTER-trigger, der sletter, *alle* tests med en AFTER-trigger, der ruller tilbage osv.).


Gennemsnitlig varighed, i millisekunder, for hver variabel isoleret em>

Et par ting springer ud af os med det samme:

  • INSTEAD OF-triggeren her er dobbelt så hurtig som begge AFTER-triggere.
  • At have transaktionsloggen på SSD gjorde lidt af en forskel. Placeringen af ​​datafilen meget mindre.
  • Batchen på 20.000 singleton-skær var 7-8 gange langsommere end nogen anden batchdistribution.
  • Den enkelte batch-indsats på 20.000 rækker var langsommere end nogen af ​​de ikke-singleton-distributioner.
  • Fejlfrekvens, snapshot-isolering og gendannelsesmodel havde ringe eller ingen indflydelse på ydeevnen.

Graf 2 – Bedste 10 samlet

Denne graf viser de hurtigste 10 resultater, når hver variabel tages i betragtning. Disse er alle I STEDET FOR triggere, hvor den største procentdel af rækker fejler (50%). Overraskende nok havde den hurtigste (dog ikke ret meget) både data og log på den samme HDD (ikke SSD). Der er en blanding af disklayouts og gendannelsesmodeller her, men alle 10 havde snapshot-isolering aktiveret, og de øverste 7 resultater involverede alle batchstørrelsen på 10 x 2.000 rækker.


Bedste 10 varigheder i millisekunder, i betragtning af hver variabel

Den hurtigste AFTER-trigger – en ROLLBACK-variant med 10 % fejlrate i batchstørrelsen på 100 x 200 rækker – kom i position #144 (806 ms).

Graf 3 – Værste 10 samlet

Denne graf viser de langsomste 10 resultater, når hver variabel tages i betragtning; alle er AFTER-varianter, alle involverer de 20.000 singleton-indsatser, og alle har data og log på den samme langsomme HDD.


Værste 10 varigheder, i millisekunder, i betragtning af hver variabel

Den langsomste I STEDET FOR test var i position #97, ved 5.680 ms – en 20.000 singleton indstikstest, hvor 10 % mislykkedes. Det er også interessant at observere, at ikke en enkelt AFTER-trigger ved brug af 20.000 singleton-skærs batchstørrelse klarede sig bedre – faktisk var det 96. værste resultat en EFTER (slet) test, der kom ind på 10.219 ms – næsten det dobbelte af det næstlangsommeste resultat.

Graf 4 – Log Disk Type, Singleton Inserts

Graferne ovenfor giver os en nogenlunde idé om de største smertepunkter, men de er enten for zoomet ind eller ikke zoomet nok ind. Denne graf filtrerer ned til data baseret på virkeligheden:i de fleste tilfælde vil denne type operation være en enkelt indsættelse. Jeg tænkte, at jeg ville opdele det efter fejlfrekvens og typen af ​​disk, loggen er på, men kun se på rækker, hvor batchen består af 20.000 individuelle indstik.


Varighed, i millisekunder, grupperet efter fejlrate og logplacering, for 20.000 individuelle skær

Her ser vi, at alle AFTER-triggere er i gennemsnit i intervallet 10-11 sekunder (afhængigt af logplacering), mens alle I STEDET FOR triggere er et godt stykke under 6-sekundersmærket.

Konklusion

Indtil videre forekommer det mig klart, at STEDET FOR-triggeren er en vinder i de fleste tilfælde – i nogle tilfælde mere end andre (f.eks. da fejlraten stiger). Andre faktorer, såsom genopretningsmodellen, ser ud til at have meget mindre indflydelse på den samlede ydeevne.

Hvis du har andre ideer til, hvordan du opdeler dataene, eller gerne vil have en kopi af dataene til at udføre din egen udskæring og opdeling, så lad mig det vide. Hvis du gerne vil have hjælp til at sætte dette miljø op, så du kan køre dine egne tests, kan jeg også hjælpe med det.

Selvom denne test viser, at I STEDET FOR triggere absolut er værd at overveje, er det ikke hele historien. Jeg slog bogstaveligt talt disse triggere sammen ved at bruge den logik, som jeg troede gav mest mening for hvert scenarie, men triggerkode - som enhver T-SQL-sætning - kan indstilles til optimale planer. I et opfølgende indlæg vil jeg tage et kig på en potentiel optimering, der kan gøre AFTER-triggeren mere konkurrencedygtig.

Bilag

Forespørgsler brugt til sektionen Resultater:

Graf 1 – Samlede aggregater

SELECT RTRIM(l.loops) + ' x ' + RTRIM(l.perloop), AVG(r.Duration*1.0)
  FROM dbo.TestResults AS r
  INNER JOIN dbo.Loops AS l
  ON r.LoopID = l.LoopID
  GROUP BY RTRIM(l.loops) + ' x ' + RTRIM(l.perloop);
 
SELECT t.IsSnapshot, AVG(Duration*1.0)
  FROM dbo.TestResults AS tr
  INNER JOIN dbo.Tests AS t
  ON tr.TestID = t.TestID 
  GROUP BY t.IsSnapshot;
 
SELECT t.RecoveryModel, AVG(Duration*1.0)
  FROM dbo.TestResults AS tr
  INNER JOIN dbo.Tests AS t
  ON tr.TestID = t.TestID 
  GROUP BY t.RecoveryModel;
 
SELECT t.DiskLayout, AVG(Duration*1.0)
  FROM dbo.TestResults AS tr
  INNER JOIN dbo.Tests AS t
  ON tr.TestID = t.TestID 
  GROUP BY t.DiskLayout;
 
SELECT t.TriggerType, AVG(Duration*1.0)
  FROM dbo.TestResults AS tr
  INNER JOIN dbo.Tests AS t
  ON tr.TestID = t.TestID 
  GROUP BY t.TriggerType;
 
SELECT t.FailureRate, AVG(Duration*1.0)
  FROM dbo.TestResults AS tr
  INNER JOIN dbo.Tests AS t
  ON tr.TestID = t.TestID 
  GROUP BY t.FailureRate;

Graf 2 og 3 – Bedste og værste 10

;WITH src AS 
(
    SELECT DiskLayout, RecoveryModel, TriggerType, FailureRate, IsSnapshot,
      Batch = RTRIM(l.loops) + ' x ' + RTRIM(l.perloop),
      Duration = AVG(Duration*1.0)
    FROM dbo.Tests AS t
    INNER JOIN dbo.TestResults AS tr
    ON tr.TestID = t.TestID 
    INNER JOIN dbo.Loops AS l
    ON tr.LoopID = l.LoopID
    GROUP BY DiskLayout, RecoveryModel, TriggerType, FailureRate, IsSnapshot,
      RTRIM(l.loops) + ' x ' + RTRIM(l.perloop)
),
agg AS
(
    SELECT label = REPLACE(REPLACE(DiskLayout,'Data',''),'_Log','/')
      + ', ' + RecoveryModel + ' recovery, ' + TriggerType
  	+ ', ' + RTRIM(FailureRate) + '% fail'
	+ ', Snapshot = ' + CASE IsSnapshot WHEN 1 THEN 'ON' ELSE 'OFF' END
  	+ ', ' + Batch + ' (ops x rows)',
      best10  = ROW_NUMBER() OVER (ORDER BY Duration), 
      worst10 = ROW_NUMBER() OVER (ORDER BY Duration DESC),
      Duration
    FROM src
)
SELECT grp, label, Duration FROM
(
  SELECT TOP (20) grp = 'best', label = RIGHT('0' + RTRIM(best10),2) + '. ' + label, Duration
    FROM agg WHERE best10 <= 10
    ORDER BY best10 DESC
  UNION ALL
  SELECT TOP (20) grp = 'worst', label = RIGHT('0' + RTRIM(worst10),2) + '. ' + label, Duration
    FROM agg WHERE worst10 <= 10
    ORDER BY worst10 DESC
  ) AS b
  ORDER BY grp;

Graf 4 – Log Disk Type, Singleton Inserts

;WITH x AS
(
    SELECT 
      TriggerType,FailureRate,
      LogLocation = RIGHT(DiskLayout,3), 
      Duration = AVG(Duration*1.0)
    FROM dbo.TestResults AS tr
    INNER JOIN dbo.Tests AS t
    ON tr.TestID = t.TestID 
    INNER JOIN dbo.Loops AS l
    ON l.LoopID = tr.LoopID
    WHERE l.loops = 20000
    GROUP BY RIGHT(DiskLayout,3), FailureRate, TriggerType
)
SELECT TriggerType, FailureRate, 
  HDDDuration = MAX(CASE WHEN LogLocation = 'HDD' THEN Duration END),
  SSDDuration = MAX(CASE WHEN LogLocation = 'SSD' THEN Duration END)
FROM x 
GROUP BY TriggerType, FailureRate
ORDER BY TriggerType, FailureRate;

  1. pyodbc - meget langsom bulk indsættelseshastighed

  2. Hvad er MySQL JDBC-driverforbindelsesstrengen?

  3. Brug af Alias ​​In When Del af en sagserklæring i Oracle SQL

  4. Hvordan kan jeg med MySQL generere en kolonne, der indeholder postindekset i en tabel?