sql >> Base de Datos >  >> RDS >> Database

Cómo no llamar a los procedimientos almacenados compilados de forma nativa de Hekaton

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.