Nota:questo post è stato originariamente pubblicato solo nel nostro eBook, High Performance Techniques for SQL Server, Volume 2. Puoi trovare informazioni sui nostri eBook qui. Tieni inoltre presente che alcune di queste cose potrebbero cambiare con i miglioramenti pianificati a OLTP in memoria in SQL Server 2016.
Ci sono alcune abitudini e best practice che molti di noi sviluppano nel tempo per quanto riguarda il codice Transact-SQL. Con le stored procedure in particolare, ci sforziamo di passare i valori dei parametri del tipo di dati corretto e denominare i nostri parametri in modo esplicito anziché basarsi esclusivamente sulla posizione ordinale. A volte, però, potremmo diventare pigri per questo:potremmo dimenticare di anteporre a una stringa Unicode N
o semplicemente elencare le costanti o le variabili in ordine invece di specificare i nomi dei parametri. O entrambi.
In SQL Server 2014, se utilizzi OLTP in memoria ("Hekaton") e procedure compilate in modo nativo, potresti voler modificare un po' il tuo pensiero su queste cose. Dimostrerò con del codice rispetto all'esempio OLTP in memoria RTM di SQL Server 2014 su CodePlex, che estende il database di esempio AdventureWorks2012. (Se hai intenzione di configurarlo da zero per continuare, dai una rapida occhiata alle mie osservazioni in un post precedente.)
Diamo un'occhiata alla firma per la procedura memorizzata 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
Ero curioso di sapere se importava se i parametri fossero denominati o se le procedure compilate in modo nativo gestissero le conversioni implicite come argomenti per le procedure memorizzate meglio delle procedure memorizzate tradizionali. Per prima cosa ho creato una copia Sales.usp_InsertSpecialOffer_inmem
come una procedura memorizzata tradizionale:ciò comportava semplicemente la rimozione di ATOMIC
bloccare e rimuovere il NOT NULL
dichiarazioni dai parametri di input:
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
Per ridurre al minimo i criteri di spostamento, la procedura inserisce comunque nella versione In-Memory della tabella, Sales.SpecialOffer_inmem.
Quindi ho voluto cronometrare 100.000 chiamate a entrambe le copie della procedura memorizzata con questi criteri:
Parametri denominati in modo esplicito | Parametri senza nome | |
---|---|---|
Tutti i parametri del tipo di dati corretto | x | x |
Alcuni parametri di tipo di dati errato | x | x |
Utilizzando il seguente batch, copiato per la versione tradizionale della stored procedure (semplicemente rimuovendo _inmem
dai quattro EXEC
chiamate):
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
Ho eseguito ogni test 10 volte, ed ecco le durate medie, in millisecondi:
Procedura archiviata tradizionale | |
---|---|
Parametri | Durata media (millisecondi) |
Tipi propri e con nome | 72.132 |
Tipi propri non denominati | 72.846 |
Tipi impropri con nome | 76.154 |
Tipi non denominati, impropri | 76.902 |
Procedura memorizzata compilata in modo nativo | |
Parametri | Durata media (millisecondi) |
Tipi propri e con nome | 63.202 |
Tipi propri non denominati | 61.297 |
Tipi impropri con nome | 64.560 |
Tipi non denominati, impropri | 64.288 |
Durata media, in millisecondi, di vari metodi di chiamata
Con la tradizionale procedura memorizzata, è chiaro che l'utilizzo di tipi di dati errati ha un impatto sostanziale sulle prestazioni (circa 4 secondi di differenza), mentre non nominare i parametri ha avuto un effetto molto meno drammatico (aggiungendo circa 700 ms). Ho sempre cercato di seguire le migliori pratiche e di utilizzare i tipi di dati corretti, nonché di nominare tutti i parametri e questo piccolo test sembra confermare che ciò può essere vantaggioso.
Con la stored procedure compilata in modo nativo, l'utilizzo di tipi di dati errati portava comunque a un calo delle prestazioni simile a quello della stored procedure tradizionale. Questa volta, però, nominare i parametri non ha aiutato molto; infatti ha avuto un impatto negativo, aggiungendo quasi due secondi alla durata complessiva. Ad essere onesti, si tratta di un gran numero di chiamate in un tempo abbastanza breve, ma se stai cercando di ottenere le prestazioni più all'avanguardia in assoluto che puoi ottenere da questa funzione, ogni nanosecondo conta.
Scoprire il problema
Come puoi sapere se le tue stored procedure compilate in modo nativo vengono chiamate con uno di questi metodi "lenti"? C'è un XEvent per questo! L'evento si chiama natively_compiled_proc_slow_parameter_passing
, e al momento non sembra essere documentato nella documentazione in linea. Puoi creare la seguente sessione di eventi estesi da monitorare per questo evento:
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;
Una volta che la sessione è in esecuzione, puoi provare una qualsiasi delle quattro chiamate precedenti individualmente, quindi puoi eseguire questa query:
;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;
A seconda di cosa hai eseguito, dovresti vedere risultati simili a questo:
timestamp | db | id_oggetto | motivo | lotto |
---|---|---|---|---|
01-07-2014 16:23:14 | AdventureWorks2012 | 2087678485 | parametri_nominati | 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; |
01-07-2014 16:23:22 | AdventureWorks2012 | 2087678485 | conversione_parametro | 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; |
Risultati di esempio da eventi estesi
Si spera che il batch
colonna è sufficiente per identificare il colpevole, ma se hai batch di grandi dimensioni che contengono più chiamate a procedure compilate in modo nativo e devi rintracciare gli oggetti che stanno attivando specificamente questo problema, puoi semplicemente cercarli da object_id
nei rispettivi database.
Ora, non consiglio di eseguire tutte le 400.000 chiamate nel testo mentre la sessione è attiva o di attivare questa sessione in un ambiente di produzione altamente simultaneo:se lo fai spesso, può causare un sovraccarico significativo. È molto meglio controllare questo tipo di attività nel tuo ambiente di sviluppo o di staging, purché tu possa sottoporlo a un carico di lavoro adeguato che copra un intero ciclo economico.
Conclusione
Sono stato decisamente sorpreso dal fatto che la denominazione dei parametri, a lungo considerata una best practice, sia stata trasformata in una pratica peggiore con le stored procedure compilate in modo nativo. Ed è noto a Microsoft per essere un potenziale problema sufficiente a creare un evento esteso progettato specificamente per tenerne traccia. Se si utilizza OLTP in memoria, questa è una cosa da tenere sotto controllo durante lo sviluppo di stored procedure di supporto. So che dovrò sicuramente non allenare la mia memoria muscolare dall'uso di parametri denominati.