Database
 sql >> Base de Dados >  >> RDS >> Database

Como não chamar procedimentos armazenados compilados nativamente do Hekaton


Observação:Este post foi publicado originalmente apenas em nosso eBook, High Performance Techniques for SQL Server, Volume 2. Você pode descobrir mais sobre nossos eBooks aqui. Observe também que algumas dessas coisas podem mudar com os aprimoramentos planejados para o OLTP na memória no SQL Server 2016.

Existem alguns hábitos e práticas recomendadas que muitos de nós desenvolvemos ao longo do tempo em relação ao código Transact-SQL. Com procedimentos armazenados em particular, nos esforçamos para passar valores de parâmetro do tipo de dados correto e nomear nossos parâmetros explicitamente, em vez de confiar apenas na posição ordinal. Às vezes, porém, podemos ficar preguiçosos com isso:podemos esquecer de prefixar uma string Unicode com N , ou apenas liste as constantes ou variáveis ​​em ordem em vez de especificar os nomes dos parâmetros. Ou ambos.

No SQL Server 2014, se você estiver usando OLTP na memória ("Hekaton") e procedimentos compilados nativamente, convém ajustar um pouco seu pensamento sobre essas coisas. Demonstrarei com algum código no exemplo de OLTP in-memory do SQL Server 2014 RTM no CodePlex, que estende o banco de dados de exemplo AdventureWorks2012. (Se você for configurar isso do zero para acompanhar, dê uma olhada rápida nas minhas observações em um post anterior.)

Vamos dar uma olhada na assinatura do procedimento armazenado 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

Eu estava curioso se importava se os parâmetros eram nomeados ou se os procedimentos compilados nativamente manipulavam conversões implícitas como argumentos para procedimentos armazenados melhor do que os procedimentos armazenados tradicionais. Primeiro criei uma cópia Sales.usp_InsertSpecialOffer_inmem como um procedimento armazenado tradicional - isso envolvia apenas remover o ATOMIC bloquear e remover o NOT NULL declarações dos 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 os critérios de deslocamento, o procedimento ainda é inserido na versão In-Memory da tabela, Sales.SpecialOffer_inmem.

Então eu queria cronometrar 100.000 chamadas para ambas as cópias do procedimento armazenado com estes critérios:
Parâmetros explicitamente nomeados Parâmetros não nomeados
Todos os parâmetros do tipo de dados correto x x
Alguns parâmetros de tipo de dados incorreto x x


Usando o seguinte lote, copiado para a versão tradicional do procedimento armazenado (simplesmente removendo _inmem dos quatro EXEC chamadas):
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

Executei cada teste 10 vezes e aqui estavam as durações médias, em milissegundos:
Procedimento armazenado tradicional
Parâmetros Duração média
(milissegundos)
Tipos apropriados e nomeados 72.132
Sem nome, tipos próprios 72.846
Tipos nomeados e impróprios 76.154
Sem nome, tipos impróprios 76.902
Procedimento armazenado compilado nativamente
Parâmetros Duração média
(milissegundos)
Tipos apropriados e nomeados 63.202
Sem nome, tipos próprios 61.297
Tipos nomeados e impróprios 64.560
Sem nome, tipos impróprios 64.288

Duração média, em milissegundos, de vários métodos de chamada

Com o procedimento armazenado tradicional, fica claro que usar os tipos de dados errados tem um impacto substancial no desempenho (cerca de 4 segundos de diferença), enquanto não nomear os parâmetros teve um efeito muito menos dramático (adicionando cerca de 700 ms). Sempre tentei seguir as práticas recomendadas e usar os tipos de dados corretos, bem como nomear todos os parâmetros, e esse pequeno teste parece confirmar que isso pode ser benéfico.

Com o procedimento armazenado compilado nativamente, o uso de tipos de dados incorretos ainda leva a uma queda de desempenho semelhante à do procedimento armazenado tradicional. Desta vez, porém, nomear os parâmetros não ajudou muito; na verdade, teve um impacto negativo, acrescentando quase dois segundos à duração total. Para ser justo, este é um grande número de chamadas em um período de tempo bastante curto, mas se você estiver tentando espremer o desempenho mais avançado possível desse recurso, cada nanossegundo conta.

Descobrindo o problema


Como você pode saber se seus procedimentos armazenados compilados nativamente estão sendo chamados com um desses métodos "lentos"? Existe um XEvent para isso! O evento é chamado de natively_compiled_proc_slow_parameter_passing , e não parece estar documentado nos Manuais Online neste momento. Você pode criar a seguinte sessão de Eventos Estendidos para monitorar 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;

Quando a sessão estiver em execução, você poderá tentar qualquer uma das quatro chamadas acima individualmente e, em seguida, poderá executar 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;

Dependendo do que você executou, você deve ver resultados semelhantes a este:
carimbo de data e hora db object_id motivo lote
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;

Exemplos de resultados de eventos estendidos

Espero que o lote coluna é suficiente para identificar o culpado, mas se você tiver grandes lotes que contêm várias chamadas para procedimentos compilados nativamente e precisar rastrear os objetos que estão desencadeando especificamente esse problema, basta procurá-los por object_id em seus respectivos bancos de dados.

Agora, não recomendo executar todas as 400.000 chamadas no texto enquanto a sessão estiver ativa ou ativar essa sessão em um ambiente de produção altamente simultâneo – se você estiver fazendo muito isso, poderá causar uma sobrecarga significativa. É muito melhor verificar esse tipo de atividade em seu ambiente de desenvolvimento ou de teste, desde que possa sujeitá-lo a uma carga de trabalho adequada que cubra um ciclo de negócios completo.

Conclusão


Fiquei definitivamente surpreso com o fato de que a nomenclatura de parâmetros – há muito considerada uma prática recomendada – foi transformada na pior prática com procedimentos armazenados compilados nativamente. E é conhecido pela Microsoft que é um problema potencial suficiente que eles criaram um Evento Estendido projetado especificamente para rastreá-lo. Se você estiver usando o OLTP na memória, isso é algo que você deve manter em seu radar ao desenvolver procedimentos armazenados de suporte. Eu sei que definitivamente vou ter que destreinar minha memória muscular usando parâmetros nomeados.