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.usp_RebuildOrReorganizeIndexes', 'P') IS NOT NULL
    DROP PROCEDURE dbo.usp_RebuildOrReorganizeIndexes;
GO

CREATE PROCEDURE dbo.usp_RebuildOrReorganizeIndexes
    -- ── Scope filters ──────────────────────────────────────────────
    @SchemaName                     NVARCHAR(128)   = NULL,   -- NULL = all schemas
    @TableName                      NVARCHAR(128)   = NULL,   -- NULL = all tables
    @IndexName                      NVARCHAR(128)   = NULL,   -- NULL = all indexes

    -- ── Fragmentation thresholds ───────────────────────────────────
    @MinFragmentationPct            FLOAT           = 10.0,   -- below this = skip
    @ReorganizeThreshold            FLOAT           = 30.0,   -- below this = REORGANIZE, >= REBUILD
    @MinPageCount                   INT             = 1000,   -- tiny indexes skip karo

    -- ── Scan quality ───────────────────────────────────────────────
    -- LIMITED  = fast, header-level (default, production-safe)
    -- SAMPLED  = 1% row sample, more accurate
    -- DETAILED = full scan, most accurate but slowest
    @ScanMode                       NVARCHAR(10)    = 'LIMITED',

    -- ── Rebuild options ────────────────────────────────────────────
    @OnlineRebuild                  BIT             = 0,      -- 1 = ONLINE (Enterprise only)
    @SortInTempDB                   BIT             = 1,      -- tempdb sort buffer use karo
    @FillFactor                     TINYINT         = 0,      -- 0 = existing fill factor use karo
    @MaxDOP                         TINYINT         = 0,      -- 0 = server default, 1 = serial
    @DataCompression                NVARCHAR(10)    = NULL,   -- NULL=no change | PAGE | ROW | NONE

    -- ── Online rebuild blocking guard ─────────────────────────────
    -- Jab ONLINE=ON ho aur koi blocker ho, kitne minute WAIT karo
    -- phir ABORT_AFTER_WAIT = BLOCKERS karke unhe kill karo
    -- 0 = feature disable karo
    @WaitAtLowPriorityMaxDuration   TINYINT         = 0,      -- minutes (0 = skip this option)

    -- ── Statistics ────────────────────────────────────────────────
    @UpdateStats                    BIT             = 1,      -- REORGANIZE ke baad stats update

    -- ── Maintenance window guard ──────────────────────────────────
    -- SP khud end time track karta hai; window cross hone par gracefully exit
    @MaxDurationMinutes             INT             = 120,    -- 0 = no limit

    -- ── Dry run ────────────────────────────────────────────────────
    @DryRun                         BIT             = 0       -- 1 = sirf plan print karo
