See Also: Main_Page - Transact SQL Code Library - Index Performance Tuning

This query can help identify indexes that have not been used since the last restart. You can also switch the sort order to see your heavily used indexes.

The "reads_per_write" field helps to find indexes that aren't helping to improve performance. For every 1 write to the index, you want to see as many reads as possible. Indexes with a reads_per_write score of 1 mean that for every 1 write, the index is also used 1 time to help with performance. Ideally, you want to see scores much higher than that. Consider dropping indexes with a reads_per_write score under zero, and strongly consider dropping ones with scores under .1.

This isn't a hard-and-fast rule: for example, you may have an index that's only used once per month for a single report, but that report is run by the CEO and he wants it instantaneously. Before dropping indexes, know what they're used for, or make sure alternate indexes exist. Alternate indexes would be indexes that are wider than the index you're dropping, and include enough fields to serve the query's needs.

Contents

Tutorial Video

In this short tutorial video, Brent Ozar explains how to use this code to tune your environment. http://tutorials.sqlserverpedia.com/SQLServerPedia-20090324-IndexTuning1.flv

T-SQL Code

SELECT 
o.name
, indexname=i.name
, i.index_id   
, reads=user_seeks + user_scans + user_lookups   
, writes =  user_updates   
, rows = (SELECT SUM(p.rows) FROM sys.partitions p WHERE p.index_id = s.index_id AND s.object_id = p.object_id)
, CASE
	WHEN s.user_updates < 1 THEN 100
	ELSE 1.00 * (s.user_seeks + s.user_scans + s.user_lookups) / s.user_updates

 END AS reads_per_write

, 'DROP INDEX ' + QUOTENAME(i.name) 
+ ' ON ' + QUOTENAME(c.name) + '.' + QUOTENAME(OBJECT_NAME(s.object_id)) as 'drop statement'
FROM sys.dm_db_index_usage_stats s  
INNER JOIN sys.indexes i ON i.index_id = s.index_id AND s.object_id = i.object_id   
INNER JOIN sys.objects o on s.object_id = o.object_id
INNER JOIN sys.schemas c on o.schema_id = c.schema_id
WHERE OBJECTPROPERTY(s.object_id,'IsUserTable') = 1
AND s.database_id = DB_ID()   
AND i.type_desc = 'nonclustered'
AND i.is_primary_key = 0
AND i.is_unique_constraint = 0
AND (SELECT SUM(p.rows) FROM sys.partitions p WHERE p.index_id = s.index_id AND s.object_id = p.object_id) > 10000
ORDER BY reads

Query Test Checklist

  • Works on SQL Server 2008: Yes
  • Works on SQL Server 2005: Yes
  • Works on SQL Server 2000: No. Unfortunately, there's no way to get this data for SQL Server 2000.
  • Works on Standard Edition: Yes
  • Works on case-sensitive servers: Yes

Changelog

2009-07-04 - Brent Ozar - now filters out duplicate rows for partitioned tables.