Obs:Det här inlägget publicerades ursprungligen endast i vår e-bok, High Performance Techniques for SQL Server, volym 2. Du kan ta reda på om våra e-böcker här. Observera också att vissa av dessa saker kan förändras med de planerade förbättringarna av In-Memory OLTP i SQL Server 2016.
Det finns några vanor och bästa praxis som många av oss utvecklar med tiden när det gäller Transact-SQL-kod. Speciellt med lagrade procedurer strävar vi efter att skicka parametervärden av rätt datatyp och namnge våra parametrar uttryckligen snarare än att bara förlita oss på ordningsposition. Men ibland kan vi bli lata om detta:vi kanske glömmer att prefixet en Unicode-sträng med N
, eller bara lista konstanterna eller variablerna i ordning istället för att ange parameternamnen. Eller båda.
I SQL Server 2014, om du använder In-Memory OLTP ("Hekaton") och inbyggda kompilerade procedurer, kanske du vill justera ditt tänkande om dessa saker lite. Jag ska demonstrera med lite kod mot SQL Server 2014 RTM In-Memory OLTP Sample på CodePlex, som utökar exempeldatabasen AdventureWorks2012. (Om du ska ställa in det här från början för att följa med, vänligen ta en snabb blick på mina observationer i ett tidigare inlägg.)
Låt oss ta en titt på signaturen för den lagrade proceduren 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
Jag var nyfiken på om det spelade någon roll om parametrarna var namngivna eller om inbyggda kompilerade procedurer hanterade implicita omvandlingar som argument till lagrade procedurer bättre än traditionella lagrade procedurer. Först skapade jag en kopia Sales.usp_InsertSpecialOffer_inmem
som en traditionell lagrad procedur – detta innebar bara att ta bort ATOMIC
blockera och ta bort NOT NULL
deklarationer från indataparametrarna:
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
För att minimera förskjutningskriterier, infogas proceduren fortfarande i In-Memory-versionen av tabellen, Sales.SpecialOffer_inmem.
Sedan ville jag tajma 100 000 samtal till båda kopiorna av den lagrade proceduren med dessa kriterier:
Parametrar som är explicit namngivna | Parametrar inte namngivna | |
---|---|---|
Alla parametrar av korrekt datatyp | x | x |
Vissa parametrar av fel datatyp | x | x |
Med följande batch, kopierad för den traditionella versionen av den lagrade proceduren (bara att ta bort _inmem
från de fyra EXEC
samtal):
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
Jag körde varje test 10 gånger, och här var den genomsnittliga varaktigheten, i millisekunder:
Traditionell lagrad procedur | |
---|---|
Parametrar | Genomsnittlig varaktighet (millisekunder) |
Namngivna, korrekta typer | 72 132 |
Inte namngiven, korrekta typer | 72 846 |
Namngivna, olämpliga typer | 76 154 |
Inte namngiven, felaktiga typer | 76 902 |
Natively kompilerad lagrad procedur | |
Parametrar | Genomsnittlig varaktighet (millisekunder) |
Namngivna, korrekta typer | 63 202 |
Inte namngiven, korrekta typer | 61 297 |
Namngivna, olämpliga typer | 64 560 |
Inte namngiven, felaktiga typer | 64 288 |
Genomsnittlig varaktighet, i millisekunder, för olika anropsmetoder
Med den traditionella lagrade proceduren är det tydligt att användning av fel datatyper har en betydande inverkan på prestandan (cirka 4 sekunders skillnad), samtidigt som att inte namnge parametrarna hade en mycket mindre dramatisk effekt (tillför cirka 700 ms). Jag har alltid försökt följa bästa praxis och använda rätt datatyper samt namnge alla parametrar, och det här lilla testet verkar bekräfta att det kan vara fördelaktigt att göra det.
Med den inbyggt kompilerade lagrade proceduren ledde användningen av fel datatyper fortfarande till en liknande prestandaminskning som med den traditionella lagrade proceduren. Men den här gången hjälpte det inte så mycket att namnge parametrarna; i själva verket hade det en negativ inverkan, och lade till nästan två sekunder till den totala varaktigheten. För att vara rättvis är det här ett stort antal samtal på ganska kort tid, men om du försöker få ut den absolut mest avancerade prestanda du kan ur den här funktionen, räknas varje nanosekund.
Att upptäcka problemet
Hur kan du veta om dina inbyggt kompilerade lagrade procedurer anropas med någon av dessa "långsamma" metoder? Det finns ett XEvent för det! Händelsen kallas natively_compiled_proc_slow_parameter_passing
, och det verkar inte vara dokumenterat i Books Online just nu. Du kan skapa följande utökade händelsesession för att övervaka denna händelse:
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örs kan du prova något av ovanstående fyra anrop individuellt, och sedan kan du köra den här frågan:
;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;
Beroende på vad du körde bör du se resultat som liknar detta:
tidsstämpel | db | objekt-id | orsak | batch |
---|---|---|---|---|
2014-07-01 16:23:14 | AdventureWorks2012 | 2087678485 | named_parameters | 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_conversion | 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; |
Exempelresultat från utökade evenemang
Förhoppningsvis batchen
kolumnen räcker för att identifiera den skyldige, men om du har stora partier som innehåller flera anrop till inbyggt kompilerade procedurer och du behöver spåra de objekt som specifikt utlöser detta problem, kan du helt enkelt slå upp dem med objekt_id i sina respektive databaser.
Nu rekommenderar jag inte att köra alla 400 000 anrop i texten medan sessionen är aktiv, eller att aktivera den här sessionen i en mycket samtidig produktionsmiljö – om du gör det här mycket kan det orsaka betydande overhead. Du är mycket bättre av att kolla efter den här typen av aktivitet i din utvecklings- eller iscensättningsmiljö, så länge du kan utsätta den för en ordentlig arbetsbelastning som täcker en hel affärscykel.
Slutsats
Jag blev definitivt förvånad över det faktum att namngivningsparametrar – länge ansett som en bästa praxis – har förvandlats till en sämsta praxis med inbyggda lagrade procedurer. Och det är känt av Microsoft för att vara tillräckligt av ett potentiellt problem att de skapade en utökad händelse designad speciellt för att spåra det. Om du använder In-Memory OLTP, är detta en sak du bör ha på din radar när du utvecklar stödjande lagrade procedurer. Jag vet att jag definitivt kommer att behöva avträna mitt muskelminne från att använda namngivna parametrar.