AS
BEGIN
    SET NOCOUNT ON;

    -- ============================================================
    -- STEP 0 : Parameter validation
    -- ============================================================
    IF @ScanMode NOT IN ('LIMITED', 'SAMPLED', 'DETAILED')
    BEGIN
        RAISERROR('@ScanMode must be LIMITED, SAMPLED, or DETAILED.', 16, 1);
        RETURN;
    END

    IF @DataCompression IS NOT NULL AND @DataCompression NOT IN ('PAGE','ROW','NONE')
    BEGIN
        RAISERROR('@DataCompression must be PAGE, ROW, NONE, or NULL.', 16, 1);
        RETURN;
    END

    IF @FillFactor > 100
    BEGIN
        RAISERROR('@FillFactor must be 0–100. (0 = keep existing)', 16, 1);
        RETURN;
    END

    -- ============================================================
    -- STEP 1 : Session-level temp log (no permanent table)
    --          Yahi is run ka audit trail hai
    -- ============================================================
    IF OBJECT_ID('tempdb..#IndexRunLog') IS NOT NULL
        DROP TABLE #IndexRunLog;

    CREATE TABLE #IndexRunLog (
        RowID               INT             NOT NULL,
        SchemaName          NVARCHAR(128)   NOT NULL,
        TableName           NVARCHAR(128)   NOT NULL,
        IndexName           NVARCHAR(128)   NOT NULL,
        IndexType           NVARCHAR(60)    NOT NULL,  -- CLUSTERED, NONCLUSTERED, COLUMNSTORE, etc.
        PartitionNumber     INT             NOT NULL,
        FragmentationPct    FLOAT           NOT NULL,
        PageCount           BIGINT          NOT NULL,
        PlannedAction       NVARCHAR(20)    NOT NULL,  -- REBUILD / REORGANIZE
        ExecutedSQL         NVARCHAR(MAX)   NULL,
        ExecutionStatus     NVARCHAR(20)    NULL,      -- SUCCESS / FAILED / SKIPPED / DRYRUN
        ErrorMessage        NVARCHAR(MAX)   NULL,
        DurationMS          INT             NULL,
        SkipReason          NVARCHAR(200)   NULL
    );

    -- ============================================================
    -- STEP 2 : Fragmented index list — partition-aware
    -- ============================================================
    DECLARE @DbID INT = DB_ID();

    INSERT INTO #IndexRunLog
        (RowID, SchemaName, TableName, IndexName, IndexType,
         PartitionNumber, FragmentationPct, PageCount, PlannedAction)
    SELECT
        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
        AND (NULLIF(@SchemaName, '') IS NULL OR s.name = @SchemaName)
        AND (NULLIF(@TableName,  '') IS NULL OR o.name = @TableName)
        AND (NULLIF(@IndexName,  '') IS NULL OR i.name = @IndexName);

    -- ============================================================
    -- STEP 3 : Columnstore indexes — force REBUILD
    --          (REORGANIZE on columnstore = delta store flush only,
    --           fragmentation metric alag hoti hai — DBA trap!)
    -- ============================================================
    UPDATE #IndexRunLog
    SET PlannedAction = 'REBUILD'
    WHERE IndexType IN ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE');

    -- ============================================================
    -- STEP 4 : Dry Run — sirf plan dikhao
    -- ============================================================
    IF @DryRun = 1
    BEGIN
        RAISERROR('--- DRY RUN MODE: No changes will be made ---', 10, 1) WITH NOWAIT;

        SELECT
            RowID,
            SchemaName,
            TableName,
            IndexName,
            IndexType,
            PartitionNumber,
            CAST(FragmentationPct AS DECIMAL(5,2))  AS [Frag%],
            PageCount,
            PlannedAction,
            'DRYRUN'                                AS WillExecute
        FROM #IndexRunLog
        ORDER BY FragmentationPct DESC;

        RETURN;
    END

    -- ============================================================
    -- STEP 5 : Build option clauses (reusable strings)
    -- ============================================================

    -- REBUILD WITH (...) clause parts
    DECLARE @RebuildOptions     NVARCHAR(500) = '';
    DECLARE @Sep                NVARCHAR(5)   = '';  -- first option ke pehle koi comma nahi

    -- SORT_IN_TEMPDB
    SET @RebuildOptions += @Sep + 'SORT_IN_TEMPDB = ' + CASE WHEN @SortInTempDB=1 THEN 'ON' ELSE 'OFF' END;
    SET @Sep = ', ';

    -- ONLINE
    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

    -- MAXDOP
    IF @MaxDOP > 0
        SET @RebuildOptions += @Sep + 'MAXDOP = ' + CAST(@MaxDOP AS NVARCHAR(3));

    -- FILLFACTOR
    IF @FillFactor > 0
        SET @RebuildOptions += @Sep + 'FILLFACTOR = ' + CAST(@FillFactor AS NVARCHAR(3));

    -- DATA_COMPRESSION
    IF @DataCompression IS NOT NULL
        SET @RebuildOptions += @Sep + 'DATA_COMPRESSION = ' + @DataCompression;

    -- ============================================================
    -- STEP 6 : Process loop — with maintenance window guard
    -- ============================================================
    DECLARE
        @TotalCount     INT         = (SELECT COUNT(*) FROM #IndexRunLog),
        @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 #IndexRunLog
            SET ExecutionStatus = 'SKIPPED',
                SkipReason      = 'Maintenance window exceeded'
            WHERE 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 #IndexRunLog
        WHERE 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
            -- Columnstore: no WITH options needed (they're silently ignored anyway,
            -- but keeping it clean for DBA readability)
            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
                -- Partition-aware: agar 1 se zyada partition hai to PARTITION = N karo
                -- (ips mein ek row per partition hoti hai)
                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
            -- Columnstore REORGANIZE = delta store flush (different intent)
            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;

            -- REORGANIZE ke baad statistics update (REBUILD auto-updates)
            IF @Action = 'REORGANIZE' AND @UpdateStats = 1
                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 #IndexRunLog
            SET ExecutionStatus = 'SUCCESS',
                ExecutedSQL     = @SQL,
                DurationMS      = DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME())
            WHERE RowID = @I;

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

            UPDATE #IndexRunLog
            SET ExecutionStatus = 'FAILED',
                ExecutedSQL     = @SQL,
                ErrorMessage    = @ErrMsg,
                DurationMS      = DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME())
            WHERE 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

    -- ============================================================
    -- STEP 7 : Final summary (from session temp table)
    -- ============================================================
    RAISERROR('--- Run Complete ---', 10, 1) WITH NOWAIT;

    -- 7a. Aggregate summary
    SELECT
        PlannedAction                       AS [Action],
        ExecutionStatus                     AS [Status],
        COUNT(*)                            AS [IndexCount],
        SUM(PageCount)                      AS [TotalPages],
        MIN(CAST(FragmentationPct AS DECIMAL(5,2))) AS [MinFrag%],
        MAX(CAST(FragmentationPct AS DECIMAL(5,2))) AS [MaxFrag%],
        AVG(DurationMS)                     AS [AvgDurationMS],
        SUM(DurationMS)                     AS [TotalDurationMS]
    FROM #IndexRunLog
    GROUP BY PlannedAction, ExecutionStatus
    ORDER BY PlannedAction, ExecutionStatus;

    -- 7b. Detailed log (FAILED first, then rest by frag%)
    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 #IndexRunLog
    ORDER BY
        CASE ExecutionStatus WHEN 'FAILED' THEN 0 ELSE 1 END,
        FragmentationPct DESC;

