In-Memory OLTP (Hekaton) and Sort Warnings

In-Memory OLTP is all about In-Memory. But that’s only half the truth. In today’s blog posting I want to show you that even In-Memory OLTP can cause disk activity when you just read data from memory. The problem here is inaccurate statistics in combination with explicit sort operators in the execution plan.

The problem with the Sort Operator

As you might know, a sort operator needs a so-called Memory Grant for its execution. This memory area is used to perform the sorting of the incoming records in the execution plan. The size of this memory grant is based on the number of the estimated rows. During Cardinality Estimation the query optimizer estimates how many rows are expected by each operator in the execution plan.

Back in 2011 I wrote a blog posting that showed how a sort operator can spill over to TempDb when that estimate is wrong. And the same thing can happen in In-Memory OLTP: you have a sort operator in the execution plan, and when the estimated number of rows is wrong, the sort operator will spill over to TempDb! Let’s try to reproduce this scenario.

TempDb spills with In-Memory OLTP

Let’s create a new database which is configured with an In-Memory OLTP file group.

-- Create new database
CREATE DATABASE InMemoryOLTP
GO

-- Add MEMORY_OPTIMIZED_DATA filegroup to the database.
ALTER DATABASE InMemoryOLTP
ADD FILEGROUP InMemoryOLTPFileGroup CONTAINS MEMORY_OPTIMIZED_DATA
GO

USE InMemoryOLTP
GO

-- Add a new file to the previously created file group
ALTER DATABASE InMemoryOLTP ADD FILE
(
	NAME = N'InMemoryOLTPContainer', 
	FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL12.SQL2014\MSSQL\DATA\InMemoryOLTPContainer'
)
TO FILEGROUP [InMemoryOLTPFileGroup]
GO

In the next step I create a new memory optimized table.

-- Create a test table
CREATE TABLE Table1
(
	Column1 INT IDENTITY,
	Column2 INT
	CONSTRAINT pk_Column1 PRIMARY KEY NONCLUSTERED HASH (Column1) WITH (BUCKET_COUNT = 1) 
) WITH
(
	MEMORY_OPTIMIZED = ON,
	DURABILITY = SCHEMA_ONLY
)
GO

As you can see from the table definition, I have created a hash index on the column Column1. And because it is a hash index, you also have to specify how many hash buckets you want to have in the underlying hash table. In my case I just have specified 1 hash bucket, which is very, very bad idea. With just one hash bucket you will get a huge amount of hash collisions when you insert records into that table. Normally you should have as many hash buckets as you have unique values in the column on which you have defined your hash index. The following code inserts 14001 records into the previously created table.

-- Insert 14001 records
INSERT INTO Table1(Column2) VALUES (1)

SELECT TOP 14000 IDENTITY(INT, 1, 1) AS n INTO #Nums
FROM
master.dbo.syscolumns sc1

INSERT INTO Table1 (Column2)
SELECT 2 FROM #nums
DROP TABLE #nums
GO

You can check the number of hash collisions through the DMV (Dynamic Management View) sys.dm_db_xtp_hash_index_stats. As you can see from the output of this DMV, you have 140001 records in the one and only hash bucket of this hash index. And now let’s run a SELECT statement that includes an explicit sort operator in the execution plan.

-- The sort operator in this execution plan spills over to TempDb!
SELECT * FROM Table1
ORDER BY Column1

When you now look at the execution, you can see that the sort operator is spilled over to TempDb.

A Sort Warning with In-Memory OLTP (Hekaton)

This happens because of the inaccurate statistics on the hash index. When you look at the Index Scan (NonClusteredHash) operator in the execution plan, you can see that the Query Optimizer just expected one row from our hash index. In reality we returned 140001 rows.

A wrong Cardinality Estimation with too less Hash Buckets

The estimation of the Index Scan (NonClusteredHash) operator is always based on the number of hash buckets in the underlying hash index. The assumption that the Query Optimizer makes here is that you have no hash collisions – which in our case is not correct. Therefore the memory grant for the sort operator is dimensioned according to that incorrect estimate, and has to spill over to TempDb. On my system this query takes around 80ms , which is a very long time for an In-Memory technology.

How can you fix that problem? Drop your table, and carefully plan the number of hash buckets in your hash index. Welcome to the wonderful world of In-Memory OLTP…

Summary

When you use hash indexes with In-Memory OLTP you have to carefully design your hash bucket count for your hash indexes. When they are wrong, the performance will suffer. I also already blogged more than a year ago, about how hash collisions will decrease the performance of In-Memory OLTP – even with no TempDb spills!

The moral of the story: with hash collisions in your hash indexes you can’t expect an amazingly fast performance with In-Memory OLTP, because they introduce a huge overhead and also affect the cardinality estimation.

Like or share to get the source code.

Thanks for your time,

-Klaus

Leave a Comment

Your email address will not be published. Required fields are marked *