Identifying unused non clustered index

Having an Index on a table will indeed be good for the performance of queries. At the same time having too many indexes on a table will hurt the query performance, increases storage requirement and add additional overhead for maintenance.

This article covers how you can identify unused non-clustered indexes in the database.

SQL Server logs the number of times an index was scanned by the user query and time when it was last scanned. Similarly, seek, lookup and updates operations are logged and they are visible through the dynamic management view (DMV). sys.dm_db_index_usage_stats is the view that shows the index usage information and it will be useful for us to determine the index usage.

The following script will list all the non-clustered indexes from the database except the index with the primary key. It shows record count, record size for each index. Provides the index usage count and the last time index accessed and SQL Server start time.

It is important to know the start time of the SQL server as the information provided by the view is cleaned whenever the server restarted. Refer to BOL for more information about the view and limitations.

The information returned from the usage count is aggregated from all the four operations (scan, seek, lookup and updates)

The data returned from the Last_User_Used_date is the maximum of all the four operations (scan, seek, lookup and updates)


declare @start_time as datetime 
select 
  @start_time = sqlserver_start_time 
from 
  sys.dm_os_sys_info;
with nonclustered_index_size as (
  select 
    t.object_id, 
    i.index_id, 
    t.name as table_name, 
    schema_name(t.schema_id) as table_schema, 
    i.name as index_name, 
    i.type_desc as index_type, 
    pa.data_compression_desc, 
    au.type_desc as storage_type, 
    pa.rows, 
    au.total_pages 
  from 
    sys.tables t 
    join sys.indexes i on i.object_id = t.object_id 
    and i.index_id >= 2 
    and i.is_primary_key = 0 
    join sys.partitions pa on t.object_id = pa.object_id 
    and i.index_id = pa.index_id 
    join sys.allocation_units au 
    on au.container_id = case when au.type in (1, 3)
       then pa.hobt_id else pa.partition_id end
), 
index_usage_stats as (
  select 
    object_id, 
    index_id, 
    Last_User_Used_date, 
    usage_count 
  from 
    sys.dm_db_index_usage_stats cross apply (
      select 
        max(n) 
      from 
        (
          values 
            (last_user_lookup), 
            (last_user_scan), 
            (last_user_seek), 
            (last_user_update)
        ) as m(n)
    ) as a(Last_User_Used_date) cross apply (
      select 
        sum(n) 
      from 
        (
          values 
            (user_seeks), 
            (user_scans), 
            (user_lookups), 
            (user_updates)
        ) as m(n)
    ) as b(usage_count) 
  where 
    database_id = db_id()
) 
select 
  table_schema, 
  table_name, 
  index_name, 
  index_type, 
  data_compression_desc, 
  storage_type, 
  rows, 
  total_pages, 
  total_pages * 8 as total_kb, 
  usage_count, 
  Last_User_Used_date, 
  @start_time as server_start_time 
from 
  nonclustered_index_size s 
  left join index_usage_stats u on s.object_id = u.object_id 
  and s.index_id = u.index_id 
order by 
  s.table_name, 
  s.index_name

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: