{"id":85292,"date":"2019-09-30T23:00:28","date_gmt":"2019-09-30T23:00:28","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=85292"},"modified":"2019-09-30T14:45:33","modified_gmt":"2019-09-30T14:45:33","slug":"the-case-of-the-vanishing-index-compression","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/blogs\/the-case-of-the-vanishing-index-compression\/","title":{"rendered":"The Case of the Vanishing Index Compression"},"content":{"rendered":"<p>Recently, we started adding compression to some of our larger tables in our data warehouse where columnstore indexes didn\u2019t make sense. (For example, some very large dimension tables, and a few facts where they are not used for large aggregates in the common manner).<\/p>\n<p>The changes were checked into source control, we monitored performance, all seemed good. Then next time we were doing a code review\/compare source control with our development instances, we discovered changes that we were not expecting. Indexes were no longer compressed in some cases.<\/p>\n<p>So I pulled out my Sherlock Holmes hat (which I guarantee you doesn\u2019t exist, and if it did it would have Mickey ears on it), and started sleuthing. To recreate the crime, I will create the following index in the WideWorldImporters database, just a simple index with page level creation turned on:<\/p>\n<pre class=\"lang:none theme:none\">CREATE INDEX Description ON Sales.OrderLines(Description) \r\n                           WITH (DATA_COMPRESSION = PAGE);<\/pre>\n<p>So now, the index is compressed, right? If you are wondering what the answer is to this question, the answer is yes, clearly, but there is a slight problem with that logic. Is the index really compressed? Check the metadata of the index:<\/p>\n<pre class=\"lang:none theme:none\">SELECT *\r\nFROM   sys.indexes\r\nWHERE  indexes.object_id = OBJECT_ID('Sales.OrderLines')\r\n  AND indexes.name = 'Description';<\/pre>\n<p>The only mention of compression is &#8220;compression_delay&#8221;, which still might give you the feeling that compression is a part of the index metadata. What happened in our case was that in our processes to truncate and reload a table, we were disabling one or more indexes, loading the data, then rebuilding the index, using a process such as:<\/p>\n<pre class=\"lang:none theme:none\">ALTER INDEX Description ON Sales.OrderLines DISABLE;\r\n--Load the data\r\nALTER INDEX Description ON Sales.OrderLines REBUILD;<\/pre>\n<p>Now the index should be right back to where it started, right? If you query sys.indexes, everything will look the same, but remember how sys.indexes had nothing about compression? The compression is no longer in effect, as you can see in sys.partitions.<\/p>\n<pre class=\"lang:none theme:none\">SELECT partitions.data_compression_desc\r\nFROM   sys.partitions\r\n         JOIN sys.indexes\r\n           ON indexes.object_id = partitions.object_id\r\n               AND indexes.index_id = partitions.index_id\r\nWHERE  indexes.object_id = OBJECT_ID('Sales.OrderLines')\r\nAND indexes.name = 'Description';<\/pre>\n<p>This returns:<\/p>\n<pre class=\"lang:none theme:none\">data_compression_desc\r\n---------------------------------\r\nNONE<\/pre>\n<p>Partitions are where compression information is stored, because partitions (even if the table\/index is not technically partitioned it has one partition), are compressed. Compression delay is part of a columnstore index, which is always compressed, and how long it waits to move the changes from the delta store to a columnstore segment is controlled by the delay. For more on that, see Niko Neugebaur&#8217;s blog <a href=\"http:\/\/www.nikoport.com\/2016\/02\/04\/columnstore-indexes-part-76-compression-delay\/\">here<\/a>:):<\/p>\n<p>The way you have to rebuild the index if you wish to keep it compressed after a disable, is to specify the data compression again:<\/p>\n<pre class=\"lang:none theme:none\">ALTER INDEX Description ON Sales.OrderLines DISABLE;\r\n--Load the data\r\nALTER INDEX Description ON Sales.OrderLines REBUILD \r\n                    WITH (DATA_COMPRESSION = PAGE);<\/pre>\n<p>Checking again in sys.partitions you have:<\/p>\n<pre class=\"lang:none theme:none\">data_compression_desc\r\n---------------------------------\r\nPAGE<\/pre>\n<p>Problem solved and fairly easy to handle if you are seeing this problem in a typical OLTP database. In these cases, if you are disabling and reenabling indexes on a regular basis, it should be only a few indexes, (and if you are commonly disabling them first, you may need to question your processes.)<\/p>\n<p>Just executing the following code to rebuild your index:<\/p>\n<pre class=\"lang:none theme:none\">ALTER INDEX Description ON Sales.OrderLines REBUILD;<\/pre>\n<p>Effectively recreates the index, but it does not change the compression of the object&#8217;s partition, because a REBUILD doesn&#8217;t drop the index in the same way that a DISABLE does. If your physical structure is partitioned, this will be possibly be more obvious to you, in that you can compress only certain partitions. (If you are using the code that follows, you would need to extend it if your tables are partititoned, and it includes protections that stop it from working on partitioned tables).<\/p>\n<p>Note that the way you change the compression status of the index is to do a rebuild.<\/p>\n<pre class=\"lang:none theme:none\">ALTER INDEX Description ON Sales.OrderLines REBUILD \r\n                    WITH (DATA_COMPRESSION = PAGE);<\/pre>\n<p>The way we combatted this is in our data warehouse, non-partitioned case was by creating a table of indexes that are compressed and using that information our index rebuild stored procedure. This works fine if every programmer and DBA realizes this method and doesn&#8217;t use direct DDL to do this process.<\/p>\n<pre class=\"lang:none theme:none\">CREATE SCHEMA Utility;\r\nGO\r\n-- Use to hold the status of compression for an index with disabling it\r\nCREATE TABLE Utility.IndexCompressionMetadata\r\n(\r\n     SchemaName sysname NOT NULL,\r\n     TableName sysname NOT NULL,\r\n     IndexName sysname NOT NULL,\r\n     CompressionStyle sysname NOT NULL,\r\n     CONSTRAINT PKIndexCompressionMetadata PRIMARY KEY (SchemaName,TableName,IndexName)\r\n)\r\nGO\r\n\r\nCREATE OR ALTER PROCEDURE Utility.Index$Disable\r\n(\r\n     @SchemaName sysname,\r\n     @TableName sysname,\r\n     @IndexName sysname\r\n)\r\nAS\r\n BEGIN\r\n     SET NOCOUNT ON;\r\n     IF EXISTS (\r\n         SELECT *\r\n         FROM Utility.IndexCompressionMetadata\r\n         WHERE IndexCompressionMetadata.SchemaName = @SchemaName\r\n           AND IndexCompressionMetadata.TableName = @TableName\r\n           AND IndexCompressionMetadata.IndexName = @IndexName) \r\n      THROW 50000,'This Index Already Has Compression Metadata. Fix Manually',1;\r\n     IF EXISTS (\r\n           SELECT *\r\n           FROM sys.indexes \r\n                 INNER join sys.data_spaces \r\n                  ON indexes.data_space_id = \r\n                            data_spaces.data_space_id\r\n           WHERE data_spaces.type_desc ='PARTITION_SCHEME'\r\n             AND OBJECT_SCHEMA_NAME(indexes.object_id) \r\n                               = @SchemaName\r\n             AND OBJECT_NAME(indexes.object_id) = @TableName\r\n             AND Indexes.name = @Indexname\r\n     )\r\n     THROW 50000,'This Utility may not be used with Partitioned Indexes',1;\r\n     \r\n     BEGIN TRY;\r\n     BEGIN TRANSACTION;\r\n       INSERT INTO Utility.IndexCompressionMetadata\r\n                (SchemaName, TableName,\r\n                 IndexName,CompressionStyle)\r\n       SELECT @SchemaName, @TableName,@IndexName, \r\n              partitions.data_compression_desc\r\n       FROM   sys.partitions\r\n                 JOIN sys.indexes\r\n                   ON indexes.object_id = partitions.object_id\r\n                     AND indexes.index_id = partitions.index_id\r\n       WHERE  indexes.object_id = \r\n                  OBJECT_ID(CONCAT(@SchemaName,'.',@TableName))\r\n         AND indexes.name = 'Description';\r\n\r\n       DECLARE @query varchar(MAX);\r\n       SET @query = CONCAT ('ALTER INDEX ',@IndexName,\r\n                          ' ON ',@SchemaName,'.',\r\n                          @TableName,' DISABLE');\r\n       --SELECT @query\r\n       EXEC (@query);\r\n       COMMIT TRANSACTION;\r\n       END TRY\r\n      BEGIN CATCH\r\n          IF @@TRANCOUNT &gt; 0\r\n               ROLLBACK;\r\n          THROW;\r\n       END CATCH;\r\nEND;<\/pre>\n<p>Now to disable the index using:<\/p>\n<pre class=\"lang:none theme:none \">EXEC Utility.[Index$Disable] @SchemaName = 'Sales',\r\n                             @TableName = 'OrderLines',\r\n                             @IndexName = 'Description';<\/pre>\n<p>Now you can check the compression on the partition, and what you will see is that it doesn&#8217;t even exist, because for indexes, the partition is deleted in the disable process.<\/p>\n<pre class=\"lang:none theme:none\">SELECT partitions.data_compression_desc\r\nFROM   sys.partitions\r\n          JOIN sys.indexes\r\n            ON indexes.object_id = partitions.object_id\r\n               AND indexes.index_id = partitions.index_id\r\nWHERE  indexes.object_id = OBJECT_ID('Sales.OrderLines')\r\nAND indexes.name = 'Description';<\/pre>\n<p>You can also see in the metadata, that the index will have PAGE compression when the index is rebuilt using the tool we are about to create:<\/p>\n<pre class=\"lang:none theme:none\">SELECT *\r\nFROM   Utility.IndexCompressionMetadata;<\/pre>\n<p>This returns:<\/p>\n<pre class=\"lang:none theme:none\">SchemaName    TableName    IndexName     CompressionStyle\r\n------------- ------------ ------------- -------------------\r\nSales         OrderLines   Description   PAGE<\/pre>\n<p>Then this stored procedure will be used to reenable the index, with the last compression status<\/p>\n<pre class=\"lang:none theme:none\">CREATE OR ALTER PROCEDURE Utility.Index$Enable\r\n(\r\n     @SchemaName sysname,\r\n     @TableName sysname,\r\n     @IndexName sysname\r\n)\r\nAS\r\n BEGIN\r\n   BEGIN TRY;\r\n     BEGIN TRANSACTION;\r\n     DECLARE @compressionStyle sysname = \r\n         (SELECT IndexCompressionMetadata.CompressionStyle\r\n          FROM  Utility.IndexCompressionMetadata\r\n          WHERE IndexCompressionMetadata.SchemaName = @SchemaName\r\n            AND IndexCompressionMetadata.TableName = @TableName\r\n            AND IndexCompressionMetadata.IndexName = @IndexName)\r\n    \r\n     IF @compressionStyle IS NULL\r\n          THROW 50000,'This procedure can only be used on indexes that are disabled, and registered with Utility.Index$Disable',1;\r\n\r\n     DECLARE @query varchar(MAX);\r\n     SET @query = CONCAT ('ALTER INDEX ',@IndexName,' ON ',@SchemaName,'.',@TableName,' REBUILD',\r\n         ' WITH (DATA_COMPRESSION = ' + @compressionStyle + ')')\r\n     --SELECT @query\r\n     EXEC (@query);\r\n\r\n     DELETE FROM Utility.IndexCompressionMetadata\r\n     WHERE IndexCompressionMetadata.SchemaName = @SchemaName\r\n       AND IndexCompressionMetadata.TableName = @TableName\r\n       AND IndexCompressionMetadata.IndexName = @IndexName;\r\n\r\n     COMMIT TRANSACTION;\r\n   END TRY\r\n   BEGIN CATCH\r\n          IF @@TRANCOUNT &gt; 0\r\n          ROLLBACK;\r\n          THROW;\r\n   END CATCH;\r\nEND;<\/pre>\n<p>Execute the procedure:<\/p>\n<pre class=\"lang:none theme:none \">EXEC Utility.Index$Enable  @SchemaName = 'Sales',\r\n                           @TableName = 'OrderLines',\r\n                           @IndexName = 'Description';<\/pre>\n<p>Now you can check the compression on the partition, and what you will see is that it is not compressed with PAGE level compression.<\/p>\n<p>Mystery solved. It is funny how much fun finding things like this can be once you get started. So when you find something seems wrong in your configuration, never panic, just start looking at the system catalog views and look for the clues. The fact that the index had no compression information was my first clue that this was going to be a problem with how compression is done with indexes.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Recently, we started adding compression to some of our larger tables in our data warehouse where columnstore indexes didn\u2019t make sense. (For example, some very large dimension tables, and a few facts where they are not used for large aggregates in the common manner). The changes were checked into source control, we monitored performance, all&#8230;&hellip;<\/p>\n","protected":false},"author":56085,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"tags":[],"coauthors":[19684],"class_list":["post-85292","post","type-post","status-publish","format-standard","hentry","category-blogs"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/85292","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/56085"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=85292"}],"version-history":[{"count":2,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/85292\/revisions"}],"predecessor-version":[{"id":85294,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/85292\/revisions\/85294"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=85292"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=85292"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=85292"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=85292"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}