Back to all posts
Database maintenance

Index Rebuilding vs. Reorganization in SQL Server

As a database grows, maintaining optimal performance becomes crucial. One of the key contributors to performance degradation over time is index fragmentation...

As a database grows, maintaining optimal performance becomes crucial. One of the key contributors to performance degradation over time is index fragmentation. SQL Server provides two essential methods to handle index fragmentation: Rebuilding and Reorganizing indexes.


Understanding Index Fragmentation

Indexes are like a roadmap for SQL Server, allowing it to quickly find and retrieve data. However, over time, changes to the data (such as inserts, updates, and deletes) can cause fragmentation, meaning the data in the index is no longer stored contiguously. This slows down data retrieval and increases the amount of work SQL Server has to do.

Fragmentation Types:

  1. Internal Fragmentation: Empty space within pages due to updates or deletes.

  2. External Fragmentation: Data pages are out of order, leading to slower scans.

To fix fragmentation, SQL Server provides two operations: Rebuilding and Reorganizing indexes.


Rebuild vs. Reorganize: What’s the Difference?

1. Rebuild Index

Rebuilding an index drops and recreates the index from scratch. This method ensures that all data is restructured in contiguous pages, which removes fragmentation completely.

Key Features of Rebuild:

  • Fully reorganizes the index pages.

  • Drops and recreates the index, which locks the table (unless done online).

  • Automatically updates statistics for the index.

  • Requires more resources (CPU, memory) and can be time-consuming for large tables.

Syntax:

SQL
ALTER INDEX  ON  REBUILD;

When to Rebuild?

  • Use when fragmentation exceeds 30%.

  • If you need maximum performance improvement.

  • When you have a maintenance window, as it’s more resource-intensive.


2. Reorganize Index

Reorganizing an index is a lighter operation compared to rebuilding. It defragments the index at the leaf level by physically reordering the data pages, but does not recreate the entire index.

Key Features of Reorganize:

  • Only affects the leaf-level pages of the index.

  • It’s an online operation, so the table remains accessible.

  • Consumes fewer resources than a rebuild.

  • Does not automatically update statistics.

Syntax:

SQL
ALTER INDEX  ON  REORGANIZE;

When to Reorganize?

  • Use when fragmentation is between 10% and 30%.

  • When you need a lightweight operation with minimal disruption.


How to Check Index Fragmentation

Before deciding to rebuild or reorganize an index, it’s essential to know the fragmentation level. You can check this using SQL Server’s built-in Dynamic Management Views (DMVs). The following query will show the average fragmentation percentage for indexes in your database:

SQL
SELECT
    OBJECT_NAME(ips.object_id) AS TableName,
    si.name AS IndexName,
    ips.avg_fragmentation_in_percent
FROM
    sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
JOIN
    sys.indexes si ON ips.object_id = si.object_id AND ips.index_id = si.index_id
WHERE
    ips.avg_fragmentation_in_percent > 10;

This query will return all indexes with fragmentation above 10%, allowing you to decide whether to rebuild or reorganize.


Automating Index Maintenance

Manually monitoring and maintaining indexes can be time-consuming, especially for large databases. Automating the process ensures that indexes are maintained regularly, keeping your database in top shape.

Here’s a script that automates index maintenance based on fragmentation levels:

SQL
IF OBJECT_ID('dbo.ADTDB_IndexMaintenanceLog', 'U') IS NOT NULL
BEGIN
    DROP TABLE dbo.ADTDB_IndexMaintenanceLog;
END

GO


IF OBJECT_ID('dbo.ADTDB_IndexMaintenanceLog', 'U') IS NULL
BEGIN
    CREATE TABLE dbo.ADTDB_IndexMaintenanceLog (
        LogID               BIGINT              IDENTITY(1,1)  NOT NULL,
        RunID               UNIQUEIDENTIFIER    NOT NULL,
        RunDateTime         DATETIME2           NOT NULL,
        RowID               INT                 NOT NULL,  -- run ke andar row order (loop cursor)
        SchemaName          NVARCHAR(128)       NOT NULL,
        TableName           NVARCHAR(128)       NOT NULL,
        IndexName           NVARCHAR(128)       NOT NULL,
        IndexType           NVARCHAR(60)        NOT NULL,
        PartitionNumber     INT                 NOT NULL,
        FragmentationPct    FLOAT               NOT NULL,
        PageCount           BIGINT              NOT NULL,
        PlannedAction       NVARCHAR(20)        NOT NULL,
        ExecutedSQL         NVARCHAR(MAX)       NULL,
        ExecutionStatus     NVARCHAR(20)        NULL,
        ErrorMessage        NVARCHAR(MAX)       NULL,
        DurationMS          INT                 NULL,
        SkipReason          NVARCHAR(200)       NULL,
        CONSTRAINT PK_ADTDB_IndexMaintenanceLog PRIMARY KEY CLUSTERED (LogID)
    );

    CREATE UNIQUE NONCLUSTERED INDEX IX_ADTDB_IndexMaintenanceLog_RunID_RowID
        ON dbo.ADTDB_IndexMaintenanceLog (RunID, RowID);
