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.