For this post, I wanted to cover some of the indexing subtleties for memory-optimized tables, with an accent on columnstore indexes
Let’s create a memory-optimized table:
1 2 3 4 5 6 7 |
CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL ,[col1] [CHAR](100) NOT NULL ,PRIMARY KEY NONCLUSTERED HASH ([PK]) WITH (BUCKET_COUNT = 1000) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); |
Now, let’s attempt to create a NONCLUSTERED COLUMNSTORE INDEX:
1 2 |
ALTER TABLE dbo.InMemADD ADD INDEX NCCI_InMem NONCLUSTERED COLUMNSTORE (col1); |
Msg 10794, Level 16, State 76, Line 76
The feature ‘NONCLUSTERED COLUMNSTORE’ is not supported with memory optimized tables.
It fails because we can only create a CLUSTERED columnstore index (CCI). For 25 years, Microsoft SQL Server differentiated between indexes that physically ordered data on storage (CLUSTERED) and those that did not (NONCLUSTERED). Unfortunately, they chose to ignore that pattern when creating the syntax for memory-optimized tables; using the word CLUSTERED is required when creating a columnstore index on memory-optimized tables.
Can we create a clustered columnstore index on a memory-optimized table that is defined as SCHEMA_ONLY?
Only one way to find out:
1 2 3 4 5 6 7 8 9 10 11 12 |
DROP TABLE IF EXISTS [dbo].[InMem]; GO CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL ,[col1] [CHAR](100) NOT NULL ,PRIMARY KEY NONCLUSTERED HASH ([PK]) WITH (BUCKET_COUNT = 1000) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY); ALTER TABLE dbo.InMem ADD INDEX CCI_InMem CLUSTERED COLUMNSTORE |
Msg 35320, Level 16, State 1, Line 39
Column store indexes are not allowed on tables for which the durability option SCHEMA_ONLY is specified.
That won’t work, so let’s create our table with SCHEMA_AND_DATA:
1 2 3 4 5 6 7 8 9 |
DROP TABLE IF EXISTS [dbo].[InMem]; GO CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL PRIMARY KEY NONCLUSTERED ,[col1] [CHAR](100) NOT NULL, ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO |
Now, let’s create a clustered columnstore index:
1 |
ALTER TABLE dbo.InMem ADD INDEX CCI_InMem CLUSTERED COLUMNSTORE |
Success! Let’s attempt to create a NONCLUSTERED index….
1 |
ALTER TABLE dbo.InMem ADD INDEX IX_Index1 (col1); |
Msg 10794, Level 16, State 15, Line 117
The operation ‘ALTER TABLE’ is not supported with memory optimized tables that have a column store index.
Ooops – no can do. Once you add a clustered columnstore index to a memory-optimized table, the schema is totally locked down.
What about if we create the CCI and nonclustered index inline?
1 2 3 4 5 6 7 8 9 10 11 12 |
DROP TABLE IF EXISTS [dbo].[InMem]; GO CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL ,[col1] [CHAR](100) NOT NULL ,PRIMARY KEY NONCLUSTERED HASH ([PK]) WITH (BUCKET_COUNT = 1000) ,INDEX CCI_InMem CLUSTERED COLUMNSTORE ,INDEX IX_InMem1 (col1) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO |
Awesome! We’ve proven that we can create both clustered columnstore and nonclustered indexes, but we must create them inline.
Now that we’ve got our indexes created, let’s try to add a column:
1 |
ALTER TABLE dbo.InMem ADD col2 INT NULL; |
Msg 12349, Level 16, State 1, Line 68
Operation not supported for memory optimized tables having columnstore index.
Hey, when I said that the schema is locked down once you add a clustered columnstore index, I mean it!
What type of index maintenance is possible for indexes on memory-optimized tables?
For HASH indexes there is only one possible type of index maintenance, and that’s to modify/adjust the bucket count. There is zero index maintenance for RANGE/NONCLUSTERED indexes.
Let’s create a memory-optimized table with a HASH index, and verify the syntax for rebuilding the bucket count.
1 2 3 4 5 6 7 8 9 10 11 |
DROP TABLE IF EXISTS [dbo].[InMem]; GO CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL ,[col1] [CHAR](100) NOT NULL ,PRIMARY KEY NONCLUSTERED HASH ([PK]) WITH (BUCKET_COUNT = 1000) ,INDEX IX_InMem1 HASH (col1) WITH (BUCKET_COUNT = 1000) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO |
Here’s the syntax for rebuilding the bucket count for a HASH INDEX:
1 2 3 4 |
ALTER TABLE dbo.InMem ALTER INDEX IX_InMem1 REBUILD WITH(BUCKET_COUNT = 1001) GO |
We can add a column, as long as we don’t have a CCI in place:
1 2 |
ALTER TABLE dbo.InMem ADD col2 INT NULL; GO |
How about trying to rebuild the bucket count if we created the memory-optimized table with inline CCI and HASH indexes?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
DROP TABLE IF EXISTS [dbo].[InMem]; GO CREATE TABLE [dbo].[InMem] ( [PK] [INT] IDENTITY(1, 1) NOT NULL ,[col1] [CHAR](100) NOT NULL ,PRIMARY KEY NONCLUSTERED HASH ([PK]) WITH (BUCKET_COUNT = 1000) ,INDEX CCI_InMem CLUSTERED COLUMNSTORE ,INDEX IX_InMem1 HASH (col1) WITH (BUCKET_COUNT = 1000) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO ALTER TABLE dbo.InMem ALTER INDEX IX_InMem1 REBUILD WITH(BUCKET_COUNT = 1001) GO |
Msg 10794, Level 16, State 13, Line 136
The operation ‘ALTER TABLE’ is not supported with memory optimized tables that have a column store index.
You can’t rebuild that index if you also have a columnstore index on the table. We would have to drop the columnstore index, reconfigure the bucket count for the HASH index, and then recreate the columnstore index. Both the drop and the create of the columnstore index will be fully logged, and executed serially. Not a huge problem if the amount of data is not too large, but it’s a potentially much larger problem if you’ve got a lot of data.
We can create a clustered columnstore index on a #temp table (on-disk):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
DROP TABLE IF EXISTS #tempgo GO CREATE TABLE #temp ( col1 INT NOT NULL ); GO -- succeeds CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_InMem ON #temp (col1); --OR clustered, which also succeeds CREATE CLUSTERED COLUMNSTORE INDEX NCCI_InMem ON #temp; |
We can create multiple indexes with a single command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
DROP TABLE IF EXISTS [dbo].[InMemLOB]; GO CREATE TABLE [dbo].[InMemLOB] ( [OrderId] [INT] IDENTITY NOT NULL ,[StoreID] INT NOT NULL ,[Notes1] [VARCHAR](8000) NULL ,[ValidFrom] [DATETIME2](7) NOT NULL ,[ValidTo] [DATETIME2](7) NOT NULL ,CONSTRAINT [PK_InMemLOB_OrderID] PRIMARY KEY NONCLUSTERED (OrderId) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO ALTER TABLE [InMemLOB] ADD INDEX IX_1 (OrderId) ,INDEX IX_2 (OrderId) ,INDEX IX_3 (OrderId) ,INDEX IX_4 (OrderId) ,INDEX IX_5 (OrderId) ,INDEX IX_6 (OrderId) ,INDEX IX_7 (OrderId) |
Can we create a columnstore index on a memory-optimized table variable?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
DROP TYPE IF EXISTS dbo.typeTableMem GO CREATE TYPE dbo.typeTableMem AS TABLE ( [PK] INT NOT NULL PRIMARY KEY NONCLUSTERED ,[col1] NVARCHAR(255) NOT NULL ) WITH (MEMORY_OPTIMIZED = ON); GO DECLARE @Table1 dbo.typeTableMem; -- fails--Msg 102, Level 15, State 1--Incorrect syntax near '@Table1'. ALTER TABLE @Table1 ADD INDEX CCI_InMem CLUSTERED COLUMNSTORE |
Create a table that includes a LOB column with a MAX datatype, then add a clustered columnstore index:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
DROP TABLE IF EXISTS [dbo].[InMemLOB]; GO CREATE TABLE [dbo].[InMemLOB] ( [OrderId] [INT] IDENTITY NOT NULL ,[StoreID] INT NOT NULL ,[CustomerID] INT NOT NULL ,[OrderDate] [DATETIME] NOT NULL ,[DeliveryDate] DATETIME NULL ,[Amount] FLOAT NOT NULL ,[Notes] [NVARCHAR](MAX) NULL ,[ValidFrom] [DATETIME2](7) NOT NULL ,[ValidTo] [DATETIME2](7) NOT NULL ,CONSTRAINT [PK_InMemLOB_OrderID] PRIMARY KEY NONCLUSTERED (OrderId) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO ALTER TABLE dbo.[InMemLOB] ADD INDEX CCI_InMemLOB CLUSTERED COLUMNSTORE |
Msg 35343, Level 16, State 1, Line 22 The statement failed. Column ‘Notes’ has a data type that cannot participate in a columnstore index. Omit column ‘Notes’.
Msg 1750, Level 16, State 1, Line 22 Could not create constraint or index. See previous errors.
For memory-optimized tables, LOB columns prevent creation of a clustered columnstore index.
Now let’s try creating a table using CHAR(8000). Astute readers will notice that the following table would create rows that are 32,060 bytes wide – this would fail for on-disk tables, but is perfectly valid for memory-optimized tables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
DROP TABLE IF EXISTS [dbo].[InMemLOB]; GO CREATE TABLE [dbo].[InMemLOB] ( [OrderId] [INT] IDENTITY NOT NULL ,[StoreID] INT NOT NULL ,[CustomerID] INT NOT NULL ,[OrderDate] [DATETIME] NOT NULL ,[DeliveryDate] DATETIME NULL ,[Amount] FLOAT ,[Notes1] [CHAR](8000) NULL ,[Notes2] [CHAR](8000) NULL ,[Notes3] [CHAR](8000) NULL ,[Notes4] [CHAR](8000) NULL ,[ValidFrom] [DATETIME2](7) NOT NULL ,[ValidTo] [DATETIME2](7) NOT NULL ,CONSTRAINT [PK_InMemLOB_OrderID] PRIMARY KEY NONCLUSTERED (OrderId) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO |
Msg 41833, Level 16, State 1, Line 29 Columnstore index ‘CCI_InMemLOB’ cannot be created, because table ‘InMemLOB’ has columns stored off-row.
Columnstore indexes can only be created on memory-optimized table if the columns fit within the 8060 byte limit for in-row data.
Reduce the size of the columns to fit within 8060 bytes.
Create a table with non-MAX LOB columns, but they are stored on-row, then add a clustered columnstore index:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
DROP TABLE IF EXISTS [dbo].[InMemLOB]; GO CREATE TABLE [dbo].[InMemLOB] ( [OrderId] [INT] IDENTITY NOT NULL ,[StoreID] INT NOT NULL ,[Notes1] [CHAR](8000) NULL ,[ValidFrom] [DATETIME2](7) NOT NULL ,[ValidTo] [DATETIME2](7) NOT NULL ,CONSTRAINT [PK_InMemLOB_OrderID] PRIMARY KEY NONCLUSTERED (OrderId) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); GO -- works ALTER TABLE InMemLOB ADD INDEX IndOrders_StoreID (StoreID); GO -- works ALTER TABLE dbo.[InMemLOB] ADD INDEX CCI_InMemLOB CLUSTERED COLUMNSTORE GO --Add data to table: SET NOCOUNT ON GO INSERT dbo.[InMemLOB] SELECT 3 AS [StoreID] ,'ABC' AS [Notes1] ,GETDATE() AS [ValidFrom] ,GETDATE() AS [ValidTo] GO 1000 SELECT * FROM dbo.[InMemLOB] |
Let’s create a natively compiled module that selects from this table:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
DROP PROCEDURE IF EXISTS dbo.InMem_native_sp GO CREATE PROCEDURE dbo.InMem_native_sp WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER AS BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english') SELECT [OrderId] ,[StoreID] ,[Notes1] ,[ValidFrom] ,[ValidTo] FROM dbo.[InMemLOB]; END; GO |
ENABLE “Actual Plan” and SELECT – which index is used?
1 2 3 4 5 6 7 |
SELECT [OrderId] ,[StoreID] ,[Notes1] ,[ValidFrom] ,[ValidTo] FROM dbo.[InMemLOB]; |
Now highlight the EXEC statement, and click “Estimated Plan” – which index is used?
1 |
EXEC dbo.InMem_native_sp |
The SELECT statement uses the columnstore index, but the natively compiled procedure does not (that’s because natively compiled procedures ignore columnstore indexes).
Summing up
In this post, we’ve covered some of the finer points of indexing memory-optimized tables. Never know when they might come in handy….
Pingback: Memory-Optimized Table Maintenance – Curated SQL