END
GO



IF OBJECT_ID('dbo.uspADTDB_RebuildOrReorganizeIndexes', 'P') IS NOT NULL
    DROP PROCEDURE dbo.uspADTDB_RebuildOrReorganizeIndexes;
GO

CREATE PROCEDURE dbo.uspADTDB_RebuildOrReorganizeIndexes
AS
BEGIN
    SET NOCOUNT ON;

 
    DECLARE @MinFragmentationPct            FLOAT         = 10.0;      -- below this = skip
    DECLARE @ReorganizeThreshold            FLOAT         = 30.0;      -- below this = REORGANIZE, >= REBUILD
    DECLARE @MinPageCount                   INT           = 1000;      -- tiny indexes skip karo
    DECLARE @ScanMode                       NVARCHAR(10)  = 'DETAILED'; -- LIMITED | SAMPLED | DETAILED
    DECLARE @WaitAtLowPriorityMaxDuration   TINYINT       = 0;         -- minutes (0 = feature disable)
    DECLARE @MaxDurationMinutes             INT           = 120;       -- 0 = no limit


    DECLARE @OnlineRebuild BIT =
        CASE WHEN CAST(SERVERPROPERTY('EngineEdition') AS INT) IN (3,5,6,8)
             THEN 1 ELSE 0
        END;

    
    DECLARE @RunID     UNIQUEIDENTIFIER = NEWID();
    DECLARE @RunAt     DATETIME2        = SYSDATETIME();


    DECLARE @DbID INT = DB_ID();

    INSERT INTO dbo.ADTDB_IndexMaintenanceLog
        (RunID, RunDateTime, RowID, SchemaName, TableName, IndexName, IndexType,
         PartitionNumber, FragmentationPct, PageCount, PlannedAction)
    SELECT
        @RunID,
        @RunAt,
        ROW_NUMBER() OVER (ORDER BY ips.avg_fragmentation_in_percent DESC),
        s.name,
        o.name,
        i.name,
        i.type_desc,                        -- CLUSTERED / NONCLUSTERED / XML / etc.
        ips.partition_number,
        ips.avg_fragmentation_in_percent,
        ips.page_count,
        CASE
            WHEN ips.avg_fragmentation_in_percent >= @ReorganizeThreshold THEN 'REBUILD'
            ELSE 'REORGANIZE'
        END
    FROM
        sys.dm_db_index_physical_stats(@DbID, NULL, NULL, NULL, @ScanMode) ips
        JOIN sys.indexes  i ON ips.object_id   = i.object_id
                            AND ips.index_id    = i.index_id
        JOIN sys.objects  o ON o.object_id      = i.object_id
        JOIN sys.schemas  s ON s.schema_id      = o.schema_id
    WHERE
        ips.avg_fragmentation_in_percent  >  @MinFragmentationPct
        AND ips.page_count               >= @MinPageCount
        AND i.index_id                   >  0          -- heap skip
        AND i.is_disabled                =  0          -- disabled skip
        AND i.is_hypothetical            =  0          -- hypothetical skip
        AND o.type                       =  'U'        -- user tables only
        AND ips.alloc_unit_type_desc     =  'IN_ROW_DATA';  -- avoid duplicate LOB rows

   
    UPDATE dbo.ADTDB_IndexMaintenanceLog
    SET PlannedAction = 'REBUILD'
    WHERE RunID = @RunID
      AND IndexType IN ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE');

    

    -- REBUILD WITH (...) clause parts
    DECLARE @RebuildOptions     NVARCHAR(500) = 'SORT_IN_TEMPDB = ON';
    DECLARE @Sep                NVARCHAR(5)   = ', ';

    -- ONLINE (auto-detected in STEP 0.5)
    SET @RebuildOptions += @Sep + 'ONLINE = ' + CASE WHEN @OnlineRebuild=1 THEN 'ON' ELSE 'OFF' END;

    -- WAIT_AT_LOW_PRIORITY (only meaningful with ONLINE = ON)
    IF @OnlineRebuild = 1 AND @WaitAtLowPriorityMaxDuration > 0
    BEGIN
        SET @RebuildOptions +=
            '(WAIT_AT_LOW_PRIORITY (MAX_DURATION = '
            + CAST(@WaitAtLowPriorityMaxDuration AS NVARCHAR(5))
            + ' MINUTES, ABORT_AFTER_WAIT = BLOCKERS))';
    END

    DECLARE
        @TotalCount     INT         = (SELECT COUNT(*) FROM dbo.ADTDB_IndexMaintenanceLog WHERE RunID = @RunID),
        @I              INT         = 1,
        @SchName        NVARCHAR(128),
        @TblName        NVARCHAR(128),
        @IdxName        NVARCHAR(128),
        @IdxType        NVARCHAR(60),
        @PartNo         INT,
        @Action         NVARCHAR(20),
        @FragPct        FLOAT,
        @PageCnt        BIGINT,
        @SQL            NVARCHAR(MAX),
        @StatSQL        NVARCHAR(MAX),
        @StartTime      DATETIME2,
        @RunStart       DATETIME2   = SYSDATETIME(),
        @ErrMsg         NVARCHAR(MAX),
        @ProgressMsg    NVARCHAR(500);

    WHILE @I <= @TotalCount
    BEGIN
        -- ── Maintenance window check ──────────────────────────────
        IF @MaxDurationMinutes > 0
           AND DATEDIFF(MINUTE, @RunStart, SYSDATETIME()) >= @MaxDurationMinutes
        BEGIN
            SET @ProgressMsg = FORMATMESSAGE(
                '[WINDOW] Maintenance window of %d min reached. Stopping at index %d of %d.',
                @MaxDurationMinutes, @I, @TotalCount);
            RAISERROR(@ProgressMsg, 10, 1) WITH NOWAIT;

            -- Remaining rows SKIPPED mark karo
            UPDATE dbo.ADTDB_IndexMaintenanceLog
            SET ExecutionStatus = 'SKIPPED',
                SkipReason      = 'Maintenance window exceeded'
            WHERE RunID = @RunID AND RowID >= @I AND ExecutionStatus IS NULL;

            BREAK;
        END

        -- ── Fetch current row ─────────────────────────────────────
        SELECT
            @SchName  = SchemaName,
            @TblName  = TableName,
            @IdxName  = IndexName,
            @IdxType  = IndexType,
            @PartNo   = PartitionNumber,
            @FragPct  = FragmentationPct,
            @PageCnt  = PageCount,
            @Action   = PlannedAction
        FROM dbo.ADTDB_IndexMaintenanceLog
        WHERE RunID = @RunID AND RowID = @I;

        SET @StartTime  = SYSDATETIME();
        SET @ErrMsg     = NULL;
        SET @SQL        = NULL;

        -- ── Live progress message ─────────────────────────────────
        SET @ProgressMsg = FORMATMESSAGE(
            '[%d/%d] %s: %s.%s.%s  Frag=%s%%  Pages=%I64d  Partition=%d',
            @I, @TotalCount, @Action,
            @SchName, @TblName, @IdxName,
            CAST(CAST(@FragPct AS DECIMAL(5,2)) AS VARCHAR(10)), @PageCnt, @PartNo);
        RAISERROR(@ProgressMsg, 10, 1) WITH NOWAIT;

        -- ── Build SQL ─────────────────────────────────────────────

        IF @Action = 'REBUILD'
        BEGIN
         
            IF @IdxType IN ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE')
            BEGIN
                SET @SQL =
                    N'ALTER INDEX ' + QUOTENAME(@IdxName)
                  + N' ON '        + QUOTENAME(@SchName) + N'.' + QUOTENAME(@TblName)
                  + N' REBUILD;';
            END
            ELSE
            BEGIN
               
                SET @SQL =
                    N'ALTER INDEX ' + QUOTENAME(@IdxName)
                  + N' ON '        + QUOTENAME(@SchName) + N'.' + QUOTENAME(@TblName)
                  + N' REBUILD'
                  + CASE WHEN @PartNo > 1
                         THEN N' PARTITION = ' + CAST(@PartNo AS NVARCHAR(10))
                         ELSE N''
                    END
                  + CASE WHEN LEN(@RebuildOptions) > 0
                         THEN N' WITH (' + @RebuildOptions + N')'
                         ELSE N''
                    END
                  + N';';
            END
        END
        ELSE -- REORGANIZE
        BEGIN
          
            IF @IdxType IN ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE')
            BEGIN
                SET @SQL =
                    N'ALTER INDEX ' + QUOTENAME(@IdxName)
                  + N' ON '        + QUOTENAME(@SchName) + N'.' + QUOTENAME(@TblName)
                  + N' REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);';
            END
            ELSE
            BEGIN
                SET @SQL =
                    N'ALTER INDEX ' + QUOTENAME(@IdxName)
                  + N' ON '        + QUOTENAME(@SchName) + N'.' + QUOTENAME(@TblName)
                  + N' REORGANIZE'
                  + CASE WHEN @PartNo > 1
                         THEN N' PARTITION = ' + CAST(@PartNo AS NVARCHAR(10))
                         ELSE N''
                    END
                  + N';';
            END
        END

        -- ── Execute ───────────────────────────────────────────────
        BEGIN TRY
            EXEC sp_executesql @SQL;

            
            IF @Action = 'REORGANIZE'
                AND @IdxType NOT IN ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE')
            BEGIN
                SET @StatSQL =
                    N'UPDATE STATISTICS '
                  + QUOTENAME(@SchName) + N'.' + QUOTENAME(@TblName)
                  + N' ' + QUOTENAME(@IdxName) + N';';
                EXEC sp_executesql @StatSQL;
            END

            UPDATE dbo.ADTDB_IndexMaintenanceLog
            SET ExecutionStatus = 'SUCCESS',
                ExecutedSQL     = @SQL,
                DurationMS      = DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME())
            WHERE RunID = @RunID AND RowID = @I;

        END TRY
        BEGIN CATCH
            SET @ErrMsg = ERROR_MESSAGE();

            UPDATE dbo.ADTDB_IndexMaintenanceLog
            SET ExecutionStatus = 'FAILED',
                ExecutedSQL     = @SQL,
                ErrorMessage    = @ErrMsg,
                DurationMS      = DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME())
            WHERE RunID = @RunID AND RowID = @I;

            RAISERROR('[FAILED] %s on %s.%s: %s', 10, 1,
                      @IdxName, @SchName, @TblName, @ErrMsg) WITH NOWAIT;
        END CATCH

        SET @I = @I + 1;
    END -- WHILE

   
    RAISERROR('--- Run Complete ---', 10, 1) WITH NOWAIT;
    
    SELECT
        RowID,
        SchemaName,
        TableName,
        IndexName,
        IndexType,
        PartitionNumber,
        CAST(FragmentationPct AS DECIMAL(5,2))  AS [Frag%],
        PageCount,
        PlannedAction                           AS [Action],
        ExecutionStatus                         AS [Status],
        DurationMS,
        SkipReason,
        ErrorMessage
    FROM dbo.ADTDB_IndexMaintenanceLog
    WHERE RunID = @RunID
    ORDER BY
        CASE ExecutionStatus WHEN 'FAILED' THEN 0 ELSE 1 END,
        FragmentationPct DESC;

END;
GO


EXEC dbo.uspADTDB_RebuildOrReorganizeIndexes;

How it works:

  • The script checks the fragmentation level of each index.

  • If fragmentation is above 30%, it rebuilds the index.

  • If fragmentation is between 10% and 30%, it reorganizes the index.

  • This process can be scheduled as a SQL Server Agent job to run during off-peak hours, ensuring regular maintenance.


When to Rebuild vs. Reorganize?

  • Rebuild: Use when fragmentation is above 30%. This is a more resource-intensive operation but provides the best performance improvement.

  • Reorganize: Use when fragmentation is between 10% and 30%. It is less disruptive and can be run while the database is in use.

For very large databases, consider rebuilding indexes online to avoid downtime. However, note that online index rebuilds require SQL Server Enterprise Edition or higher.

0 likes

Rate this post

No rating

Tap a star to rate

0 comments

Latest comments

0 comments

No comments yet.

Keep building your data skillset

Explore more SQL, Python, analytics, and engineering tutorials.