solving sql server problems for millions of dbas and developers since 2006



SQL Server DBA Tips SQL Server Developer Tips SQL Server Business Intelligence Tips SQL Server Career Tips SQL Server Tip Categories SQL Server Tutorials SQL Server Webcasts SQL Server Whitepapers SQL Server Tools SQL Server Questions and Answers MSSQLTips Authors About MSSQLTips SQL Server User Groups MSSLQTips Giveaways MSSQLTips Advertising Options

MSSQLTips Facebook Page MSSQLTips LinkedIn Page MSSQLTips RSS Feed MSSQLTips Twitter Page MSSQLTips Google+ Page

SQL Product Highlight

Red Gate Software - SQL Monitor

SQL Server performance monitoring and alerting - SQL Monitor offers an easy entrance to advanced server monitoring with a simple design that's a refreshing change from the status quo. Red Gate have added custom metrics and user roles to the product without spoiling its ease-of-use, to help you answer that timeless question, 'How healthy are your servers?'

Learn more!








Deeper insight into unused indexes for SQL Server

By: | Read Comments (9) | Print

Greg is the President of Edgewood Solutions and a co-founder of MSSQLTips.com.

Related Tips: More

Problem
One of the balancing acts of SQL Server is the use of indexes.  Too few indexes can cause scans to occur which hurts performance and too many indexes causes overhead for index maintenance during data updates and also a bloated database.  So what steps can be taken to determine which indexes are being used and how they are being used.

Solution
In a previous tip, How to get index usage information in SQL Server, we talked about how to get index usage information by using the DMVs sys.dm_db_index_operational_stats and sys.dm_db_index_usage_stats.  The one issue with these views is that it only gives you part of the picture and you still need to dig deeper to get all of the information you need to determine which indexes are not used.

In this tip we will take it a step further and provide some additional queries that will shed some much needed light on the issue. 

We will be using the DMV sys.dm_db_index_usage_stats which keeps track of each index that has been used and how it has been used.  This was covered in the tip mentioned above.  These stats are collected whenever an object is accessed.   SQL Server resets these values if SQL Server is restarted or if you detach and reattach the database.

There are seven queries below and each one builds upon the others to give you more and more information. If you can't wait you can just skip right to Query 7.  To select the code you should be able to triple click in the table cell to select all of the code.

Note: the information below was collected by restarting SQL Server and then doing selected queries on the AdventureWorks database.  For a more heavily used database your numbers and index usage will be much higher.


Query 1

In this first query we are just using sys.dm_db_index_usage_stats and sys.objects to get a list of the indexes that have been used and how they are being used.

