Nota:Esta publicación se publicó originalmente solo en nuestro libro electrónico, Técnicas de alto rendimiento para SQL Server, Volumen 2. Puede encontrar información sobre nuestros libros electrónicos aquí. También tenga en cuenta que algunas de estas cosas pueden cambiar con las mejoras planificadas para In-Memory OLTP en SQL Server 2016.
Hay algunos hábitos y mejores prácticas que muchos de nosotros desarrollamos con el tiempo con respecto al código Transact-SQL. Con los procedimientos almacenados en particular, nos esforzamos por pasar valores de parámetro del tipo de datos correcto y nombrar nuestros parámetros explícitamente en lugar de depender únicamente de la posición ordinal. A veces, sin embargo, podemos volvernos perezosos con esto:podemos olvidarnos de prefijar una cadena Unicode con N
, o simplemente enumere las constantes o variables en orden en lugar de especificar los nombres de los parámetros. O ambos.
En SQL Server 2014, si usa OLTP en memoria ("Hekaton") y procedimientos compilados de forma nativa, es posible que desee ajustar un poco su forma de pensar sobre estas cosas. Hago una demostración con código en el ejemplo de OLTP en memoria RTM de SQL Server 2014 en CodePlex, que amplía la base de datos de ejemplo AdventureWorks2012. (Si va a configurar esto desde cero para seguir, eche un vistazo rápido a mis observaciones en una publicación anterior).
Echemos un vistazo a la firma del procedimiento almacenado 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
Tenía curiosidad si importaba si los parámetros tenían nombre o si los procedimientos compilados de forma nativa manejaban conversiones implícitas como argumentos para los procedimientos almacenados mejor que los procedimientos almacenados tradicionales. Primero creé una copia Sales.usp_InsertSpecialOffer_inmem
como un procedimiento almacenado tradicional:esto implicó simplemente eliminar el ATOMIC
bloquear y eliminar el NOT NULL
declaraciones de los parámetros de entrada:
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
Para minimizar el cambio de criterio, el procedimiento aún se inserta en la versión en memoria de la tabla, Sales.SpecialOffer_inmem.
Luego quería programar 100 000 llamadas a ambas copias del procedimiento almacenado con estos criterios:
Parámetros nombrados explícitamente | Parámetros sin nombre | |
---|---|---|
Todos los parámetros del tipo de datos correcto | x | x |
Algunos parámetros de tipo de datos incorrecto | x | x |
Utilizando el siguiente lote, copiado para la versión tradicional del procedimiento almacenado (simplemente eliminando _inmem
de los cuatro EXEC
llamadas):
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
Realicé cada prueba 10 veces y estas son las duraciones promedio, en milisegundos:
Procedimiento almacenado tradicional | |
---|---|
Parámetros | Duración media (milisegundos) |
Tipos propios con nombre | 72.132 |
Sin nombre, tipos propios | 72.846 |
Tipos impropios con nombre | 76.154 |
Sin nombre, tipos inadecuados | 76.902 |
Procedimiento almacenado compilado de forma nativa | |
Parámetros | Duración media (milisegundos) |
Tipos propios con nombre | 63.202 |
Sin nombre, tipos propios | 61.297 |
Tipos impropios con nombre | 64.560 |
Sin nombre, tipos inadecuados | 64.288 |
Duración promedio, en milisegundos, de varios métodos de llamada
Con el procedimiento almacenado tradicional, está claro que el uso de tipos de datos incorrectos tiene un impacto sustancial en el rendimiento (alrededor de una diferencia de 4 segundos), mientras que no nombrar los parámetros tuvo un efecto mucho menos dramático (agregando alrededor de 700 ms). Siempre he tratado de seguir las mejores prácticas y usar los tipos de datos correctos, así como nombrar todos los parámetros, y esta pequeña prueba parece confirmar que hacerlo puede ser beneficioso.
Con el procedimiento almacenado compilado de forma nativa, el uso de tipos de datos incorrectos aún conducía a una caída similar en el rendimiento que con el procedimiento almacenado tradicional. Esta vez, sin embargo, nombrar los parámetros no ayudó mucho; de hecho, tuvo un impacto negativo, agregando casi dos segundos a la duración total. Para ser justos, esta es una gran cantidad de llamadas en un tiempo bastante corto, pero si está tratando de exprimir el rendimiento más avanzado que pueda con esta función, cada nanosegundo cuenta.
Descubrir el problema
¿Cómo puede saber si sus procedimientos almacenados compilados de forma nativa están siendo llamados con cualquiera de estos métodos "lentos"? ¡Hay un XEvent para eso! El evento se llama natively_compiled_proc_slow_parameter_passing
, y no parece estar documentado en Books Online en este momento. Puede crear la siguiente sesión de eventos extendidos para monitorear este 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 vez que la sesión se está ejecutando, puede probar cualquiera de las cuatro llamadas anteriores individualmente y luego puede ejecutar esta consulta:
;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;
Según lo que haya ejecutado, debería ver resultados similares a este:
marca de tiempo | db | objeto_id | motivo | lote |
---|---|---|---|---|
2014-07-01 16:23:14 | AdventureWorks2012 | 2087678485 | parámetros_nombrados | 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 | conversión_parámetro | 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; |
Resultados de muestra de eventos extendidos
Esperemos que el batch
La columna es suficiente para identificar al culpable, pero si tiene lotes grandes que contienen varias llamadas a procedimientos compilados de forma nativa y necesita rastrear los objetos que desencadenan específicamente este problema, simplemente puede buscarlos mediante object_id
en sus respectivas bases de datos.
Ahora, no recomiendo ejecutar las 400,000 llamadas en el texto mientras la sesión está activa, o encender esta sesión en un entorno de producción altamente concurrente; si hace esto con mucha frecuencia, puede causar una sobrecarga significativa. Es mucho mejor que verifique este tipo de actividad en su entorno de desarrollo o ensayo, siempre que pueda someterlo a una carga de trabajo adecuada que cubra un ciclo comercial completo.
Conclusión
Definitivamente me sorprendió el hecho de que nombrar parámetros, considerado durante mucho tiempo una mejor práctica, se haya convertido en una mala práctica con los procedimientos almacenados compilados de forma nativa. Y Microsoft sabe que es un problema potencial suficiente por lo que crearon un evento extendido diseñado específicamente para rastrearlo. Si está utilizando In-Memory OLTP, esto es algo que debe tener en cuenta a medida que desarrolla procedimientos almacenados compatibles. Sé que definitivamente voy a tener que desentrenar mi memoria muscular para que no use parámetros con nombre.