Uwaga:ten post został pierwotnie opublikowany tylko w naszym eBooku, Techniki wysokiej wydajności dla SQL Server, tom 2. Możesz dowiedzieć się o naszych eBookach tutaj. Należy również pamiętać, że niektóre z tych rzeczy mogą ulec zmianie wraz z planowanymi ulepszeniami OLTP w pamięci w SQL Server 2016.
Istnieje kilka nawyków i najlepszych praktyk, które wielu z nas rozwija z biegiem czasu w odniesieniu do kodu Transact-SQL. W szczególności w przypadku procedur składowanych staramy się przekazywać wartości parametrów o poprawnym typie danych i jawnie nazywać nasze parametry, zamiast polegać wyłącznie na pozycji porządkowej. Czasami jednak możemy być leniwi:możemy zapomnieć o prefiksie ciągu Unicode przed N
lub po prostu wymień stałe lub zmienne w kolejności zamiast określania nazw parametrów. Albo jedno i drugie.
W SQL Server 2014, jeśli używasz In-Memory OLTP ("Hekaton") i natywnie kompilowanych procedur, możesz chcieć nieco dostosować swoje myślenie o tych rzeczach. Zademonstruję z pewnym kodem na przykładzie SQL Server 2014 RTM In-Memory OLTP na CodePlex, który rozszerza przykładową bazę danych AdventureWorks2012. (Jeśli masz zamiar skonfigurować to od zera, aby kontynuować, proszę rzucić okiem na moje obserwacje w poprzednim poście.)
Przyjrzyjmy się podpisowi procedury składowanej 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
Byłem ciekaw, czy ma znaczenie, czy parametry zostały nazwane, czy natywnie skompilowane procedury obsługiwały niejawne konwersje jako argumenty procedur składowanych lepiej niż tradycyjne procedury składowane. Najpierw utworzyłem kopię Sales.usp_InsertSpecialOffer_inmem
jako tradycyjna procedura składowana – wymagało to jedynie usunięcia ATOMIC
blokowanie i usuwanie NOT NULL
deklaracje z parametrów wejściowych:
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 zminimalizować zmiany kryteriów, procedura nadal jest wstawiana do wersji tabeli w pamięci, Sales.SpecialOffer_inmem.
Następnie chciałem zsynchronizować 100 000 wywołań obu kopii procedury składowanej z następującymi kryteriami:
Parametry wyraźnie nazwane | Parametry nienazwane | |
---|---|---|
Wszystkie parametry prawidłowego typu danych | x | x |
Niektóre parametry o niewłaściwym typie danych | x | x |
Korzystając z następującej partii, skopiowanej dla tradycyjnej wersji procedury składowanej (po prostu usuwając _inmem
z czterech EXEC
połączenia):
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
Przeprowadziłem każdy test 10 razy, a oto średnie czasy trwania w milisekundach:
Tradycyjna procedura składowana | |
---|---|
Parametry | Średni czas trwania (milisekundy) |
Nazwane, właściwe typy | 72 132 |
Nie nazwane, właściwe typy | 72 846 |
Nazwane, niewłaściwe typy | 76154 |
Nie nazwane, niewłaściwe typy | 76 902 |
Natywnie skompilowana procedura składowana | |
Parametry | Średni czas trwania (milisekundy) |
Nazwane, właściwe typy | 63 202 |
Nie nazwane, właściwe typy | 61 297 |
Nazwane, niewłaściwe typy | 64 560 |
Nie nazwane, niewłaściwe typy | 64 288 |
Średni czas trwania w milisekundach różnych metod wywoływania
W przypadku tradycyjnej procedury składowanej jasne jest, że użycie niewłaściwych typów danych ma znaczny wpływ na wydajność (około 4 sekund różnicy), podczas gdy brak nazwy parametrów miał znacznie mniej dramatyczny efekt (dodanie około 700 ms). Zawsze starałem się postępować zgodnie z najlepszymi praktykami i używać właściwych typów danych, a także nazywać wszystkie parametry, a ten mały test wydaje się potwierdzać, że może to być korzystne.
W przypadku natywnie skompilowanej procedury składowanej użycie niewłaściwych typów danych nadal prowadziło do podobnego spadku wydajności, jak w przypadku tradycyjnej procedury składowanej. Tym razem jednak nazwanie parametrów nie pomogło zbytnio; w rzeczywistości miało to negatywny wpływ, dodając prawie dwie sekundy do całkowitego czasu trwania. Szczerze mówiąc, jest to duża liczba połączeń w dość krótkim czasie, ale jeśli próbujesz wycisnąć z tej funkcji absolutnie najlepszą wydajność, jaką możesz, liczy się każda nanosekunda.
Odkrywanie problemu
Skąd możesz wiedzieć, czy natywnie skompilowane procedury składowane są wywoływane za pomocą jednej z tych „wolnych” metod? Jest na to XEvent! Zdarzenie nazywa się natively_compiled_proc_slow_parameter_passing
i wydaje się, że obecnie nie jest to udokumentowane w Books Online. Możesz utworzyć następującą sesję zdarzeń rozszerzonych, aby monitorować to zdarzenie:
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;
Po uruchomieniu sesji możesz wypróbować dowolne z powyższych czterech wywołań pojedynczo, a następnie uruchomić to zapytanie:
;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;
W zależności od tego, co uruchomiłeś, powinieneś zobaczyć wyniki podobne do tego:
sygnatura czasowa | db | identyfikator_obiektu | powód | partia |
---|---|---|---|---|
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; |
Przykładowe wyniki z wydarzeń rozszerzonych
Mam nadzieję, że partia
kolumna jest wystarczająca do zidentyfikowania winowajcy, ale jeśli masz duże partie, które zawierają wiele wywołań do natywnie skompilowanych procedur i musisz wyśledzić obiekty, które konkretnie wywołują ten problem, możesz je po prostu wyszukać za pomocą object_id w odpowiednich bazach danych.
Teraz nie polecam uruchamiania wszystkich 400 000 wywołań w tekście, gdy sesja jest aktywna, ani włączania tej sesji w wysoce współbieżnym środowisku produkcyjnym — jeśli robisz to dużo, może to spowodować znaczne obciążenie. O wiele lepiej jest sprawdzić tego rodzaju aktywność w środowisku programistycznym lub pomostowym, o ile możesz poddać ją odpowiedniemu obciążeniu obejmującemu pełny cykl biznesowy.
Wniosek
Zdecydowanie zaskoczył mnie fakt, że nazewnictwo parametrów – od dawna uważane za najlepszą praktykę – zostało zamienione w najgorszą praktykę przy natywnie kompilowanych procedurach składowanych. Microsoft wie, że jest wystarczającym potencjalnym problemem, że stworzyli rozszerzone zdarzenie zaprojektowane specjalnie do jego śledzenia. Jeśli używasz protokołu OLTP w pamięci, jest to jedna rzecz, którą powinieneś mieć na swoim radarze podczas opracowywania wspierających procedur przechowywanych. Wiem, że zdecydowanie będę musiał wytrenować pamięć mięśniową od używania nazwanych parametrów.