May 10, 2025

SQL Server Online Rebuild Index Bug

 

What do you do if you have a very large table and you can’t take an outage window to rebuild the clustered index offline?

·         Exclude the clustered index from your index maintenance jobs

·         Configure it to only have REORGANIZE operations, not REBUILDS

·         Rebuild Index Online

As Microsoft’s documentation says:

We recommend performing online index operations for business environments that operate 24 hours a day, seven days a week, in which the need for concurrent user activity during index operations is vital.

 

How online index operations work

To allow for concurrent user activity during an index data definition language (DDL) operation, the following structures are used during the online index operation: source and preexisting indexes, target, and for rebuilding a heap or dropping a clustered index online, a temporary mapping index.

·         During an online index operation, such as creating a clustered index on a nonindexed table (heap), the source and target go through three phases: preparation, build, and final.

·         The source is the original table or clustered index data.

·         An online index rebuild starts against an index

·         The preexisting indexes are available to concurrent users for select, insert, update, and delete operations.

·         The target or target is the new index (or heap) or a set of new indexes that is being created or rebuilt. Users insert, update, and delete operations to the source are applied by the Database Engine to the target during the index operation. The target index isn't used until the index operation is committed. Internally, the index is marked as write-only.

·         A modification query runs before the index rebuilds query. At the end of the index rebuild, it tries to get a schema modification lock. It can’t, because the modification query is still running

·         The index rebuild’s super-high-level lock request then causes a massive pile-up behind it– blocking even queries that want to do dirty reads, even NOLOCK can be blocked

The following illustration shows the process for creating an initial clustered index online. The source object (the heap) has no other indexes. The source and target structure activities are shown for each phase; concurrent user SELECTINSERTUPDATE, and DELETE operations are also shown. The preparation, build, and final phases are indicated together with the lock modes used in each phase.

 

                                                      

Diagram showing the activities performed during online index operation.

 

Side-Effects of  online Rebuild  index

·         Blocking -. In order to complete Online Rebuild Index operation, SQL server  need a very high level of lock: a schema modification lock (SCH-M) which results in blocking scenarios.

·         Offline Rebuild Index -SQL Server Online Index Rebuild sometimes happens offline without warning and it blocks both read and write queries against the table for long periods.

 

Workaround to avoid blocking

ALTER INDEX [INDEX_NAME]

ON [TABLENAME]

REBUILD WITH ( FILLFACTOR = 80,ONLINE = ON

 (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 5 MINUTES, ABORT_AFTER_WAIT = SELF)));

  • WAIT_AT_LOW_PRIORITY – is the new setting
    • MAX_DURATION – is used to specify the maximum time in minutes to wait until an action
    • ABORT_AFTER_WAIT – is used to abort the operation if it exceeds the wait time
      • NONE – is to wait for the lock with regular priority
      • SELF – exits the online index operation
      • BLOCKERS – kills transactions blocking the index rebuild

There’s no pileup! “LOW_PRIORITY” really does mean low priority – the “NOLOCK” query is able to get a shared schema lock and move forward.

 

SQL Code:

CREATE TABLE [dbo].[IndexMaintenanceLog](

                [IndexName] [varchar](200) NULL,

                [TableName] [varchar](200) NULL,

                [CommandText] [varchar](1500) NULL,

                [Status] [varchar](200) NULL,

                [ErrorMessage] [varchar](1500) NULL,

                [CreatedON] [datetime] NULL

) ON [PRIMARY]

GO


SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

CREATE PROCEDURE [dbo].[ IndexOptimize]

     @UseResumable BIT = 0,

    @UseLowPriorityWait BIT = 1

 

AS

BEGIN

    SET NOCOUNT ON;

               

    DECLARE @sql NVARCHAR(MAX), @IndexName SYSNAME, @TableName SYSNAME, @SchemaName SYSNAME, @Frag FLOAT, @IndexTypeDesc NVARCHAR(60);

 

    SELECT

        s.name AS SchemaName,

        t.name AS TableName,

        i.name AS IndexName,

        ips.avg_fragmentation_in_percent AS Fragmentation,

        i.type_desc AS IndexTypeDesc

    INTO #IndexStats

    FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') AS ips

    INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id

    INNER JOIN sys.tables t ON i.object_id = t.object_id

    INNER JOIN sys.schemas s ON t.schema_id = s.schema_id

   

    AND i.name IS NOT NULL

    AND ips.page_count > 100 -- Skip small indexes

    AND ips.index_level = 0 -- Only leaf level

    ORDER BY ips.avg_fragmentation_in_percent DESC;

 

    DECLARE index_cursor CURSOR FOR

    SELECT SchemaName, TableName, IndexName, Fragmentation, IndexTypeDesc FROM #IndexStats;

 

    OPEN index_cursor;

 

    FETCH NEXT FROM index_cursor INTO @SchemaName, @TableName, @IndexName, @Frag, @IndexTypeDesc;

 

    WHILE @@FETCH_STATUS = 0

    BEGIN

        BEGIN TRY

            IF @Frag >= 30

            BEGIN

                SET @sql = N'ALTER INDEX [' + @IndexName + '] ON [' + @SchemaName + '].[' + @TableName + '] REBUILD WITH ( FILLFACTOR = 80,ONLINE = ON ('

 

                IF @UseLowPriorityWait = 1

                    SET @sql += 'WAIT_AT_LOW_PRIORITY (MAX_DURATION = 5 MINUTES, ABORT_AFTER_WAIT = SELF)';

 

                IF @UseResumable = 1

                    SET @sql += ', RESUMABLE = ON, MAX_DURATION = 15 MINUTES';

 

                SET @sql += '));';

            END

            ELSE IF @Frag >= 10

            BEGIN

                SET @sql = N'ALTER INDEX [' + @IndexName + '] ON [' + @SchemaName + '].[' + @TableName + '] REORGANIZE;';

            END

            ELSE

            BEGIN

                FETCH NEXT FROM index_cursor INTO @SchemaName, @TableName, @IndexName, @Frag, @IndexTypeDesc;

                CONTINUE;

            END

 

                                                PRINT @sql

            EXEC sp_executesql @sql;

 

            INSERT INTO IndexMaintenanceLog (IndexName, TableName, CommandText, Status,CreatedOn)

            VALUES (@IndexName, @TableName, @sql, 'Success',getdate());

        END TRY

        BEGIN CATCH

            INSERT INTO IndexMaintenanceLog (IndexName, TableName, CommandText, Status, ErrorMessage,CreatedOn)

            VALUES (@IndexName, @TableName, @sql, 'Failure', ERROR_MESSAGE(),getdate());

        END CATCH;

 

        FETCH NEXT FROM index_cursor INTO @SchemaName, @TableName, @IndexName, @Frag, @IndexTypeDesc;

    END

 

    CLOSE index_cursor;

    DEALLOCATE index_cursor;

 

    SET NOCOUNT OFF;

END

 

https://learn.microsoft.com/en-us/sql/relational-databases/indexes/how-online-index-operations-work?view=sql-server-ver16

 

https://www.brentozar.com/archive/2015/01/testing-alter-index-rebuild-wait_at_low_priority-sql-server-2014/

 

No comments:

Fashion Catalog Similarity Search using Datastax AstraDB Vector Database

DataStax Astra DB's vector database capabilities can be leveraged to build an efficient fashion catalog similarity search, enabling user...