Забележка:Тази публикация първоначално е публикувана само в нашата електронна книга, Високопроизводителни техники за SQL Server, том 2. Можете да научите за нашите електронни книги тук. Също така имайте предвид, че някои от тези неща може да се променят с планираните подобрения на OLTP в паметта в SQL Server 2016.
Има някои навици и най-добри практики, които много от нас развиват с течение на времето по отношение на кода на Transact-SQL. По-специално със съхранените процедури, ние се стремим да предаваме стойности на параметри от правилния тип данни и да назоваваме параметрите си изрично, вместо да разчитаме единствено на порядковата позиция. Понякога обаче може да ни мързи за това:може да забравим да поставим префикс на Unicode низ с N
, или просто избройте константите или променливите по ред, вместо да указвате имената на параметрите. Или и двете.
В SQL Server 2014, ако използвате OLTP в паметта („Hekaton“) и компилирани процедури, може да искате да коригирате малко мисленето си за тези неща. Ще демонстрирам с малко код срещу SQL Server 2014 RTM In-Memory OLTP Sample на CodePlex, който разширява примерната база данни на AdventureWorks2012. (Ако ще настроите това от нулата, за да следвате, моля, хвърлете бърз поглед върху наблюденията ми в предишна публикация.)
Нека да разгледаме подписа за съхранената процедура 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
Бях любопитен дали има значение дали параметрите са наименувани, или компилираните процедури обработват неявни преобразувания като аргументи към съхранените процедури по-добре от традиционните съхранени процедури. Първо създадох копие Sales.usp_InsertSpecialOffer_inmem
като традиционна съхранена процедура – това включваше просто премахване на ATOMIC
блокиране и премахване на NOT NULL
декларации от входните параметри:
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
За да се сведе до минимум критериите за изместване, процедурата все още се вмъква във версията In-Memory на таблицата, Sales.SpecialOffer_inmem.
След това исках да синхронизирам 100 000 извиквания към двете копия на съхранената процедура със следните критерии:
Изрично именувани параметри | Параметрите не са наименувани | |
---|---|---|
Всички параметри с правилен тип данни | x | x |
Някои параметри с грешен тип данни | x | x |
Използвайки следната партида, копирана за традиционната версия на съхранената процедура (просто премахване на _inmem
от четирите EXEC
обаждания):
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
Проведох всеки тест 10 пъти и ето средната продължителност в милисекунди:
Традиционна съхранена процедура | |
---|---|
Параметри | Средна продължителност (милисекунди) |
Именувани, правилни типове | 72 132 |
Без име, правилни типове | 72 846 |
Именувани, неправилни типове | 76 154 |
Неименувани, неправилни типове | 76 902 |
Естествено компилирана съхранена процедура | |
Параметри | Средна продължителност (милисекунди) |
Именувани, правилни типове | 63 202 |
Без име, правилни типове | 61 297 |
Именувани, неправилни типове | 64 560 |
Неименувани, неправилни типове | 64 288 |
Средна продължителност в милисекунди на различни методи на извикване
С традиционната съхранена процедура е ясно, че използването на грешни типове данни оказва значително влияние върху производителността (около 4 секунди разлика), докато не назоваването на параметрите има много по-малко драматичен ефект (добавяне на около 700 ms). Винаги съм се опитвал да следвам най-добрите практики и да използвам правилните типове данни, както и да назовавам всички параметри и този малък тест изглежда потвърждава, че това може да бъде от полза.
С нативно компилираната съхранена процедура, използването на грешни типове данни все още води до подобен спад в производителността, както при традиционната съхранена процедура. Този път обаче наименуването на параметрите не помогна толкова; всъщност имаше отрицателно въздействие, добавяйки почти две секунди към общата продължителност. За да бъдем честни, това е голям брой обаждания за доста кратко време, но ако се опитвате да извлечете от тази функция абсолютно най-съвършената производителност, която можете, всяка наносекунда е от значение.
Откриване на проблема
Как можете да разберете дали компилираните ви съхранени процедури се извикват с някой от тези "бавни" методи? Има XEvent за това! Събитието се нарича natively_compiled_proc_slow_parameter_passing
, и изглежда не е документирано в Books Online в момента. Можете да създадете следната сесия за разширени събития, за да наблюдавате това събитие:
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;
След като сесията започне, можете да опитате някое от горните четири повиквания поотделно и след това можете да изпълните тази заявка:
;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;
В зависимост от това, което сте изпълнили, трябва да видите резултати, подобни на този:
timestamp | db | object_id | причина | партида |
---|---|---|---|---|
2014-07-01 16:23:14 | AdventureWorks2012 | 2087678485 | именувани_параметри | 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 | преобразуване на параметър | 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; |
Примерни резултати от разширени събития
Надяваме се партида
колоната е достатъчна, за да идентифицира виновника, но ако имате големи партиди, които съдържат множество извиквания към нативно компилирани процедури и трябва да проследите обектите, които конкретно задействат този проблем, можете просто да ги потърсите чрез object_idкод> в съответните им бази данни.
Сега не препоръчвам да изпълнявате всички 400 000 обаждания в текста, докато сесията е активна, или да включвате тази сесия в силно едновременна производствена среда – ако правите това много, това може да доведе до значителни разходи. Много по-добре е да проверите за този вид дейност във вашата среда за разработка или етапи, стига да можете да я подложите на подходящо работно натоварване, покриващо пълен бизнес цикъл.
Заключение
Определено бях изненадан от факта, че параметрите за именуване – дълго време считани за най-добра практика – се превърнаха в най-лошата практика с компилираните в собствен произход съхранени процедури. И е известно от Microsoft, че е достатъчен потенциален проблем, че те създадоха разширено събитие, предназначено специално за проследяването му. Ако използвате In-Memory OLTP, това е едно нещо, което трябва да държите на радара си, докато разработвате поддържащи съхранени процедури. Знам, че определено ще трябва да не тренирам мускулната си памет от използването на посочени параметри.