Index fragmentation is one of those silent performance killers that creeps up on SQL Server databases over time. As data is inserted, updated, and deleted, indexes become fragmented, causing slower query performance and increased I/O operations. That's why automated index maintenance is essential for keeping databases running smoothly.
This script automates the process of identifying and fixing fragmented indexes by using SQL Server's dynamic management views to find fragmentation, then automatically choosing between REBUILD (for severe fragmentation) and REORGANIZE (for moderate fragmentation). Here's how it works and why each piece matters.
TL;DR
- Check fragmentation: Use
sys.dm_db_index_physical_statsto find fragmented indexes - Rebuild if >30%:
ALTER INDEX ... REBUILDfor heavily fragmented indexes - Reorganize if 5-30%:
ALTER INDEX ... REORGANIZEfor moderate fragmentation - Automate with script: Use cursor-based script to process all fragmented indexes automatically
- Run regularly: Schedule index maintenance weekly or monthly depending on database activity
- Monitor performance: Track query performance before and after maintenance
Understanding Index Fragmentation
Index fragmentation occurs when the logical order of pages in an index doesn't match the physical order on disk. This happens because:
- Page splits: When inserts occur and pages are full, SQL Server splits pages, creating fragmentation
- Deletes: Removing rows leaves gaps in index pages
- Updates: Changing values that affect index keys can cause rows to move
Fragmented indexes require more I/O operations to read the same data, leading to slower query performance. The solution is regular index maintenance using REBUILD or REORGANIZE operations.
The Complete Script
Here's the complete index maintenance script:
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
DECLARE @TableName varchar(255);
DECLARE @IndexName varchar(255);
DECLARE @Fragmentation FLOAT;
DECLARE @IndexScript varchar(255);
DECLARE @Schema varchar(255);
SELECT
dbtables.[name],
dbindexes.[name],
indexstats.avg_fragmentation_in_percent,
indexstats.page_count [pages]
FROM
sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats
INNER JOIN sys.tables dbtables
on dbtables.[object_id] = indexstats.[object_id]
INNER JOIN sys.schemas dbschemas
on dbtables.[schema_id] = dbschemas.[schema_id]
INNER JOIN sys.indexes AS dbindexes
ON dbindexes.[object_id] = indexstats.[object_id]
AND indexstats.index_id = dbindexes.index_id
WHERE
indexstats.database_id = DB_ID()
AND indexstats.avg_fragmentation_in_percent >= 5.0
AND indexstats.page_count > 10
ORDER BY
indexstats.page_count ASC,
indexstats.avg_fragmentation_in_percent ASC
DECLARE TableCursor CURSOR FOR
SELECT
dbtables.[name],
dbindexes.[name],
indexstats.avg_fragmentation_in_percent,
dbschemas.[name]
FROM
sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats
INNER JOIN sys.tables dbtables
on dbtables.[object_id] = indexstats.[object_id]
INNER JOIN sys.schemas dbschemas
on dbtables.[schema_id] = dbschemas.[schema_id]
INNER JOIN sys.indexes AS dbindexes
ON dbindexes.[object_id] = indexstats.[object_id]
AND indexstats.index_id = dbindexes.index_id
WHERE
indexstats.database_id = DB_ID()
AND indexstats.avg_fragmentation_in_percent >= 5.0
AND indexstats.page_count > 10
ORDER BY
indexstats.page_count ASC,
indexstats.avg_fragmentation_in_percent ASC;
OPEN TableCursor
FETCH NEXT FROM TableCursor INTO
@TableName,
@IndexName,
@Fragmentation,
@Schema
WHILE @@FETCH_STATUS = 0
BEGIN
IF (@Fragmentation >= 30.0)
SET @IndexScript = 'ALTER INDEX [' + @IndexName + '] ON ' + @Schema + '.' + @TableName + ' REBUILD';
ELSE IF (@Fragmentation >= 5.0)
SET @IndexScript = 'ALTER INDEX [' + @IndexName + '] ON ' + @Schema + '.' + @TableName + ' REORGANIZE';
ELSE
SET @IndexScript = NULL;
IF (@IndexScript IS NOT NULL)
BEGIN
--SELECT @IndexScript
RAISERROR (@IndexScript, 10, 0) WITH NOWAIT
WAITFOR DELAY '00:00:01';
EXEC(@IndexScript);
END
FETCH NEXT FROM TableCursor INTO
@TableName,
@IndexName,
@Fragmentation,
@Schema;
END
CLOSE TableCursor;
DEALLOCATE TableCursor;
Understanding the SET Statements
The script starts with several SET statements that configure the SQL Server session settings. These ensure consistent behavior and prevent issues:
SET QUOTED_IDENTIFIER ON
This setting allows double quotes to be used as identifiers (like table and column names). When ON, SQL Server treats double-quoted strings as object identifiers rather than string literals. This is important when working with object names that might be reserved words or contain special characters.
SET ARITHABORT ON
ARITHABORT terminates a query when an overflow or divide-by-zero error occurs. Setting it ON is a best practice because it prevents queries from continuing with invalid data, which is especially important during maintenance operations. This setting can also affect index usage in some scenarios.
SET NUMERIC_ROUNDABORT OFF
This setting controls error generation when rounding causes a loss of precision. Setting it OFF prevents errors from being raised during rounding operations. When ARITHABORT is ON, NUMERIC_ROUNDABORT is typically set to OFF to avoid conflicts.
SET CONCAT_NULL_YIELDS_NULL ON
When this is ON, concatenating a NULL value with any string results in NULL. This ensures predictable string concatenation behavior in the script when building dynamic SQL statements.
SET ANSI_NULLS ON
This setting controls NULL comparison behavior. When ON, any comparison with NULL using = or != returns NULL (unknown) rather than TRUE or FALSE. This is the SQL standard behavior and ensures consistent NULL handling.
SET ANSI_PADDING ON
When ON, strings are padded to their defined length, and trailing blanks in varchar values are significant. This ensures consistent string comparison behavior.
SET ANSI_WARNINGS ON
This setting enables ANSI standard warning messages for conditions like divide-by-zero, NULL in aggregate functions, and string truncation. Keeping warnings enabled helps identify potential issues during index maintenance.
How the Script Works
Step 1: Identify Fragmented Indexes
The first SELECT statement uses sys.dm_db_index_physical_stats to identify fragmented indexes. This dynamic management view provides physical statistics about indexes, including fragmentation percentages.
The query filters for indexes where:
avg_fragmentation_in_percent >= 5.0: Only indexes with at least 5% fragmentationpage_count > 10: Only indexes with more than 10 pages (small indexes don't benefit from maintenance)database_id = DB_ID(): Only indexes in the current database
Results are ordered by page count (ascending) and fragmentation (ascending), so smaller, less fragmented indexes are processed first. This is a safety measure - if you need to stop the script, you've at least fixed the smaller indexes first.
Step 2: Process Each Index with a Cursor
The script uses a cursor to iterate through each fragmented index. While cursors can be slower than set-based operations, they're appropriate here because:
- Each index needs individual processing
- We want to control the order of operations
- We need to build and execute dynamic SQL for each index
Step 3: Choose REBUILD or REORGANIZE
For each index, the script decides whether to REBUILD or REORGANIZE based on fragmentation level:
- Fragmentation >= 30%: Uses
ALTER INDEX ... REBUILD - Fragmentation >= 5% but < 30%: Uses
ALTER INDEX ... REORGANIZE - Fragmentation < 5%: No action (though this shouldn't happen due to the WHERE clause)
REBUILD vs REORGANIZE: When to Use Each
REBUILD
REBUILD completely rebuilds the index from scratch. It:
- Eliminates fragmentation completely
- Updates statistics automatically
- Reclaims disk space
- Requires more resources (CPU, I/O, locks)
- Can be done online (SQL Server 2012+) with
WITH (ONLINE = ON)
Use REBUILD for severe fragmentation (30%+) because it's more thorough and efficient for heavily fragmented indexes.
REORGANIZE
REORGANIZE physically reorganizes leaf-level pages to match logical order. It:
- Reduces fragmentation but doesn't eliminate it completely
- Uses fewer resources than REBUILD
- Can be interrupted and resumed
- Is always online (doesn't block queries)
- Doesn't update statistics automatically
Use REORGANIZE for moderate fragmentation (5-30%) because it's faster and less resource-intensive, while still providing significant benefit.
Step 4: Execute the Maintenance
For each index, the script:
- Builds the ALTER INDEX statement dynamically
- Uses
RAISERROR ... WITH NOWAITto output the command immediately (so you can see progress) - Waits 1 second (
WAITFOR DELAY '00:00:01') to prevent overwhelming the server - Executes the maintenance command
- Moves to the next index
How to Use This Script
Step 1: Review Fragmentation First
Before running the maintenance script, run just the SELECT query to see what will be processed:
SELECT
dbtables.[name],
dbindexes.[name],
indexstats.avg_fragmentation_in_percent,
indexstats.page_count [pages]
FROM
sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats
INNER JOIN sys.tables dbtables
on dbtables.[object_id] = indexstats.[object_id]
INNER JOIN sys.schemas dbschemas
on dbtables.[schema_id] = dbschemas.[schema_id]
INNER JOIN sys.indexes AS dbindexes
ON dbindexes.[object_id] = indexstats.[object_id]
AND indexstats.index_id = dbindexes.index_id
WHERE
indexstats.database_id = DB_ID()
AND indexstats.avg_fragmentation_in_percent >= 5.0
AND indexstats.page_count > 10
ORDER BY
indexstats.page_count ASC,
indexstats.avg_fragmentation_in_percent ASC
This shows you how many indexes will be processed and their fragmentation levels. Review this to ensure the script will run for a reasonable amount of time.
Step 2: Run During Maintenance Window
While REORGANIZE is online and REBUILD can be online (with the ONLINE option), index maintenance still consumes resources. Run this script during a maintenance window or low-activity period to minimize impact on production workloads.
Step 3: Execute the Full Script
Run the complete script in SQL Server Management Studio. The RAISERROR ... WITH NOWAIT statements will show you the progress in real-time, so you can see which indexes are being processed.
Step 4: Monitor Progress
Watch the Messages tab in SSMS to see each command being executed. The script processes indexes from smallest to largest, so you'll see progress as it works through the list.
Important Considerations
Transaction Log Space
Index maintenance operations, especially REBUILD, generate significant transaction log activity. Ensure you have:
- Sufficient transaction log space
- Appropriate backup strategy (full or bulk-logged recovery models)
- Consider using
WITH (ONLINE = ON)for REBUILD operations on Enterprise Edition
Resource Usage
Index maintenance is resource-intensive. Consider:
- Running during maintenance windows
- Adjusting the
WAITFOR DELAYif needed to reduce load - Using MAXDOP option to control parallelism for REBUILD operations
- Running on a subset of indexes if needed (modify the WHERE clause)
Large Indexes
For very large indexes, REBUILD operations can take hours. Consider:
- Using
WITH (ONLINE = ON)to allow queries during rebuild - Breaking large indexes into separate maintenance jobs
- Monitoring and potentially adjusting the fragmentation threshold
Enhanced Version with Online Rebuild
For SQL Server Enterprise Edition, you can enhance the script to use online rebuilds:
IF (@Fragmentation >= 30.0)
SET @IndexScript = 'ALTER INDEX [' + @IndexName + '] ON ' + @Schema + '.' + @TableName + ' REBUILD WITH (ONLINE = ON)';
ELSE IF (@Fragmentation >= 5.0)
SET @IndexScript = 'ALTER INDEX [' + @IndexName + '] ON ' + @Schema + '.' + @TableName + ' REORGANIZE';
Online rebuilds allow queries to continue accessing the table during maintenance, but they require Enterprise Edition and use more resources than offline rebuilds.
Automating with SQL Agent Jobs
To automate this maintenance, create a SQL Server Agent job that runs on a schedule:
- Create a new SQL Server Agent job
- Add a step with this script
- Schedule it to run during maintenance windows (e.g., weekly during low-activity periods)
- Configure notifications for job completion/failure
Be careful with automation - you may want to add additional checks or logging before scheduling this to run automatically.
Best Practices
- Run regularly: Schedule index maintenance weekly or monthly depending on your database's update patterns
- Monitor fragmentation: Track fragmentation levels over time to understand your maintenance needs
- Consider fill factor: If indexes fragment quickly, consider adjusting fill factor to leave more space for inserts
- Update statistics: After index maintenance, consider updating statistics (REBUILD does this automatically, but REORGANIZE doesn't)
- Test first: Run the SELECT query first to understand what will be processed
- Monitor performance: Track query performance before and after maintenance to measure improvement
When Not to Use This Script
This script may not be appropriate if:
- You have very large indexes that would take too long to maintain
- Your maintenance window is too short
- You need more granular control over which indexes are maintained
- You want to use Ola Hallengren's IndexOptimize (a popular third-party solution)
- You need to maintain specific indexes with custom options
Conclusion
This script automates index maintenance by identifying fragmented indexes and choosing the appropriate maintenance operation based on fragmentation level. REBUILD for severe fragmentation (30%+), REORGANIZE for moderate fragmentation (5-30%), and skip small indexes that don't benefit from maintenance.
Regular index maintenance is essential for maintaining query performance as databases grow and change. This script provides a solid foundation for automated index maintenance, but remember to monitor resource usage, transaction log space, and adjust thresholds based on your specific environment.
Start by running the SELECT query to see what needs maintenance, then run the full script during a maintenance window. For production environments, consider enhancing it with online rebuilds, better logging, and scheduling it as a SQL Agent job for regular automated maintenance.