Aaron Bertrand

How not to call Hekaton natively-compiled stored procedures

SentryOne eBooks

In these books, you will find useful, hand-picked articles that will help give insight into some of your most vexing performance problems. These articles were written by several of the SQL Server industry’s leading experts, including Paul White, Paul Randal, Jonathan Kehayias, Erin Stellato, Glenn Berry, Aaron Bertrand, and Joe Sack.

Free Download

Featured Author

Paul White is an independent SQL Server consultant specializing in performance tuning, execution plans, and the query optimizer.

Paul’s Posts

Note: This post was originally published only in our eBook, High Performance Techniques for SQL Server, Volume 2. You can find out about our eBooks here. Also note that some of these things may change with the planned enhancements to In-Memory OLTP in SQL Server 2016.

There are some habits and best practices that a lot of us develop over time with regard to Transact-SQL code. With stored procedures in particular, we strive to pass parameter values of the correct data type, and name our parameters explicitly rather than rely solely on ordinal position. Sometimes, though, we might get lazy about this: we might forget to prefix a Unicode string with N, or just list the constants or variables in order instead of specifying the parameter names. Or both.

In SQL Server 2014, if you are using In-Memory OLTP ("Hekaton") and natively compiled procedures, you might want to adjust your thinking on these things a little bit. I'll demonstrate with some code against the SQL Server 2014 RTM In-Memory OLTP Sample on CodePlex, which extends the AdventureWorks2012 sample database. (If you are going to set this up from scratch to follow along, please take a quick glance at my observations in a previous post.)

Let's take a look at the signature for the stored procedure 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

I was curious if it mattered whether the parameters were named, or if natively compiled procedures handled implicit conversions as arguments to stored procedures any better than traditional stored procedures. First I created a copy Sales.usp_InsertSpecialOffer_inmem as a traditional stored procedure – this involved merely removing the ATOMIC block and removing the NOT NULL declarations from the input parameters:

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

To minimize shifting criteria, the procedure still inserts into the In-Memory version of the table, Sales.SpecialOffer_inmem.

Then I wanted to time 100,000 calls to both copies of the stored procedure with these criteria:

  Parameters explicitly named Parameters not named
All parameters of correct data type x x
Some parameters of wrong data type x x

 
Using the following batch, copied for the traditional version of the stored procedure (simply removing _inmem from the four EXEC calls):

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

I ran each test 10 times, and here were the average durations, in milliseconds:

Traditional Stored Procedure
Parameters Average Duration
(milliseconds)
Named, proper types 72,132
Not named, proper types 72,846
Named, improper types 76,154
Not named, improper types 76,902
Natively Compiled Stored Procedure
Parameters Average Duration
(milliseconds)
Named, proper types 63,202
Not named, proper types 61,297
Named, improper types 64,560
Not named, improper types 64,288

Average duration, in milliseconds, of various call methods

With the traditional stored procedure, it is clear that using the wrong data types has a substantial impact on performance (about a 4 second difference), while not naming the parameters had a much less dramatic effect (adding about 700ms). I've always tried to follow best practices and use the right data types as well as name all parameters, and this small test seems to confirm that doing so can be beneficial.

With the natively compiled stored procedure, using the wrong data types still led to a similar drop in performance as with the traditional stored procedure. This time, though, naming the parameters didn't help out so much; in fact, it had a negative impact, adding almost two seconds to the overall duration. To be fair, this is a large number of calls in a fairly short time, but if you're trying to squeeze the absolute most bleeding-edge performance you can out of this feature, every nanosecond counts.

Discovering the Problem

How can you know if your natively compiled stored procedures are getting called with either of these "slow" methods? There's an XEvent for that! The event is called natively_compiled_proc_slow_parameter_passing, and it doesn't seem to be documented in Books Online at this time. You can create the following Extended Events session to monitor for this event:

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;

Once the session is running, you could try any of the above four calls individually, and then you can run this 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;

Depending on what you ran, you should see results similar to this:

timestamp db object_id reason batch
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;

Sample results from Extended Events

Hopefully the batch column is enough to identify the culprit, but if you have large batches that contain multiple calls to natively compiled procedures and you need to track down the objects that are specifically triggering this problem, you can simply look them up by object_id in their respective databases.

Now, I don't recommend running all 400,000 calls in the text while the session is active, or turning this session on in a highly concurrent, production environment – if you are doing this a lot, it can cause some significant overhead. You are much better off checking for this kind of activity in your development or staging environment, as long as you can subject it to a proper workload covering a full business cycle.

Conclusion

I was definitely surprised by the fact that naming parameters – long considered a best practice – has been turned into a worst practice with natively-compiled stored procedures. And it is known by Microsoft to be enough of a potential problem that they created an Extended Event designed specifically to track it. If you are using In-Memory OLTP, this is one thing you should keep on your radar as you develop supporting stored procedures. I know I am definitely going to have to un-train my muscle memory from using named parameters.