SELECT DB_NAME(DATABASE_IDAS DATABASENAME,
       
OBJECT_NAME(B.OBJECT_IDAS TABLENAME,
       
INDEX_NAME (SELECT NAME
                     
FROM   SYS.INDEXES A
                     
WHERE  A.OBJECT_ID B.OBJECT_ID
                            
AND A.INDEX_ID B.INDEX_ID),
       
USER_SEEKS,
       
USER_SCANS,
       
USER_LOOKUPS,
       
USER_UPDATES
FROM   SYS.DM_DB_INDEX_USAGE_STATS B
       
INNER JOIN SYS.OBJECTS C
         
ON B.OBJECT_ID C.OBJECT_ID
WHERE  DATABASE_ID DB_ID(DB_NAME())
       AND 
C.TYPE <> 'S'


Query 2

In this query we are listing each user table and all of the tables indexes that have not been used by using a NOT EXISTS against sys.dm_db_index_usage_stats.

SELECT   DB_NAME() AS DATABASENAME,
         
OBJECT_NAME(B.OBJECT_IDAS TABLENAME,
         
B.NAME AS INDEXNAME,
         
B.INDEX_ID
FROM     SYS.OBJECTS A
         
INNER JOIN SYS.INDEXES B
           
ON A.OBJECT_ID B.OBJECT_ID
WHERE    NOT EXISTS (SELECT *
                     
FROM   SYS.DM_DB_INDEX_USAGE_STATS C
                     
WHERE  B.OBJECT_ID C.OBJECT_ID
                            
AND B.INDEX_ID C.INDEX_ID)
         AND 
A.TYPE <> 'S'
ORDER BY 123


Query 3

In this query we are listing each user table, all of its indexes and the columns that make up the index.  The issue with this query is that you have a row for each column in the index which could get confusing if you have a lot of indexes.

SELECT   A.NAME,
         
B.NAME,
         
C.KEY_ORDINAL,
         
D.NAME
FROM     SYS.OBJECTS A
         
INNER JOIN SYS.INDEXES B
           
ON A.OBJECT_ID B.OBJECT_ID
         
INNER JOIN SYS.INDEX_COLUMNS C
           
ON B.OBJECT_ID C.OBJECT_ID
              
AND B.INDEX_ID C.INDEX_ID
         
INNER JOIN SYS.COLUMNS D
           
ON C.OBJECT_ID D.OBJECT_ID
              
AND C.COLUMN_ID D.COLUMN_ID
WHERE    A.TYPE <> 'S'
ORDER BY 123


Query 4

In this query we use most of Query 3, but we are doing a PIVOT so we can see the index and the index columns in one row.  This only accounts for 7 index columns, but it could easily be increased to handle more in the PIVOT operation.  Here is another tip related to the use of PIVOT, Crosstab queries using PIVOT in SQL Server 2005. if you would like to better understand how PIVOT works.

SELECT   TABLENAME, INDEXNAME, INDEXID, [1] AS COL1, [2] AS COL2, [3] AS COL3,
         
[4] AS COL4,  [5] AS COL5, [6] AS COL6, [7] AS COL7
FROM     (SELECT A.NAME AS TABLENAME,
                 
B.NAME AS INDEXNAME,
                 
B.INDEX_ID AS INDEXID,
                 
D.NAME AS COLUMNNAME,
                 
C.KEY_ORDINAL
          
FROM   SYS.OBJECTS A
                 
INNER JOIN SYS.INDEXES B
                   
ON A.OBJECT_ID B.OBJECT_ID
                 
INNER JOIN SYS.INDEX_COLUMNS C
                   
ON B.OBJECT_ID C.OBJECT_ID
                      
AND B.INDEX_ID C.INDEX_ID
                 
INNER JOIN SYS.COLUMNS D
                   
ON C.OBJECT_ID D.OBJECT_ID
                      
AND C.COLUMN_ID D.COLUMN_ID
          
WHERE  A.TYPE <> 'S'P
         PIVOT
         
(MIN(COLUMNNAME)
          
FOR KEY_ORDINAL IN [1],[2],[3],[4],[5],[6],[7] ) ) AS PVT
ORDER BY TABLENAMEINDEXNAME;


Query 5

In this query we tie in our PIVOT query above with sys.dm_db_index_usage_stats so we can look at only the indexes that have been used since the last time the stats were reset.

SELECT   TABLENAME, INDEXNAME, INDEX_ID, [1] AS COL1, [2] AS COL2, [3] AS COL3,
         
[4] AS COL4[5] AS COL5, [6] AS COL6, [7] AS COL7
FROM     (SELECT A.NAME AS TABLENAME,
                 
A.OBJECT_ID,
                 
B.NAME AS INDEXNAME,
                 
B.INDEX_ID,
                 
D.NAME AS COLUMNNAME,
                 
C.KEY_ORDINAL
          
FROM   SYS.OBJECTS A
                 
INNER JOIN SYS.INDEXES B
                   
ON A.OBJECT_ID B.OBJECT_ID
                 
INNER JOIN SYS.INDEX_COLUMNS C
                   
ON B.OBJECT_ID C.OBJECT_ID
                      
AND B.INDEX_ID C.INDEX_ID
                 
INNER JOIN SYS.COLUMNS D
                   
ON C.OBJECT_ID D.OBJECT_ID
                      
AND C.COLUMN_ID D.COLUMN_ID
          
WHERE  A.TYPE <> 'S'P
         PIVOT
         
(MIN(COLUMNNAME)
          
FOR KEY_ORDINAL IN [1],[2],[3],[4],[5],[6],[7] ) ) AS PVT
WHERE    EXISTS (SELECT OBJECT_ID,
                        
INDEX_ID
                 
FROM   SYS.DM_DB_INDEX_USAGE_STATS B
                 
WHERE  DATABASE_ID DB_ID(DB_NAME())
                        AND 
PVT.OBJECT_ID B.OBJECT_ID
                        
AND PVT.INDEX_ID B.INDEX_ID)
ORDER BY TABLENAMEINDEXNAME;


Query 6

This query also uses the PIVOT query along with sys.dm_db_index_usage_stats so we can also see the stats on the indexes that have been used.

SELECT   PVT.TABLENAME, PVT.INDEXNAME, [1] AS COL1, [2] AS COL2, [3] AS COL3,
         
[4] AS COL4, [5] AS COL5, [6] AS COL6, [7] AS COL7, B.USER_SEEKS,
         
B.USER_SCANSB.USER_LOOKUPS
FROM     (SELECT A.NAME AS TABLENAME,
                 
A.OBJECT_ID,
                 
B.NAME AS INDEXNAME,
                 
B.INDEX_ID,
                 
D.NAME AS COLUMNNAME,
                 
C.KEY_ORDINAL
          
FROM   SYS.OBJECTS A
                 
INNER JOIN SYS.INDEXES B
                   
ON A.OBJECT_ID B.OBJECT_ID
                 
INNER JOIN SYS.INDEX_COLUMNS C
                   
ON B.OBJECT_ID C.OBJECT_ID
                      
AND B.INDEX_ID C.INDEX_ID
                 
INNER JOIN SYS.COLUMNS D
                   
ON C.OBJECT_ID D.OBJECT_ID
                      
AND C.COLUMN_ID D.COLUMN_ID
          
WHERE  A.TYPE <> 'S'P
         PIVOT
         
(MIN(COLUMNNAME)
          
FOR KEY_ORDINAL IN [1],[2],[3],[4],[5],[6],[7] ) ) AS PVT
         
INNER JOIN SYS.DM_DB_INDEX_USAGE_STATS B
           
ON PVT.OBJECT_ID B.OBJECT_ID
              
AND PVT.INDEX_ID B.INDEX_ID
              
AND B.DATABASE_ID DB_ID()
ORDER BY TABLENAMEINDEXNAME;


Query 7

This last query allow us to see both used and unused indexes.  Since the DMV sys.dm_db_index_usage_stats only tracks when an index is used it is hard to compare the used and unused indexes.  The query below allows you to see all indexes to compare both used and unused indexes since the stats were collected by using a UNION.

SELECT PVT.TABLENAME, PVT.INDEXNAME, PVT.INDEX_ID, [1] AS COL1, [2] AS COL2, [3] AS COL3,
       
[4] AS COL4,  [5] AS COL5, [6] AS COL6, [7] AS COL7, B.USER_SEEKS,
       
B.USER_SCANSB.USER_LOOKUPS
FROM   (SELECT A.NAME AS TABLENAME,
               
A.OBJECT_ID,
               
B.NAME AS INDEXNAME,
               
B.INDEX_ID,
               
D.NAME AS COLUMNNAME,
               
C.KEY_ORDINAL
        
FROM   SYS.OBJECTS A
               
INNER JOIN SYS.INDEXES B
                 
ON A.OBJECT_ID B.OBJECT_ID
               
INNER JOIN SYS.INDEX_COLUMNS C
                 
ON B.OBJECT_ID C.OBJECT_ID
                    
AND B.INDEX_ID C.INDEX_ID
               
INNER JOIN SYS.COLUMNS D
                 
ON C.OBJECT_ID D.OBJECT_ID
                    
AND C.COLUMN_ID D.COLUMN_ID
        
WHERE  A.TYPE <> 'S'P
       PIVOT
       
(MIN(COLUMNNAME)
        
FOR KEY_ORDINAL IN [1],[2],[3],[4],[5],[6],[7] ) ) AS PVT
       
INNER JOIN SYS.DM_DB_INDEX_USAGE_STATS B
         
ON PVT.OBJECT_ID B.OBJECT_ID
            
AND PVT.INDEX_ID B.INDEX_ID
            
AND B.DATABASE_ID DB_ID()
UNION 
SELECT 
TABLENAME, INDEXNAME, INDEX_ID, [1] AS COL1, [2] AS COL2, [3] AS COL3,
       
[4] AS COL4[5] AS COL5, [6] AS COL6, [7] AS COL7, 0, 0, 0
FROM   (SELECT A.NAME AS TABLENAME,
               
A.OBJECT_ID,
               
B.NAME AS INDEXNAME,
               
B.INDEX_ID,
               
D.NAME AS COLUMNNAME,
               
C.KEY_ORDINAL
        
FROM   SYS.OBJECTS A
               
INNER JOIN SYS.INDEXES B
                 
ON A.OBJECT_ID B.OBJECT_ID
               
INNER JOIN SYS.INDEX_COLUMNS C
                 
ON B.OBJECT_ID C.OBJECT_ID
                    
AND B.INDEX_ID C.INDEX_ID
               
INNER JOIN SYS.COLUMNS D
                 
ON C.OBJECT_ID D.OBJECT_ID
                    
AND C.COLUMN_ID D.COLUMN_ID
        
WHERE  A.TYPE <> 'S'P
       PIVOT
       
(MIN(COLUMNNAME)
        
FOR KEY_ORDINAL IN [1],[2],[3],[4],[5],[6],[7] ) ) AS PVT
WHERE  NOT EXISTS (SELECT OBJECT_ID,
                          
INDEX_ID
                   
FROM   SYS.DM_DB_INDEX_USAGE_STATS B
                   
WHERE  DATABASE_ID DB_ID(DB_NAME())
                          AND 
PVT.OBJECT_ID B.OBJECT_ID
                          
AND PVT.INDEX_ID B.INDEX_ID)
ORDER BY TABLENAMEINDEX_ID;


Next Steps

  • The above queries should give you a jump start to determine how indexes are being used and which indexes are being used.  This will allow you to remove unused indexes as well as look for duplicate indexes that can be removed.
  • Based on the information collected you can determine which indexes can safely be dropped.  Just make sure you collect stats long enough to have a good sampling of queries that are run against your database.
  • Now that you know how to determine which unused indexes you can get rid of, stay tuned for future tips on how to determine which indexes are needed


Related Tips: More | Become a paid author


Last Update: 7/22/2008

Share: Share 






Comments and Feedback:

Tuesday, July 22, 2008 - 2:12:34 AM - Jensch Read The Tip

Realy nice statements,but my SSMS seems to dislike Query 3.
It disconnects at every attempt executing the query.
Any idea as to this?

Best regards
- Jens


Tuesday, July 22, 2008 - 4:50:01 AM - admin Read The Tip

Not sure why this would be the case. 

The one issue I have found is if you have a case sensitive database the upper case causes an issue.


Wednesday, July 23, 2008 - 1:44:43 AM - ESL Read The Tip

 Hi,

sorry but all the request with PIVOT option doesn't work, error is :

Line 20: Incorrect syntax near 'PIVOT'.

 

do you have an issue to correct .

 regards ,

 

Eric 


Wednesday, July 23, 2008 - 5:22:29 AM - grobido Read The Tip

Are you using SQL 2005?  PIVOT only works with SQL Server 2005 and up.


Wednesday, July 23, 2008 - 5:33:12 AM - ESL Read The Tip

 Yes i do,

i have the solution, this is beacause  QUOTED_IDENTIFIER  is not set on  

 

thank's 


Tuesday, August 05, 2008 - 1:59:19 AM - Rimsky Read The Tip

Very valuable, thank you. I have a number of databases with the same structure and indexes. Is there a way I can run the following query on all databases? I think I need to join to master..sysdatabases where databasename like 'McBam%' , but how do I do that?

SELECT DB_NAME() AS DATABASENAME,

OBJECT_NAME(B.OBJECT_ID) AS TABLENAME,

B.NAME AS INDEXNAME,

B.INDEX_ID

FROM SYS.OBJECTS A

INNER JOIN SYS.INDEXES B

ON A.OBJECT_ID = B.OBJECT_ID

WHERE NOT EXISTS (SELECT *

FROM SYS.DM_DB_INDEX_USAGE_STATS C

WHERE B.OBJECT_ID = C.OBJECT_ID

AND B.INDEX_ID = C.INDEX_ID)

AND A.TYPE <> 'S'

AND B.NAME > ''

AND B.NAME not like '%rowid%'

ORDER BY 1, 2, 3


Tuesday, August 05, 2008 - 7:11:07 AM - aprato Read The Tip

 Use sp_MSForEachDB.   This should not be used as production code. 

Double check query too... I didn't test it.

sp_MSForEachDB 'use ?;
                declare @name sysname
                set @name = db_name()
                if @name like ''McBam%''
                   SELECT DB_NAME() AS DATABASENAME, OBJECT_NAME(B.OBJECT_ID) AS TABLENAME, B.NAME AS INDEXNAME, B.INDEX_ID
                   FROM SYS.OBJECTS A
                   INNER JOIN SYS.INDEXES B ON A.OBJECT_ID = B.OBJECT_ID
                   WHERE NOT EXISTS (SELECT 1
                                     FROM SYS.DM_DB_INDEX_USAGE_STATS C
                                     WHERE B.OBJECT_ID = C.OBJECT_ID
                                     AND B.INDEX_ID = C.INDEX_ID)
                                     AND A.TYPE <> ''S''
                                     AND B.NAME > ''''
                                     AND B.NAME not like ''%rowid%''
                                     ORDER BY 1, 2, 3'
 


Thursday, September 02, 2010 - 8:34:45 AM - Douglas Osborne Read The Tip
I tweaked query 7 a bit since we have a few really long indexes - so a quick way to find your limit is to keep bumping up the pivot number until you return no rows, I had to go to [10] - eg

 

SELECT *
FROM
(
    SELECT A.NAME AS TABLENAME, A.OBJECT_ID, B.NAME AS INDEXNAME, B.INDEX_ID, D.NAME AS COLUMNNAME, C.KEY_ORDINAL
    FROM SYS.OBJECTS A
    INNER JOIN SYS.INDEXES B ON A.OBJECT_ID = B.OBJECT_ID
    INNER JOIN SYS.INDEX_COLUMNS C ON B.OBJECT_ID = C.OBJECT_ID AND B.INDEX_ID = C.INDEX_ID
    INNER JOIN SYS.COLUMNS D ON C.OBJECT_ID = D.OBJECT_ID AND C.COLUMN_ID = D.COLUMN_ID
    WHERE  A.TYPE <> 'S'
) P
PIVOT
(MIN(COLUMNNAME)
FOR KEY_ORDINAL IN ( [1],[2],[3],[4],[5],[6],[7], [8], [9], [10] ) ) AS PVT
INNER JOIN SYS.DM_DB_INDEX_USAGE_STATS B ON PVT.OBJECT_ID = B.OBJECT_ID AND PVT.INDEX_ID = B.INDEX_ID AND B.DATABASE_ID = DB_ID()
WHERE [10] IS NOT NULL

Nice script BTW,

Doug


Thursday, June 02, 2011 - 11:42:19 PM - Ludwig Read The Tip

Great article...that is surely helpful.

I have also heard that even though it seems an index is not used (counters 0 all over) system performance goes down once an index classified as unused is removed...therefore somehow in the back the index seems to be used even though it states it is not being used...not sure but maybe the index is used in a way that does not trigger the tracking of its usage.. Anyone with simillar expierence?

Thx Ludwig



Post a Comment or Question

Keep it clean and stay on the subject or we may delete your comment.
Your email address is not published. Required fields are marked with an asterisk (*)

*Name   *Email   Notify for updates
Comments
*Enter Code refresh code


 
Sponsor Information
"Amazing, Amazing, Amazing! SQL doctor is truly one of the most powerful tools I have seen."

SQL Monitor, server monitoring so easy, your boss could do it. Try it online.

What grade do you think your SQL Servers get? Find out with Edgewood's SQL Server Health Check consulting services.

Free Trial: Get Proactive Insight with SpotlightŪ for SQL Server Enterprise.

Join the over million SQL Server Professionals who get their issues resolved daily.

Are you waiting on SQL Server? Learn about these DMV's.


Copyright (c) 2006-2012 Edgewood Solutions, LLC All rights reserved
privacy | disclaimer | copyright | advertise | about
authors | contribute | feedback | giveaways | user groups
Some names and products listed are the registered trademarks of their respective owners.


Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com