Poznámka:Tento příspěvek byl původně publikován pouze v naší elektronické knize, High Performance Techniques for SQL Server, Volume 2. O našich elektronických knihách se můžete dozvědět zde. Všimněte si také, že některé z těchto věcí se mohou změnit s plánovanými vylepšeními In-Memory OLTP v SQL Server 2016.
Existuje několik zvyků a osvědčených postupů, které si mnoho z nás časem vyvine s ohledem na kód Transact-SQL. Zejména u uložených procedur se snažíme předávat hodnoty parametrů správného datového typu a pojmenovávat naše parametry explicitně, spíše než se spoléhat pouze na řadovou pozici. Někdy však můžeme být líní v tomto:můžeme zapomenout předponu řetězce Unicode s N
, nebo pouze seznam konstant nebo proměnných v pořadí namísto zadávání názvů parametrů. Nebo obojí.
Pokud v SQL Server 2014 používáte In-Memory OLTP ("Hekaton") a nativně zkompilované procedury, možná budete chtít své myšlení o těchto věcech trochu upravit. Předvedu nějaký kód na ukázce SQL Server 2014 RTM In-Memory OLTP Sample na CodePlex, která rozšiřuje ukázkovou databázi AdventureWorks2012. (Pokud to chcete nastavit od začátku, abyste mohli pokračovat, podívejte se prosím rychle na moje pozorování v předchozím příspěvku.)
Podívejme se na podpis pro uloženou proceduru 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
Zajímalo by mě, jestli záleží na tom, jestli jsou parametry pojmenované, nebo jestli nativně kompilované procedury zvládají implicitní převody jako argumenty pro uložené procedury lépe než tradiční uložené procedury. Nejprve jsem vytvořil kopii Sales.usp_InsertSpecialOffer_inmem
jako tradiční uložená procedura – to zahrnovalo pouze odstranění ATOMIC
blokovat a odstranit NOT NULL
deklarace ze vstupních parametrů:
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
Aby se minimalizovalo přesouvání kritérií, postup se stále vkládá do verze tabulky v paměti Sales.SpecialOffer_inmem.
Potom jsem chtěl načasovat 100 000 volání do obou kopií uložené procedury s těmito kritérii:
Výslovně pojmenované parametry | Parametry nejsou pojmenovány | |
---|---|---|
Všechny parametry správného datového typu | x | x |
Některé parametry nesprávného datového typu | x | x |
Pomocí následující dávky, zkopírované pro tradiční verzi uložené procedury (stačí odstranit _inmem
ze čtyř EXEC
hovory):
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
Každý test jsem provedl 10krát a zde byly průměrné doby trvání v milisekundách:
Tradiční uložená procedura | |
---|---|
Parametry | Průměrná doba trvání (milisekundy) |
Pojmenované, správné typy | 72 132 |
Bez názvu, správné typy | 72 846 |
Pojmenované, nevhodné typy | 76 154 |
Bez názvu, nevhodné typy | 76 902 |
Nativně zkompilovaná uložená procedura | |
Parametry | Průměrná doba trvání (milisekundy) |
Pojmenované, správné typy | 63 202 |
Bez názvu, správné typy | 61 297 |
Pojmenované, nevhodné typy | 64 560 |
Bez názvu, nevhodné typy | 64 288 |
Průměrná doba trvání různých metod volání v milisekundách
U tradiční uložené procedury je jasné, že použití nesprávných datových typů má podstatný dopad na výkon (asi 4 sekundový rozdíl), zatímco nepojmenování parametrů mělo mnohem méně dramatický efekt (přidání asi 700 ms). Vždy jsem se snažil dodržovat osvědčené postupy a používat správné datové typy a také pojmenovávat všechny parametry a zdá se, že tento malý test potvrzuje, že to může být prospěšné.
U nativně zkompilované uložené procedury vedlo použití nesprávných datových typů stále k podobnému poklesu výkonu jako u tradiční uložené procedury. Tentokrát však pojmenování parametrů tolik nepomohlo; ve skutečnosti to mělo negativní dopad a přidalo téměř dvě sekundy k celkovému trvání. Abychom byli spravedliví, jedná se o velký počet hovorů za poměrně krátkou dobu, ale pokud se z této funkce snažíte vymáčknout absolutně nejproblematičtější výkon, počítá se každá nanosekunda.
Odhalení problému
Jak můžete vědět, zda jsou vaše nativně zkompilované uložené procedury volány některou z těchto „pomalých“ metod? Na to je XEvent! Událost se nazývá native_compiled_proc_slow_parameter_passing
a nezdá se, že by to v současnosti bylo zdokumentováno v Books Online. Pro sledování této události můžete vytvořit následující relaci Extended Events:
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;
Jakmile relace běží, můžete jednotlivě vyzkoušet kterékoli z výše uvedených čtyř volání a poté můžete spustit tento dotaz:
;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;
V závislosti na tom, co jste spustili, byste měli vidět výsledky podobné tomuto:
časové razítko | db | id_objektu | důvod | dávka |
---|---|---|---|---|
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; |
Ukázkové výsledky z rozšířených událostí
Doufejme, že dávka
sloupec stačí k identifikaci viníka, ale pokud máte velké dávky, které obsahují více volání nativně kompilovaných procedur a potřebujete vysledovat objekty, které konkrétně spouštějí tento problém, můžete je jednoduše vyhledat pomocí id_objektu v jejich příslušných databázích.
Nyní nedoporučuji spouštět všech 400 000 hovorů v textu, když je relace aktivní, nebo tuto relaci zapínat ve vysoce souběžném produkčním prostředí – pokud to děláte hodně, může to způsobit značnou režii. Je pro vás mnohem lepší kontrolovat tento druh činnosti ve vašem vývojovém nebo přípravném prostředí, pokud je můžete podrobit řádné pracovní zátěži pokrývající celý obchodní cyklus.
Závěr
Rozhodně mě překvapila skutečnost, že pojmenování parametrů – dlouho považované za osvědčený postup – se u nativně kompilovaných uložených procedur změnilo v nejhorší. A Microsoft ví, že potenciální problém je dostatečný, že vytvořili Extended Event navrženou speciálně k jejímu sledování. Pokud používáte In-Memory OLTP, toto je jedna věc, kterou byste měli mít na paměti, když vyvíjíte podpůrné uložené procedury. Vím, že rozhodně budu muset odtrénovat svou svalovou paměť používáním pojmenovaných parametrů.