top of page

Help! My server ate my SQL indexes

  • Writer: sqldba
    sqldba
  • May 7, 2011
  • 4 min read

Not really, but it is a nice little title isn’t it.

So you are looking for a quick fix for your indexes. Use this little script.

Thanks to Brent Ozar and Clayton Kramer for your work on the script, I just chopped it a bit for our South African climate and added some other sections.

It will show you suggested indexes for performance improvements, then show you unused indexes and then finally heaps.

Edit: I changed the “ChangeIndexStatement” that gets generated to include a fill factor and make everything Ascending, at least I hope that is what I did.

Simple and effective.

/*-*-*-*-*-*-*-*-* ﷽‎ *-*-*-*-*-*-*-*-*-*-*-*/
/* ------------------------------------------------------------------
-- Title : FindMissingIndexes
-- Author : Brent Ozar
-- Date : 2009-04-01
-- Last Modified Date : 2012-02-20
-- Modified By : 
Clayton Kramer ckramer.kramer@gmail.com
Adrian Sullivan adriaan.sullivan@gmail.com
-- Description: This query returns indexes that SQL Server 2005(and higher) thinks are missing since the last restart.
-- The "SeekImpact" & "Scan Impact" columns are relative to the time of last restart and how bad SQL Server needs the index.
-- I consider over 10 to need attendtion.
-- Changes: Updated to expose full table name. This makes it easier to identify which database needs an index.
-- Modified the CreateIndexStatement to use the full table path and include the equality/inequality columns for easier identifcation.
-- Added the current Index usage to show a summary of unused indexes.. seeing as you are looking at indexes.
------------------------------------------------------------------ */

SET FMTONLY OFF/*This is for temporary tables in SSRS*/

DECLARE @MissingIndex TABLE(DB VARCHAR(250),SeekImpact DECIMAL(10,3), ScanImpact DECIMAL(10,3), [Table] VARCHAR(2000), ChangeIndexStatement VARCHAR(4000), equality_columns VARCHAR(4000), inequality_columns VARCHAR(4000), included_columns VARCHAR(4000))
DECLARE @HeapTable TABLE(DB VARCHAR(250), [schema] VARCHAR(250), [table] VARCHAR(250), [rows] BIGINT, user_seeks BIGINT, user_scans BIGINT, user_lookups BIGINT, user_updates BIGINT, last_user_seek DATETIME, last_user_scan DATETIME, last_user_lookup DATETIME)
DECLARE @NeverUsedTable TABLE(DB VARCHAR(250),Consideration VARCHAR(50),TableName VARCHAR(50),TypeDesc VARCHAR(50),IndexName VARCHAR(250),Updates BIGINT, ast_user_scan DATETIME,last_user_seek DATETIME,Pages BIGINT)

DECLARE @Database VARCHAR(255), @Table VARCHAR(255), @cmd NVARCHAR(3500),@cmd2 NVARCHAR(3500), @fillfactor INT, @pagemin INT, @fragmin INT, @WaitFor VARCHAR(12),@ActivateWait VARCHAR(1), @JustShowMe VARCHAR(1)
DECLARE @SQL NVARCHAR(4000)
SET @WaitFor = '00:00:15' /*hh:mm:ss Average time for a big index is around 2 minutes, and assume around 25% the trans log filled by each rebuild*/
SET @fillfactor = 80 /*Depending or your index growth anywhere between 70-90 should work*/
SET @pagemin = 1000 /*not concerned with small tables, ignore less than 1500 pages index*/
SET @fragmin = 5 /*Ignore fragmentation less than 5%*/
SET @ActivateWait = 0 /*1 will active it*/
SET @JustShowMe = 1 /*0 is off*/
DECLARE @Databases TABLE(id INT IDENTITY(1,1), databasename VARCHAR(250))
INSERT INTO @Databases
SELECT name FROM sys.databases db
WHERE name NOT IN ('master','msdb','tempdb','model','distribution')
AND db.state <> 6
AND db.user_access <> 1
ORDER BY 1
--SELECT * FROM @Databases
SET @SQL = ''

