Index Fragmentation and Statistics Maintenance in SQL Server: REORGANIZE vs REBUILD

Index Fragmentation and Statistics Maintenance in SQL Server: REORGANIZE vs REBUILD

“Always rebuild indexes weekly” is one of the most common DBA myths in this entire field. Here’s what fragmentation and statistics actually require.

Fragmentation & Stats, sketched out(measure — don’t schedule blindly)Healthy Index1234pages in physical order— one smooth scanFragmented Index1324pages scattered on disk— extra random I/O jumps< 5%5–30%> 30%do nothingREORGANIZE (online)REBUILDA tiny, always-seeked table at60% fragmentation? Doesn’t matter.Only large range-scanned tables care. 📌

Measuring Fragmentation First

This is the direct extension of Ch.8’s index-usage DMV lesson — same evidence-first instinct, now pointed at internal index health instead of usage frequency.

SELECT OBJECT_NAME(ips.object_id) AS table_name, i.name AS index_name,
    ips.avg_fragmentation_in_percent, ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id
WHERE ips.avg_fragmentation_in_percent > 5
ORDER BY ips.avg_fragmentation_in_percent DESC;

The Standard Thresholds

Fragmentation Action
< 5% Do nothing
5–30% ALTER INDEX ... REORGANIZE — online, lighter-weight
> 30% ALTER INDEX ... REBUILD — heavier, can be ONLINE = ON in Enterprise

The Myth, Directly Addressed

“Always rebuild weekly” wastes CPU/IO on indexes that were never fragmented — measure first, every time

A small, rarely-scanned table can sit at 60% fragmentation and never matter, because it’s tiny and always read via seeks. Fragmentation only meaningfully affects large tables read via range scans. Evidence-first applies here too: measure, don’t schedule blindly.

Statistics: Often the Real Culprit

-- Check when statistics were last updated and how many rows have changed since
SELECT OBJECT_NAME(s.object_id) AS table_name, s.name AS stats_name,
    sp.last_updated, sp.rows, sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_NAME(s.object_id) = 'YourTableName';

-- Manually refresh if auto-update hasn't triggered recently
UPDATE STATISTICS dbo.YourTableName WITH FULLSCAN;

By default, auto-update statistics fires after roughly 20% of rows change (with some newer, more granular thresholds on recent SQL Server versions for large tables). A query that suddenly gets a bad plan after a large bulk load is very often stale statistics, not fragmentation — check this DMV before reaching for a rebuild.


Enjoyed this?

Subscribe to get every new lesson as soon as it’s published, and share it with a developer who’d find it useful.

📡 Subscribe via RSS  | 
Share on X  | 
Share on LinkedIn  | 
Share on Facebook

Want the full structured course with quizzes and hands-on labs? Check out SQL Server Performance Tuning, coming soon on this site.