Adding Updating and Deleting Data in Columnstore Indexes

We will be constantly inserting new rows, and updating or deleting existing rows in our columnstore indexed tables. If you think about what this means for a columnstore index, you will realize that this comes with some unique challenges.

Inserting Data

When thinking about adding data to an existing columnstore, depending on the compression algorithms used, it might be necessary to first process all prior values in a segment before the new value can be added. These actions would make the insert very slow. Also, once the last rowgroup reaches its maximum size, a new rowgroup would have to be opened, and the compression methods to be used for its segments would have to be decided based on possibly just a single row to be inserted – this will never result in optimal choices!

Deltastore

To reduce fragmentation of the column segments and improve performance, the columnstore index uses an extra storage area that you can think of as a temporary holding area (deltastore) where new data is stored until sufficient rows have been collected to justify the effort of converting them to the columnstore format. The data in the deltastore is not stored in column oriented fashion, but in a B-tree structure, the same format used for traditional row oriented indexes. The deltastore operations are handled behind the scenes. To return the correct query results, the clustered columnstore index combines query results from both the columnstore and the deltastore.

16

Trickle Insert vs Bulk Load

Let’s examine the effect of trickle insert and bulk load by inserting different numbers of rows in a single INSERT statement, let’s now run the code below. I first insert a large number of rows (150,000) at once, followed by a smaller number of rows (5).

-- Add 150,000 rows
INSERT     dbo.BLT_BILL_TEST
SELECT TOP(150000) *
FROM       dbo.BLT_BILL;

-- Add 5 rows
INSERT     dbo.BLT_BILL_TEST
SELECT TOP(5) *
FROM       dbo.BLT_BILL;

The output of the metadata query below reveals some interesting results.

SELECT    
       OBJECT_NAME(rg.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       rg.row_group_id,
       rg.state,
       rg.state_description,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM      
       sys.column_store_row_groups AS rg
       INNER JOIN sys.indexes AS i
       ON i.object_id = rg.object_id AND i.index_id = rg.index_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY  
       TableName, IndexName, rg.partition_number, rg.row_group_id;

17

During a large bulk load, most of the rows go directly to the columnstore without passing through the deltastore. Some rows at the end of the bulk load might be too few in number to meet the minimum size of a rowgroup which is 102,400 rows. When this happens, the final rows go to the deltastore instead of the columnstore. Any insert of 102,399 or fewer rows is considered a “trickle insert”. For “trickle insert”, all of the rows go directly to the deltastore. These rows are added to an open deltastore if one is available (and not locked), or else a new deltastore rowgroup is created for them.

As you can see, the 150,000 rows that were added in the first query were inserted into a new rowgroup, and this rowgroup has state 3 (COMPRESSED), indicating that this rowgroup is not a deltastore but a regular columnstore rowgroup. The 5 rows of the second insert were then added to the open deltastore rowgroup.

Bulk loading is the most performant way to move data into a columnstore index because it operates on batches of rows. Bulk loading fills rowgroups to maximum capacity and compresses them directly into the columnstore.

To perform a bulk load, you can use tools like bcp utility, the ‘fast-load’ option with Integration Services (SSIS) OLEDB destination, or the T-SQL BULK INSERT statement or select rows from a staging table.

When designing a load process that adds a lot of data, make sure that the bulk load method is used to ensure the majority of data is bulk loaded, and only small amounts of rows are trickle inserted in the deltastore. Reading a single deltastore when processing a columnstore index will not significantly affect performance.

Deleting Data

Let’s examine the effect of deleting data from a columnstore index, let’s now run the following code.

-- Delete ten percent of the rows, throughout all rowgroups
DELETE FROM dbo.BLT_BILL_TEST
WHERE [TRAN_UNO] % 10 = 0;

Let’s examine the output of the metadata query below.

SELECT    
       OBJECT_NAME(rg.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       rg.row_group_id,
       rg.state,
       rg.state_description,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM      
       sys.column_store_row_groups AS rg
       INNER JOIN sys.indexes AS i
       ON i.object_id = rg.object_id AND i.index_id = rg.index_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY  
       TableName, IndexName, rg.partition_number, rg.row_group_id;

18

You can notice the deleted_rows column from the above result. These rows were not actually deleted from the columnstore index. Instead, SQL Server simply marks the row as deleted without removing any data. The data returned by the DMV query reflects this method of logically deleting the data without physically removing it – the total_rows of the rowgroups has not been changed, because all the rows are still there; the deleted_rows column tells us how many of these rows are no longer valid.

When rows in a columnstore index have to be marked as deleted, an extra storage structure is added to the index. This storage structure is often referred to as the “Deleted Bitmap”. The delete marks are stored in an internal, row-compressed B-tree, as a virtual two-column table storing only rowgroup number and row number of the row within that rowgroup.

Updating Data

When rows in a columnstore are updated, updating data can be considered as equivalent to deleting the old version of the data and inserting the new data. Not the rows that are still in a deltastore: these are still in a B-tree, so they can easily be processed by the regular update mechanism. But for the rows that are already compressed in columnstore rowgroups, every update is processed by marking the old row as deleted and trickle inserting the new data in a deltastore.

Let’s see this in action by running the following code which updates about 10% of the data in the table.

-- Update ten percent of the rows, throughout all rowgroups
UPDATE dbo.BLT_BILL_TEST
SET RELEASED = 'N'
WHERE [TRAN_UNO] % 5 = 0;

Let’s examine the output of the metadata query below.

19

If you observe the results, you can notice that there are now many rows marked as deleted in the compressed rowgroups. You can also see one open rowgroup, the result of adding over 158k “new” (updated) rows as trickle inserts into the deltastore.

Columnstore Indexes – Defragmentation

For columnstore index maintenance, there are two options: Reorganize and Rebuild are available.

Reorganizing the Columnstore Index

Reorganizing a clustered columnstore index can be useful after doing a large data load that did not use bulk load, or after doing a large update – in short, after any operation that causes a lot of trickle inserts. Trickle inserts go into an open deltastore, which will be closed when it reaches 1,048,576 rows. At that point, without further action, it will wait for the background tuple mover process to pick it up and compress it into columnstore format. Until it catches up, you can experience suboptimal performance from your columnstore index.

When you reorganize the index, you tell SQL Server to not wait for the tuple mover, but immediately compress all closed deltastore rowgroups for the specified index. Reorganizing a columnstore index is an online operation, which means that the index can be used normally during the process.

The tuple mover will any how kick in few minutes and done the same work in the background. You only have to reorganize a clustered columnstore index if there is a reason why you need the closed rowgroups to be compressed immediately.

Starting with SQL Server 2016 (13.x) the reorganize operation will also remove rows that have been deleted from the columnstore.

Let’s see the reorganize operation of a clustered columnstore index in action. Run the following code to reorganize the clustered columnstore index. Before and after the reorganize, it queries the rowgroup metadata;

SELECT    
       OBJECT_NAME(rg.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       rg.row_group_id,
       rg.state,
       rg.state_description,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM      
       sys.column_store_row_groups AS rg
       INNER JOIN sys.indexes AS i
       ON i.object_id = rg.object_id AND i.index_id = rg.index_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY  
       TableName, IndexName, rg.partition_number, rg.row_group_id;

-- Reorganize the index
ALTER INDEX CCI_BLT_BILL_TEST ON BLT_BILL_TEST REORGANIZE;

SELECT    
       OBJECT_NAME(rg.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       rg.row_group_id,
       rg.state,
       rg.state_description,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM      
       sys.column_store_row_groups AS rg
       INNER JOIN sys.indexes AS i
       ON i.object_id = rg.object_id AND i.index_id = rg.index_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY  
       TableName, IndexName, rg.partition_number, rg.row_group_id;

As you can see the results below, the reorganize operation reduced the number of rowgroups from 5 to 3, and saved 14.5 MB of storage. While it may appear as if the number of rows has gone down, this is not actually the case – just remember that the reported number of rows in compressed rowgroups includes the rows that are marked as deleted.

20

21

Rebuilding the Columnstore Index

For SQL Server 2016 (13.x) and later, rebuilding the columnstore index is usually not needed since REORGANIZE performs the essentials of a rebuild in the background as an online operation.

To rebuild a columnstore index, SQL Server acquires an exclusive lock on the table or partition while the rebuild occurs. The data is “offline” and unavailable during the rebuild. It re-compresses all data into the columnstore. Two copies of the columnstore index exist while the rebuild is taking place. When the rebuild is finished, SQL Server deletes the original columnstore index.

Rebuilding the entire table takes a long time if the index is large, and it requires enough disk space to store an additional copy of the index during the rebuild. Consider rebuilding a partition instead of the entire table. For partitioned tables, you do not need to rebuild the entire columnstore index because fragmentation is likely to occur in only the partitions that have been modified recently. While rebuilding, for a partitioned table, you can use the ON PARTITIONS clause to specify individual partitions.

Conclusion

Over time, modifications to data in a columnstore index will deteriorate its usefulness. Rows marked as deleted still take up space, trickle inserts and small bulk loads will not compress as effectively, and the global dictionary will never change after the initial index build; all this impacts the compression ratio, resulting in more IOs for the same query. Index maintenance is required in order to periodically restore the index to optimum usefulness.

References

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-data-loading-guidance?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/tools/bcp-utility?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/integration-services/sql-server-integration-services?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-defragmentation?view=sql-server-2017

 

Columnstore Indexes – Catalog Views

In this article, we will first take a look at how to recognize columnstore indexes in some of the generic catalog views. After that we will investigate the new catalog views that have been added just for columnstore indexes.

sys.indexes and sys.index_columns

We can find information about columnstore indexes in two generic catalog views: sys.indexes and sys.index_columns.

SELECT
       OBJECT_NAME(i.object_id) AS TableName,
       i.name AS IndexName,
       i.index_id,
       i.type,
       i.type_desc,
       i.is_unique,
       i.ignore_dup_key,
       i.is_primary_key,
       i.is_unique_constraint,
       i.fill_factor,
       i.is_disabled,
       i.has_filter
FROM
       sys.indexes AS i
WHERE
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Non-Clustered columnstore index.

13

sys.column_store_segments

sys.column_store_segments provides detailed information about the individual segments within the columnstore index.

SELECT    
       OBJECT_NAME(i.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       COALESCE(c.name, '* Internal *') AS ColumnName,
       s.segment_id,
       s.row_count,
       s.has_nulls,
       s.on_disk_size
FROM      
       sys.column_store_segments AS s
       INNER JOIN sys.partitions AS p
       ON p.hobt_id = s.hobt_id
       INNER JOIN sys.indexes AS i
       ON i.object_id = p.object_id AND i.index_id = p.index_id
       LEFT JOIN sys.index_columns AS ic
       ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.index_column_id = s.column_id
       LEFT JOIN sys.columns AS c
       ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY
       TableName, IndexName, s.column_id, p.partition_number, s.segment_id;

14

Column Name Description
segment_id ID of the rowgroup. For backward compatibility, the column name continues to be called segment_id even though this is the rowgroup ID. You can uniquely identify a segment using <hobt_id, partition_id, column_id>, <segment_id>.
row_count Number of rows in the row group.
has_nulls 1 if the column segment has null values.
on_disk_size

Size of segment in bytes. The on_disk_size column, when used in conjunction with row_count, can be used to quickly see how well (or how bad) the data in a segment was compressed.

A non-clustered columnstore index will sometimes contain additional columns that were not included in the CREATE INDEX statement. If you have not explicitly included all the columns of the table’s clustered (row-store) index, SQL Server will still add all of them to the non-clustered columnstore index. Additionally, if the table’s clustered index is a non-unique index, SQL Server will have created a hidden unique identifier that you cannot explicitly specify, but will still be added to the columnstore index. And if the table has no clustered index at all, SQL Server will add a hidden column for the RID (or Row ID, which is the location of the row in the heap). Data for each of these columns is included in sys.column_store_segments, but not in sys.index_columns. The outer join to sys.index_columns ensures that these “hidden columns” in sys.column_store_segments are still exposed.

sys.column_store_row_groups

sys.column_store_row_groups provides columnstore index information on a per-segment basis to help the administrator make system management decisions. sys.column_store_row_groups has a column for the total number of rows physically stored (including those marked as deleted) and a column for the number of rows marked as deleted. Use sys.column_store_row_groups to determine which row groups have a high percentage of deleted rows and should be rebuilt.

SELECT   
       OBJECT_NAME(rg.object_id) AS TableName,
       i.name AS IndexName,
       i.type_desc AS IndexType,
       rg.row_group_id,
       rg.state,
       rg.state_description,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM      
       sys.column_store_row_groups AS rg
       INNER JOIN sys.indexes AS i
       ON i.object_id = rg.object_id AND i.index_id = rg.index_id
WHERE     
       i.type IN (5, 6) -- 5 = Clustered columnstore index. 6 = Nonclustered columnstore index.
ORDER BY  
TableName, IndexName, rg.partition_number, rg.row_group_id;

15

Column Name Description
row_group_id The row group number associated with this row group. This is unique within the partition.
state ID number associated with the state_description.
0 = INVISIBLE
1 = OPEN
2 = CLOSED
3 = COMPRESSED
4 = TOMBSTONE
state_description Description of the persistent state of the row group:
INVISIBLE –A hidden compressed segment in the process of being built from data in a delta store. Read actions will use the delta store until the invisible compressed segment is completed. Then the new segment is made visible, and the source delta store is removed.

OPEN – A read/write row group that is accepting new records. An open row group is still in rowstore format and has not been compressed to columnstore format.
CLOSED – A row group that has been filled, but not yet compressed by the tuple mover process.
COMPRESSED – A row group that has filled and compressed.

TOMBSTONE – The tuple mover also deallocates any rowgroups in which every row has been deleted. Deallocated rowgroups are marked as TOMBSTONE.

total_rows Total rows physically stored in the row group. Some may have been deleted but they are still stored. The maximum number of rows in a row group is 1,048,576
deleted_rows Total rows in the row group marked deleted.
size_in_bytes

Size in bytes of all the data in this row group.

References

https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-indexes-transact-sql?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-index-columns-transact-sql?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-column-store-segments-transact-sql?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-column-store-row-groups-transact-sql?view=sql-server-2017

 

Columnstore indexes – Overview

From SQL Server 2012, we can now define columnstore indexes on our database tables. The in-memory columnstore index stores data in a column-wise (columnar) format, unlike the traditional B-tree structures used for clustered and non-clustered rowstore indexes, which store data row-wise (in rows). A columnstore index use a column-based storage model, as well as a new ‘batch mode’ of query execution and can offer significant performance gains for queries that summarize large quantities of data.

Demo

In your OLTP database, you might have a few reports that are slow, because they draw from very large tables.

In our example, we have a table dbo.TAT_TIME in our database with more than 25 million records.

Let’s see how we can benefit from columnstore indexes.

To see the benefit of columnstore indexes, let’s compare performance of the same query with and without a columnstore index.

SELECT  t.TK_EMPL_UNO,
        t.WIP_STATUS,
        COUNT(1) AS NumTimeEntries,
        SUM(t.BASE_HRS) AS BASE_HRS,
        SUM(t.TOBILL_HRS) AS TOBILL_HRS,
        SUM(t.BILLED_HRS) AS BILLED_HRS
FROM       dbo.TAT_TIME AS t
INNER JOIN dbo.HBM_MATTER AS m
      ON   t.MATTER_UNO = m.MATTER_UNO
GROUP BY   t.TK_EMPL_UNO,
           t.WIP_STATUS
ORDER BY   t.TK_EMPL_UNO,
           t.WIP_STATUS;

This query retrieved 22220 rows with the execution time of 2 minutes and 43 seconds, since the dbo.TAT_TIME table has over 25 million rows and this query has to scan the entire clustered index.

Creating the Columnstore Index

Columnstore indexes come in two flavours: clustered and non-clustered. In SQL Server 2012, only non-clustered columnstore indexes are available. The clustered version has been added in SQL Server 2014. For our example, let’s create a non-clustered columnstore index as shown below.

CREATE NONCLUSTERED COLUMNSTORE INDEX NCI_TAT_TIME_TEST
ON dbo.TAT_TIME
   ([TIME_UNO]
   ,[SOURCE]
   ,[IMPORT_NUM]
   ,[ENTRY_STATUS]
   ,[PRINTED]
   ,[WIP_STATUS]
   ,[ACTIVITY_COUNT]
   ,[MATTER_UNO]
   ,[ENTRY_EMPL_UNO]
   ,[TK_EMPL_UNO]
   ,[RANK_CODE]
   ,[TOBILL_EMPL_UNO]
   ,[BILLED_EMPL_UNO]
   ,[AU_EMPL_UNO]
   ,[ACTION_CODE]
   ,[PHASE_CODE]
   ,[TASK_CODE]
   ,[LOCATION_CODE]
   ,[NAR_TEXT_ID]
   ,[PB_TEXT_ID]
   ,[TRAN_DATE]
   ,[POST_DATE]
   ,[LATEST_PERIOD]
   ,[BASE_HRS]
   ,[TOBILL_HRS]
   ,[BILLED_HRS]
   ,[CALC_WIP]
   ,[RATE_METHOD]
   ,[BASE_AMT]
   ,[TOBILL_AMT]
   ,[BILLED_AMT]
   ,[STD_AMT]
   ,[HOLD_DATE]
   ,[TAXABLE]
   ,[BILL_PRINT]
   ,[BILLABLE_FLAG]
   ,[BILL_TRAN_UNO]
   ,[CURRENCY_CODE]
   ,[OFFC]
   ,[DEPT]
   ,[PROF]
   ,[BILL_SORT_SEQ]
   ,[ADJ_CODE]
   ,[ADJ_AU_EMPL_UNO]
   ,[ADJ_DE_EMPL_UNO]
   ,[TRANSFERRED]
   ,[UNSETTLED_BAL]
   ,[MERGE_TIME_UNO]
   ,[IS_WIP]
   ,[TRANS_MOD_UNO]
   ,[START_TIME]
   ,[ACTIVITY_CODE]
   ,[DRUNO]
   ,[PHTASK_UNO]
   ,[ORIGTASK_NUM]
   ,[BILLED_TAX_AMT]
   ,[BILLED_TAX_PCT]
   ,[PERIOD]
   ,[DOE_STATUS]
   ,[DISB_UNO]
   ,[SPLIT_NUM]
   ,[PA_SESSION_UNO]
   ,[PA_MOD_COUNT]
   ,[PROJ]
   ,[ADMIN_EDITOR_REVIEWED]
   ,[SUPER_EMPL_UNO]
   ,[WORKING_TKPR]
   ,[NEEDS_UI_VALIDATION]
   ,[HAS_ERRORS]
   ,[CREATED_DATE_ORIG]
   ,[RETAINED_BILLED_AMT]
   ,[RETAINED_BILLED_HRS]
   ,[GROSS_AMT]
   ,[ADV_PREMDISC_AMT]
   ,[CTS_PREMDISC_AMT]
   ,[DELAYED_SPLIT_NUM]
   ,[LAST_MODIFIED]);

When I now run the same query again, I get the same results as before, but the results are returned almost instantaneously. The query now finishes in about one second.

Row-oriented storage vs Column-oriented storage

In a row-oriented storage, when SQL Server has to store the data, it will start by writing all columns of the first row to a page. Then if there is enough space, the second row will be added to the same page, and so on until the page is full. And then a new page is allocated and the process continues.

This storage structure is ideal for OLTP workloads. If you need to insert a row, delete a row, or update a row, you make all changes on just a single page in the data file. And queries that are selective enough to return only a few rows usually also touch only a single page, or two pages in the worst case.

In a columnstore index, data is stored on the same 8K pages that SQL Server has been using since SQL Server 7.0. But instead of filling these pages with complete rows, a columnstore index isolates all values for a single column and stores them in one or more pages. The data of the other columns then go into different pages.

This storage structure is ideal for reporting and analytical processing. In a data warehouse (DW), most tables have dozens of columns (at least), and most queries select only a few of those columns.

rowgroup

A row group is a group of rows that are compressed into columnstore format at the same time. A rowgroup usually contains the maximum number of rows per rowgroup which is 1,048,576 rows.

For high performance and high compression rates, the columnstore index slices the table into groups of rows, called rowgroups, and then compresses each rowgroup in a column-wise manner.

Column Segment

A column segment is a column of data from within the rowgroup.

  • Each rowgroup contains one column segment for every column in the table.
  • Each column segment is compressed together and stored on physical media.

11

Columnstore Compression

Data stored in a columnstore index is always compressed. And the effectiveness of compression achieved for columnstore indexes is extremely high. Data read from a single column is more homogenous than data read from rows, and the more similar the data, the easier it is to compress.

And the more effectively you can compress your data, the more data you can fit on a single page and the more data you can pull into memory, both of which lead to a lower I/O costs.

Batch Mode

The better performance with columnstore indexes is achieved by using “batch mode execution”. It is designed to remove a lot of overhead and optimized to take optimal advantage of the characteristics of modern hardware where you will get the performance benefit that results from the I/O reduction of a columnstore index.

Batch mode processing can operate on multiple rows at a time-a batch stored in an internal memory structure in a format very similar to the on-disk storage format of columnstore indexes rather than one row at a time, as is typical with row-mode processing.

Let’s have a look at the execution plan of the query.

You can notice that the “Estimated Execution Mode” and “Actual Execution Mode” properties of the operators that process the most data of a query running in batch mode and storage as Columnstore. You can see how many batches were used in the “Actual Number of Batches” property, all rows were combined into 134585 batches. A huge saving when compared to the execution in row mode.

12

The actual number of rows in a batch is based on two factors: the amount of data that has to be stored for each row (adding more columns to the query will reduce the batch size), and the hardware you are running on (on a system with a larger CPU cache, more rows can be stored in a batch).

Clustered Columnstore Index

clustered columnstore index is the physical storage for the entire table. No column list can be specified for a clustered columnstore index. But a sort order (ASC or DESC) cannot be specified, since columnstore indexes do not store the data in ordered fashion.

Non-Clustered Columnstore Index

non-clustered columnstore index and a clustered columnstore index function the same.  It contains a copy of a subset of columns, up to and including all of the columns in the table.

Why should I use a Columnstore Index?

A columnstore index can provide a very high level of data compression, typically 10x, to reduce your data storage cost significantly. Plus, they offer better performance than a B-tree index.

Reasons why columnstore indexes are so fast:

  • Columns store values from the same domain and commonly have similar values, which results in high compression rates. This minimizes or eliminates IO bottleneck in your system while reducing the memory footprint significantly.
  • High compression rates improve query performance by using a smaller in-memory footprint. In turn, query performance can improve because SQL Server can perform more query and data operations in-memory.
  • Batch execution improves query performance, typically 2-4x, by processing multiple rows together.
  • Queries often select only a few columns from a table, which reduces total I/O from the physical media.

When should I use a Columnstore Index?

Recommended use cases:

  • Use a clustered columnstore index to store fact tables and large dimension tables for data warehousing workloads. This improves query performance and data compression by up to 10x.
  • Use a non-clustered columnstore index to perform analysis in real-time on an OLTP workload.

How do I choose between a Rowstore Index and a Columnstore Index?

Rowstore indexes perform best on queries that seek into the data, searching for a particular value, or for queries on a small range of values. Use rowstore indexes with transactional workloads since they tend to require mostly table seeks instead of table scans.

Columnstore indexes give high performance gains for analytic queries that scan large amounts of data, especially on large tables.

Beginning with SQL Server 2016 (13.x), you can create an updatable non-clustered columnstore index on a rowstore table. The columnstore index stores a copy of the chosen columns so you do need extra space for this but it will be compressed on average by 10x. By doing this, you can run analytics on the columnstore index and transactions on the rowstore index at the same time. The column store is updated when data changes in the rowstore table, so both indexes are working against the same data.

Beginning with SQL Server 2016 (13.x), you can have one or more non-clustered rowstore indexes on a columnstore index. By doing this, you can perform efficient table seeks on the underlying columnstore. Other options become available too. For example, you can enforce a primary key constraint by using a UNIQUE constraint on the rowstore table. Since a non-unique value will fail to insert into the rowstore table, SQL Server cannot insert the value into the columnstore.

Summary

Columnstore indexes are intended to be used with large tables. There is no hard minimum size, but as a rule of thumb, I would say that you need at least tens of millions of rows in a single table to get real benefit from columnstore indexes.

The only way to evaluate how useful a columnstore index is for a specific database is to compare performance with and without the columnstore index. Adding the query hint OPTION (IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX) to a query will instruct the optimizer to execute the query as if the non-clustered columnstore index does not exist.

The performance benefit available by using columnstore indexes is caused by two major factors. One is the I/O savings caused by the new index structure, the other is batch mode execution.

It uses column-based data storage and query processing to achieve up to 10x query performance gains over traditional row-oriented storage, and up to 10x data compression over the uncompressed data size.

References

https://msdn.microsoft.com/en-us/library/gg492088(v=sql.120).aspx

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-overview?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/t-sql/statements/create-columnstore-index-transact-sql?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/get-started-with-columnstore-for-real-time-operational-analytics?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-data-warehouse?view=sql-server-2017

https://docs.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-query-performance?view=sql-server-2017