DECLARE @i_Count INT, @i_Max INT
SET @i_Max = (SELECT MAX(id) FROM @Databases )
SET @i_Count = 1
WHILE @i_Count <= @i_Max
BEGIN
 SET @Database = (SELECT databasename FROM @Databases WHERE id = @i_Count)
 SET @SQL = '
 USE ['+@Database +']
 SELECT '''+@Database+''', (avg_total_user_cost * avg_user_impact/100.0) * (user_seeks ) SeekImpact
 , (avg_total_user_cost * avg_user_impact/100.0) * (user_scans) ScanImpact
 , [Table] = [statement]
 , [CreateIndexStatement] = ''CREATE NONCLUSTERED INDEX IX_'' + sys.objects.name COLLATE DATABASE_DEFAULT + ''_''
 + REPLACE(REPLACE(REPLACE(ISNULL(mid.equality_columns,'''')+ISNULL(mid.inequality_columns,''''), ''['', ''''), '']'',''''), '', '',''_'') + '' ON '' + [statement]
 + REPLACE('' ( '' + IsNull(mid.equality_columns, '''')
 + CASE WHEN mid.inequality_columns IS NULL THEN '''' ELSE CASE WHEN mid.equality_columns IS NULL THEN '''' ELSE '','' END + mid.inequality_columns END + '' ASC ) '' ,'','','' ASC ,'') + CASE WHEN mid.included_columns IS NULL THEN '''' ELSE ''INCLUDE ('' + mid.included_columns + '')'' END
 + ''WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON,FILLFACTOR = 90) ON [PRIMARY];''
 , mid.equality_columns
 , mid.inequality_columns
 , mid.included_columns
 FROM sys.dm_db_missing_index_group_stats AS migs
 INNER JOIN sys.dm_db_missing_index_groups AS mig ON migs.group_handle = mig.index_group_handle
 INNER JOIN sys.dm_db_missing_index_details AS mid ON mig.index_handle = mid.index_handle
 INNER JOIN sys.objects WITH (nolock) ON mid.OBJECT_ID = sys.objects.OBJECT_ID
 WHERE (migs.group_handle IN (SELECT TOP (500) group_handle FROM sys.dm_db_missing_index_group_stats WITH (nolock) ORDER BY (avg_total_user_cost * avg_user_impact) * (user_seeks + user_scans) DESC))
 AND OBJECTPROPERTY(sys.objects.OBJECT_ID, ''isusertable'') = 1
 ORDER BY SeekImpact DESC, ScanImpact DESC, [CreateIndexStatement] DESC

 '
 INSERT INTO @MissingIndex
 EXEC sp_executesql @SQL
 --PRINT @SQL

/*---------------------------------------Shows Indexes that have never been used---------------------------------------*/
 SET @SQL = '
 USE ['+@Database +']
 DECLARE @DaysAgo INT, @TheDate DATETIME
 SET @DaysAgo = 5
 SET @TheDate = CONVERT(DATETIME,CONVERT(INT,DATEADD(DAY,-@DaysAgo,GETDATE())))

DECLARE @db_id smallint, @tab_id int
 SET @db_id=db_id()
 SET @tab_id=object_id(''Production.Product'')

SELECT '''+@Database+''',
 CASE WHEN b.type_desc = ''CLUSTERED'' THEN ''Consider Carefully'' ELSE ''May remove'' END Consideration
 , t.name TableName
 , b.type_desc TypeDesc
 , b.name IndexName
 , a.user_updates Updates
 , a.last_user_scan
 , a.last_user_seek
 , SUM(aa.page_count) Pages
 FROM sys.dm_db_index_usage_stats as a
 JOIN sys.indexes AS b ON a.object_id = b.object_id AND a.index_id = b.index_id
 LEFT OUTER JOIN sys.tables AS t ON b.[object_id] = t.[object_id]
 LEFT OUTER JOIN INFORMATION_SCHEMA.TABLES isc ON isc.TABLE_NAME = t.name
 LEFT OUTER JOIN sys.dm_db_index_physical_stats (@db_id,@tab_id,NULL, NULL, NULL) AS aa ON aa.object_id = a.object_id
 WHERE
 (DATEDIFF(DAY,a.last_user_scan,GETDATE()) > @DaysAgo AND DATEDIFF(DAY,a.last_user_seek,GETDATE()) > @DaysAgo)
 AND t.name NOT LIKE ''sys%''
 GROUP BY t.name, b.type_desc, b.name, a.user_updates, a.last_user_scan, a.last_user_seek
 HAVING SUM(aa.page_count) > 500
 ORDER BY Pages DESC
 '
 INSERT INTO @NeverUsedTable
 EXEC sp_executesql @SQL
 --PRINT @SQL

 /*---------------------------------------Shows tables without primary key. Heaps---------------------------------------*/
 SET @SQL = '
 USE ['+@Database +']
 SELECT '''+@Database +''', SCHEMA_NAME(o.schema_id) AS [schema],object_name(i.object_id ) AS [table],p.rows,user_seeks,user_scans,user_lookups,user_updates,last_user_seek,last_user_scan,last_user_lookup
 FROM sys.indexes i
 INNER JOIN sys.objects o ON i.object_id = o.object_id
 INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
 LEFT OUTER JOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id
 WHERE i.type_desc = ''HEAP''
 AND SCHEMA_NAME(o.schema_id) NOT LIKE ''sys''
 AND rows > 500
 ORDER BY rows desc
 '
 INSERT INTO @HeapTable
 EXEC sp_executesql @SQL
 --PRINT @SQL

SET @i_Count = @i_Count + 1
END
SELECT 'MissingIndex' [ ] , * FROM @MissingIndex ORDER BY SeekImpact DESC
SELECT 'HeapTables' [ ] , * FROM @HeapTable
SELECT 'NeverUsedIndexes' [ ] , * FROM @NeverUsedTable

If there is demand for it I can make this a management script that can create/drop indexes or even the primary key stuff. Let me know if someone is interested.

Adrian

Recent Posts

See All
Where did my [sys].[sp_dboption] go?

Well in SQL Server 2012, to be quite blunt, [sys].[sp_dboption] has been deprecated, dumped, canned or if  you prefer, sent to Zim. Now isn’t that just dandy..because you will only realise this only w

 
 
 
SQL: what is this OPTION(OPTIMIZE FOR UNKNOWN)?

If you are into caching and reuse of existing query execution plans, which no doubt excites you as much as it does me, you probably use OPTION(OPTIMIZE FOR UNKNOWN) in most of your queries. If, howeve

 
 
 

Comments


Address

New Zealand - Auckland, Wellington, Christchurch
Australia - Brisbane, Perth
South Africa - Johannesburg, Cape Town
USA - Los Angeles

©2025 by SQLDBA.ORG Ltd

Contact

0800 000 174 (NZ)

+64 4 888 1683 (International)

  • LinkedIn
NZ_Service_Logo_NZ_Grown_RGB.png

Proudly 100% NZ owned and operated

bottom of page