I created the following query, which gives me a lot of good information to identify duplicate and almost duplicate indexes. It also includes other information, such as how many pages of memory an index occupies, which allows me to give higher priority to larger indexes. It shows which columns are indexed and which columns are included, so I can see if there are two indexes that are almost identical only to the small changes in the included columns.
WITH IndexSummary AS ( SELECT DISTINCT sys.objects.name AS [Table Name], sys.indexes.name AS [Index Name], SUBSTRING((SELECT ', ' + sys.columns.Name as [text()] FROM sys.columns INNER JOIN sys.index_columns ON sys.index_columns.column_id = sys.columns.column_id AND sys.index_columns.object_id = sys.columns.object_id WHERE sys.index_columns.index_id = sys.indexes.index_id AND sys.index_columns.object_id = sys.indexes.object_id AND sys.index_columns.is_included_column = 0 ORDER BY sys.columns.name FOR XML Path('')), 2, 10000) AS [Indexed Column Names], ISNULL(SUBSTRING((SELECT ', ' + sys.columns.Name as [text()] FROM sys.columns INNER JOIN sys.index_columns ON sys.index_columns.column_id = sys.columns.column_id AND sys.index_columns.object_id = sys.columns.object_id WHERE sys.index_columns.index_id = sys.indexes.index_id AND sys.index_columns.object_id = sys.indexes.object_id AND sys.index_columns.is_included_column = 1 ORDER BY sys.columns.name FOR XML Path('')), 2, 10000), '') AS [Included Column Names], sys.indexes.index_id, sys.indexes.object_id FROM sys.indexes INNER JOIN SYS.index_columns ON sys.indexes.index_id = SYS.index_columns.index_id AND sys.indexes.object_id = sys.index_columns.object_id INNER JOIN sys.objects ON sys.OBJECTS.object_id = SYS.indexES.object_id WHERE sys.objects.type = 'U' ) SELECT IndexSummary.[Table Name], IndexSummary.[Index Name], IndexSummary.[Indexed Column Names], IndexSummary.[Included Column Names], PhysicalStats.page_count as [Page Count], CONVERT(decimal(18,2), PhysicalStats.page_count * 8 / 1024.0) AS [Size (MB)], CONVERT(decimal(18,2), PhysicalStats.avg_fragmentation_in_percent) AS [Fragment %] FROM IndexSummary INNER JOIN sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS PhysicalStats ON PhysicalStats.index_id = IndexSummary.index_id AND PhysicalStats.object_id = IndexSummary.object_id WHERE (SELECT COUNT(*) as Computed FROM IndexSummary Summary2 WHERE Summary2.[Table Name] = IndexSummary.[Table Name] AND Summary2.[Indexed Column Names] = IndexSummary.[Indexed Column Names]) > 1 ORDER BY [Table Name], [Index Name], [Indexed Column Names], [Included Column Names]
The query results are as follows:
Table Name Index Indexed Cols Included Cols Pages Size (MB) Frag % My_Table Indx_1 Col1 Col2, Col3 123 0.96 8.94 My_Table Indx_2 Col1 Col2, Col3 123 0.96 8.94
Full description
For a complete explanation, see Identifying Duplicate or Redundant Indexes in SQL Server .