Bemærk:Dette indlæg blev oprindeligt kun offentliggjort i vores e-bog, High Performance Techniques for SQL Server, bind 2. Du kan finde ud af om vores e-bøger her. Bemærk også, at nogle af disse ting kan ændre sig med de planlagte forbedringer af In-Memory OLTP i SQL Server 2016.
Der er nogle vaner og bedste praksis, som mange af os udvikler over tid med hensyn til Transact-SQL-kode. Med især lagrede procedurer stræber vi efter at videregive parameterværdier af den korrekte datatype og navngive vores parametre eksplicit i stedet for udelukkende at stole på ordensposition. Nogle gange kan vi dog blive dovne over dette:vi glemmer måske at præfikse en Unicode-streng med N
, eller bare angiv konstanterne eller variablerne i rækkefølge i stedet for at angive parameternavnene. Eller begge dele.
I SQL Server 2014, hvis du bruger In-Memory OLTP ("Hekaton") og indbyggede kompilerede procedurer, vil du måske justere din tankegang om disse ting en smule. Jeg vil demonstrere med noget kode mod SQL Server 2014 RTM In-Memory OLTP Sample på CodePlex, som udvider AdventureWorks2012-eksempeldatabasen. (Hvis du vil sætte dette op fra bunden for at følge med, bedes du tage et hurtigt blik på mine observationer i et tidligere indlæg.)
Lad os tage et kig på signaturen for den lagrede procedure Sales.usp_InsertSpecialOffer_inmem
:
CREATE PROCEDURE [Sales].[usp_InsertSpecialOffer_inmem] @Description NVARCHAR(255) NOT NULL, @DiscountPct SMALLMONEY NOT NULL = 0, @Type NVARCHAR(50) NOT NULL, @Category NVARCHAR(50) NOT NULL, @StartDate DATETIME2 NOT NULL, @EndDate DATETIME2 NOT NULL, @MinQty INT NOT NULL = 0, @MaxQty INT = NULL, @SpecialOfferID INT OUTPUT WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER AS BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL=SNAPSHOT, LANGUAGE=N'us_english') DECLARE @msg nvarchar(256) -- validation removed for brevity INSERT Sales.SpecialOffer_inmem (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty, MaxQty) VALUES (@Description, @DiscountPct, @Type, @Category, @StartDate, @EndDate, @MinQty, @MaxQty) SET @SpecialOfferID = SCOPE_IDENTITY() END GO
Jeg var nysgerrig, om det havde nogen betydning, om parametrene blev navngivet, eller om native kompilerede procedurer håndterede implicitte konverteringer som argumenter til lagrede procedurer bedre end traditionelle lagrede procedurer. Først oprettede jeg en kopi Sales.usp_InsertSpecialOffer_inmem
som en traditionel lagret procedure – dette involverede blot at fjerne ATOMIC
blokere og fjerne NOT NULL
erklæringer fra inputparametrene:
CREATE PROCEDURE [Sales].[usp_InsertSpecialOffer] @Description NVARCHAR(255), @DiscountPct SMALLMONEY = 0, @Type NVARCHAR(50), @Category NVARCHAR(50), @StartDate DATETIME2, @EndDate DATETIME2, @MinQty INT = 0, @MaxQty INT = NULL, @SpecialOfferID INT OUTPUT AS BEGIN DECLARE @msg nvarchar(256) -- validation removed for brevity INSERT Sales.SpecialOffer_inmem (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty, MaxQty) VALUES (@Description, @DiscountPct, @Type, @Category, @StartDate, @EndDate, @MinQty, @MaxQty) SET @SpecialOfferID = SCOPE_IDENTITY() END GO
For at minimere forskydningskriterier indsættes proceduren stadig i In-Memory-versionen af tabellen, Sales.SpecialOffer_inmem.
Så ønskede jeg at time 100.000 opkald til begge kopier af den lagrede procedure med disse kriterier:
Parametre navngivet eksplicit | Parametre ikke navngivet | |
---|---|---|
Alle parametre af korrekt datatype | x | x |
Nogle parametre af forkert datatype | x | x |
Ved brug af følgende batch, kopieret til den traditionelle version af den lagrede procedure (simpelthen fjernelse af _inmem
fra de fire EXEC
opkald):
SET NOCOUNT ON; CREATE TABLE #x ( i INT IDENTITY(1,1), d VARCHAR(32), s DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(), e DATETIME2(7) ); GO INSERT #x(d) VALUES('Named, proper types'); GO /* this uses named parameters, and uses correct data types */ DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = @p7, @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 1; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Not named, proper types'); GO /* this does not use named parameters, but uses correct data types */ DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 2; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Named, improper types'); GO /* this uses named parameters, but incorrect data types */ DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = '10', @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 3; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Not named, improper types'); GO /* this does not use named parameters, and uses incorrect data types */ DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, '10', @p8, @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 4; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO SELECT d, duration_ms = DATEDIFF(MILLISECOND, s, e) FROM #x; GO DROP TABLE #x; GO
Jeg kørte hver test 10 gange, og her var de gennemsnitlige varigheder i millisekunder:
Traditionel lagret procedure | |
---|---|
Parametre | Gennemsnitlig varighed (millisekunder) |
Navngivne, rigtige typer | 72.132 |
Ikke navngivet, korrekte typer | 72.846 |
Navngivne, ukorrekte typer | 76.154 |
Ikke navngivet, ukorrekte typer | 76.902 |
Native kompileret lagret procedure | |
Parametre | Gennemsnitlig varighed (millisekunder) |
Navngivne, rigtige typer | 63.202 |
Ikke navngivet, korrekte typer | 61.297 |
Navngivne, ukorrekte typer | 64.560 |
Ikke navngivet, ukorrekte typer | 64.288 |
Gennemsnitlig varighed, i millisekunder, af forskellige opkaldsmetoder
Med den traditionelle lagrede procedure er det klart, at brug af de forkerte datatyper har en væsentlig indvirkning på ydeevnen (ca. 4 sekunders forskel), mens ikke at navngive parametrene havde en meget mindre dramatisk effekt (tilføj ca. 700ms). Jeg har altid forsøgt at følge bedste praksis og bruge de rigtige datatyper samt navngive alle parametre, og denne lille test ser ud til at bekræfte, at det kan være en fordel at gøre det.
Med den oprindeligt kompilerede lagrede procedure førte brug af de forkerte datatyper stadig til et lignende fald i ydeevne som med den traditionelle lagrede procedure. Denne gang hjalp det dog ikke så meget at navngive parametrene; faktisk havde det en negativ indvirkning og tilføjede næsten to sekunder til den samlede varighed. For at være retfærdig er dette et stort antal opkald på ret kort tid, men hvis du forsøger at presse den absolut mest avancerede ydeevne ud af denne funktion, tæller hvert nanosekund.
Opdagelse af problemet
Hvordan kan du vide, om dine oprindeligt kompilerede lagrede procedurer bliver kaldt med en af disse "langsomme" metoder? Der er en XEvent for det! Hændelsen kaldes natively_compiled_proc_slow_parameter_passing
, og det ser ikke ud til at være dokumenteret i Books Online på nuværende tidspunkt. Du kan oprette følgende udvidede begivenhedssession for at overvåge denne begivenhed:
CREATE EVENT SESSION [XTP_Parameter_Events] ON SERVER ADD EVENT sqlserver.natively_compiled_proc_slow_parameter_passing ( ACTION(sqlserver.sql_text) ) ADD TARGET package0.event_file(SET filename=N'C:\temp\XTPParams.xel'); GO ALTER EVENT SESSION [XTP_Parameter_Events] ON SERVER STATE = START;
Når sessionen kører, kan du prøve et af ovenstående fire opkald individuelt, og derefter kan du køre denne forespørgsel:
;WITH x([timestamp], db, [object_id], reason, batch) AS ( SELECT xe.d.value(N'(event/@timestamp)[1]',N'datetime2(0)'), DB_NAME(xe.d.value(N'(event/data[@name="database_id"]/value)[1]',N'int')), xe.d.value(N'(event/data[@name="object_id"]/value)[1]',N'int'), xe.d.value(N'(event/data[@name="reason"]/text)[1]',N'sysname'), xe.d.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar(max)') FROM sys.fn_xe_file_target_read_file(N'C:\temp\XTPParams*.xel',NULL,NULL,NULL) AS ft CROSS APPLY (SELECT CONVERT(XML, ft.event_data)) AS xe(d) ) SELECT [timestamp], db, [object_id], reason, batch FROM x;
Afhængigt af hvad du kørte, bør du se resultater svarende til dette:
tidsstempel | db | objekt-id | årsag | batch |
---|---|---|---|---|
2014-07-01 16:23:14 | AdventureWorks2012 | 2087678485 | navngivne_parametre | DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = @p7, @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; |
2014-07-01 16:23:22 | AdventureWorks2012 | 2087678485 | parameter_konvertering | DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, '10', @p8, @p9 OUTPUT; |
Eksempler på resultater fra udvidede begivenheder
Forhåbentlig er batchen
kolonnen er nok til at identificere den skyldige, men hvis du har store batches, der indeholder flere kald til native kompilerede procedurer, og du skal spore de objekter, der specifikt udløser dette problem, kan du blot slå dem op ved at object_id i deres respektive databaser.
Nu anbefaler jeg ikke at køre alle 400.000 opkald i teksten, mens sessionen er aktiv, eller at aktivere denne session i et meget samtidig produktionsmiljø – hvis du gør dette meget, kan det forårsage betydelige overhead. Du er meget bedre stillet at tjekke for denne form for aktivitet i dit udviklings- eller iscenesættelsesmiljø, så længe du kan udsætte det for en ordentlig arbejdsbyrde, der dækker en fuld forretningscyklus.
Konklusion
Jeg var bestemt overrasket over, at navngivningsparametre – længe betragtet som en bedste praksis – er blevet omdannet til en værste praksis med indbyggede lagrede procedurer. Og det er kendt af Microsoft for at være nok af et potentielt problem, at de oprettede en udvidet begivenhed designet specifikt til at spore den. Hvis du bruger In-Memory OLTP, er dette én ting, du bør holde på din radar, mens du udvikler understøttende lagrede procedurer. Jeg ved, at jeg helt sikkert bliver nødt til at aftræne min muskelhukommelse fra at bruge navngivne parametre.