END;
GO


-- ============================================================
--  USAGE EXAMPLES
-- ============================================================

-- 1. Dry Run — plan dekho, kuch execute nahi hoga
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @DryRun = 1;

-- 2. Normal run — default thresholds, SAMPLED scan
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @ScanMode = 'SAMPLED';

-- 3. Specific table, serial rebuild (low-priority server)
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @TableName  = 'PAY_SalaryDetails',
    @MaxDOP     = 1;

-- 4. Enterprise: ONLINE rebuild, 60-min window, PAGE compression
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @OnlineRebuild                  = 1,
    @WaitAtLowPriorityMaxDuration   = 2,    -- 2 min wait, phir blockers kill
    @MaxDurationMinutes             = 60,
    @DataCompression                = 'PAGE',
    @FillFactor                     = 80,
    @MaxDOP                         = 4;

-- 5. Specific schema, aggressive thresholds
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @SchemaName             = 'dbo',
    @MinFragmentationPct    = 5,
    @ReorganizeThreshold    = 15,
    @MinPageCount           = 500,
    @ScanMode               = 'DETAILED';

-- 6. Sirf specific index
EXEC dbo.usp_RebuildOrReorganizeIndexes
    @TableName  = 'ATT_DailyRecord',
    @IndexName  = 'IX_ATT_DailyRecord_EmpDate';

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.