Quick Answer
Index fragmentation occurs when logical ordering of index pages doesn't match physical storage ordering, forcing SQL Server to perform additional I/O operations during seeks and scans. Fragmentation above 10% typically degrades query performance, while above 30% severely impacts read operations requiring immediate attention.
Root Cause Analysis
Index fragmentation develops when the buffer pool manager cannot maintain contiguous page allocation during INSERT, UPDATE, and DELETE operations. When SQL Server's storage engine allocates new pages for index leaf levels, it attempts to place them physically adjacent to existing pages within extents. However, page splits during INSERT operations or random DELETE patterns force the allocation manager to place new pages in non-contiguous locations across different extents.
The lock manager compounds this issue during high-concurrency workloads. When multiple sessions modify indexed data simultaneously, SQL Server uses page-level or row-level locks that can force suboptimal page splits. The database engine's page split algorithm prioritizes maintaining logical B-tree structure over physical page ordering, creating fragmentation.
SQL Server 2016 introduced online index rebuilds for columnstore indexes and improved the resumable operations feature in 2017. SQL Server 2019 enhanced the intelligent query processing engine to better handle fragmented indexes through adaptive memory grants, though physical I/O penalties remain. Buffer Pool Extensions (introduced in SQL Server 2014) can partially mitigate fragmentation impact by caching pages on SSD storage tiers, but the underlying physical disorganization persists.
The query optimizer estimates I/O costs based on statistics that don't account for fragmentation levels. This causes execution plans to underestimate actual read costs, leading to suboptimal join algorithms and memory grant calculations when fragmentation exceeds 15-20%.
AutoDBA checks Index fragmentation levels, maintenance schedules, and page split monitoring across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Check fragmentation levels for all indexes in current database
SELECT
OBJECT_NAME(ips.object_id) AS table_name,
i.name AS index_name,
ips.index_type_desc,
ips.avg_fragmentation_in_percent,
ips.fragment_count,
ips.page_count,
ips.avg_page_space_used_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10 AND ips.page_count > 100
ORDER BY ips.avg_fragmentation_in_percent DESC;
-- Identify indexes with high logical fragmentation causing excessive reads
SELECT
OBJECT_NAME(ips.object_id) AS table_name,
i.name AS index_name,
ips.avg_fragmentation_in_percent,
ips.avg_fragment_size_in_pages,
ios.leaf_allocation_count,
ios.leaf_ghost_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
INNER JOIN sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
ON ips.object_id = ios.object_id AND ips.index_id = ios.index_id
WHERE ips.avg_fragmentation_in_percent > 20
ORDER BY ios.leaf_allocation_count DESC;
-- Find indexes causing page split pressure from fragmentation
SELECT
OBJECT_NAME(ios.object_id) AS table_name,
i.name AS index_name,
ios.leaf_insert_count,
ios.leaf_page_merge_count,
ios.leaf_allocation_count,
CASE WHEN ios.leaf_insert_count > 0
THEN (ios.leaf_allocation_count * 100.0) / ios.leaf_insert_count
ELSE 0 END AS page_split_ratio
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) ios
INNER JOIN sys.indexes i ON ios.object_id = i.object_id AND ios.index_id = i.index_id
WHERE ios.leaf_allocation_count > 100
ORDER BY page_split_ratio DESC;
-- Correlate fragmentation with actual query performance impact
SELECT
qs.sql_handle,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.total_physical_reads / qs.execution_count AS avg_physical_reads,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.total_physical_reads / qs.execution_count > 1000
ORDER BY avg_physical_reads DESC;
-- Monitor current index maintenance operations and blocking
SELECT
r.session_id,
r.command,
r.status,
r.percent_complete,
r.estimated_completion_time,
DB_NAME(r.database_id) AS database_name,
r.blocking_session_id,
w.wait_type,
w.wait_time_ms
FROM sys.dm_exec_requests r
LEFT JOIN sys.dm_os_waiting_tasks w ON r.session_id = w.session_id
WHERE r.command LIKE '%INDEX%'
OR r.command LIKE '%DBCC%'
ORDER BY r.percent_complete DESC;
Fix Scripts
Reorganize moderately fragmented indexes (10-30%)
-- Reorganizes indexes online with minimal blocking
DECLARE @sql NVARCHAR(MAX) = '';
SELECT @sql = @sql +
'ALTER INDEX [' + i.name + '] ON [' + SCHEMA_NAME(o.schema_id) + '].[' + o.name + '] REORGANIZE;' + CHAR(13)
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
INNER JOIN sys.objects o ON ips.object_id = o.object_id
WHERE ips.avg_fragmentation_in_percent BETWEEN 10 AND 30
AND ips.page_count > 100
AND i.type_desc IN ('CLUSTERED', 'NONCLUSTERED');
PRINT @sql;
-- EXEC sp_executesql @sql; -- Uncomment after review
Reorganize operations run online with minimal impact but require more log space. Test during maintenance windows for large indexes exceeding 10GB.
Rebuild heavily fragmented indexes (>30%)
-- Rebuilds indexes with online option where supported
DECLARE @sql NVARCHAR(MAX) = '';
SELECT @sql = @sql +
'ALTER INDEX [' + i.name + '] ON [' + SCHEMA_NAME(o.schema_id) + '].[' + o.name + ']
REBUILD WITH (ONLINE = ON, MAXDOP = 4, SORT_IN_TEMPDB = ON, FILLFACTOR = 90);' + CHAR(13)
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
INNER JOIN sys.objects o ON ips.object_id = o.object_id
WHERE ips.avg_fragmentation_in_percent > 30
AND ips.page_count > 100
AND i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
AND o.type = 'U'; -- User tables only
PRINT @sql;
-- EXEC sp_executesql @sql; -- Uncomment after review in dev
Rebuilds require twice the index size in tempdb and can cause blocking despite ONLINE option. Monitor for long-running transactions and plan during low-activity periods.
Create maintenance plan with thresholds
-- Automated maintenance based on fragmentation thresholds and usage patterns
WITH IndexMaintenance AS (
SELECT
SCHEMA_NAME(o.schema_id) + '.' + o.name AS table_name,
i.name AS index_name,
ips.avg_fragmentation_in_percent,
ips.page_count,
ios.user_seeks + ios.user_scans + ios.user_lookups AS user_access,
CASE
WHEN ips.avg_fragmentation_in_percent > 30 AND ips.page_count > 1000 THEN 'REBUILD'
WHEN ips.avg_fragmentation_in_percent > 10 AND ips.page_count > 100 THEN 'REORGANIZE'
ELSE 'SKIP'
END AS maintenance_action
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
INNER JOIN sys.objects o ON ips.object_id = o.object_id
LEFT JOIN sys.dm_db_index_usage_stats ios ON ips.object_id = ios.object_id AND ips.index_id = ios.index_id
WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
AND o.type = 'U'
)
SELECT
table_name,
index_name,
avg_fragmentation_in_percent,
page_count,
user_access,
maintenance_action,
CASE maintenance_action
WHEN 'REBUILD' THEN 'ALTER INDEX [' + index_name + '] ON [' + table_name + '] REBUILD WITH (ONLINE=ON, FILLFACTOR=90);'
WHEN 'REORGANIZE' THEN 'ALTER INDEX [' + index_name + '] ON [' + table_name + '] REORGANIZE;'
ELSE 'NO ACTION'
END AS maintenance_sql
FROM IndexMaintenance
WHERE maintenance_action <> 'SKIP'
ORDER BY avg_fragmentation_in_percent DESC;
Prioritizes maintenance based on fragmentation level and actual usage patterns. Unused indexes with high fragmentation should be evaluated for removal rather than maintenance.
Monitor maintenance progress and impact
-- Real-time monitoring of index maintenance operations
SELECT
r.session_id,
r.command,
DB_NAME(r.database_id) AS database_name,
r.status,
r.percent_complete,
CAST(((r.estimated_completion_time / 1000) / 60) AS INT) AS estimated_minutes_remaining,
r.cpu_time,
r.logical_reads,
r.writes,
w.wait_type,
w.wait_time_ms,
SUBSTRING(qt.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
ELSE r.statement_end_offset END - r.statement_start_offset)/2)+1) AS current_statement
FROM sys.dm_exec_requests r
LEFT JOIN sys.dm_os_waiting_tasks w ON r.session_id = w.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) qt
WHERE r.command IN ('ALTER INDEX', 'DBCC TABLE CHECK')
ORDER BY r.percent_complete DESC;
Use this during maintenance windows to identify blocked operations or excessive resource consumption requiring intervention.
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure fill factor to 90% for indexes experiencing frequent INSERT operations and random page splits. Clustered indexes on IDENTITY columns should maintain default 100% fill factor, while indexes on GUID primary keys require 70-80% fill factor to accommodate random insertions.
Implement automated maintenance using SQL Server Agent jobs with dynamic thresholds. Schedule reorganize operations weekly for fragmentation 10-30%, rebuild operations monthly for fragmentation above 30%. Exclude indexes smaller than 100 pages from maintenance routines.
Monitor page split rates using Performance Monitor counters "Page Splits/sec" and "Page Allocations/sec". Sustained ratios above 20 page splits per 100 allocations indicate suboptimal indexing strategies requiring clustered index evaluation.
Enable Query Store in SQL Server 2016+ to correlate fragmentation levels with query performance regression. Plan regression detection automatically identifies queries degraded by fragmentation, enabling targeted index maintenance rather than database-wide operations.
Partition large tables based on data access patterns to isolate fragmentation impact. Sliding window partitioning maintains recent data with minimal fragmentation while allowing older partitions to remain fragmented without performance impact.
Configure tempdb with multiple data files sized equally to support concurrent index rebuild operations. Insufficient tempdb space during rebuilds causes failures and incomplete maintenance cycles, perpetuating fragmentation issues.
Need hands-on help?
Dealing with persistent index fragmentation issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.
Related Pages
Missing Indexes
Index Maintenance
sys.dm_db_index_operational_stats
Index Analysis
sys.dm_db_index_physical_stats
Index Analysis
sys.dm_db_index_usage_stats
Index Analysis
sys.dm_db_missing_index_details
Index Analysis
sys.dm_db_missing_index_group_stats
Index Analysis