Many DBA's that I know will use the Maintenance Plan Wizard to set up the reindexing for their databases. In my opinion, this is a mistake.
When you use a maintenance plan to reindex your database, you are rebuilding every index no matter if it needs it or not. This is like sending you car for a tune up once a week. It is excessive and can cause more harm than good.
For example, lets say we have a large database that is scheduled to be reindexed each night at 1:00 AM. On average it takes 7 hours for the job to complete. That means you have a 7 hour window with random time outs and delays (or so it will appear to your users) that are caused by your reindexing job locking tables and rebuilding indexes that may or may not need to be rebuilt. (Yes, in SQL 2005 Enterprise Edition there is online reindexing but lets assume you don't have the Enterprise Edition).
Wouldn't it make more sense to to just rebuild the indexes that are > x% fragmented? Also, if you only have to reindex 25% of the indexes each night instead of 100%, the job should run a lot faster.
So, we need an intelligent reindex stored procedure that will only rebuild those indexes that need to be rebuilt and that keeps track of what indexes are being worked on and how long it takes.
Well, here is a good start to that stored procedure. Please feel free to use and modify at will. I save results to a table and it also looks at each index's usage to see if it is being used or not.
I take no responsibility for the use of the below code. Please use at your own risk. (I should not have to say this but just in case: You should never just use posted code without reviewing it yourself and testing the crap out of it.)This is for SQL 2005, I will post one for SQL 2000 later.
ALTER PROCEDURE [dbo].[ap_RebuildIndexes_2005] (
@maxfrag float = 20.0
, @MinUsage INT = 15
, @databasename varchar(255))
AS
/*
Created by Lara Rubbelke - 7/29/2007
Modified by
9/15/2007 - Jim Youmans - Added logic to write results to a table into a user database. Hardcoded database name, but could easily be passed as a parameter if need be.
8/6/2008 - Jim Youmans - Added the ability to identify and ignore unused indexes. @MinUsage is the sum of User_Seeks + User_Scans + User_Lookups from the DM view sys.dm_db_index_usage_stats
8/8/2008 - Jim Youmans - Added logic to ignore disabled indexes.
ap_RebuildIndexes_2005 is a process which will assess the level of fragmentation of all indexes in a database and reorganize those indexes which fall outside the specified parameters.
ap_RebuildIndexes_2005 accepts the following parameters:
@maxfrag The maximum level of acceptable fragmentation
@maxdensity The minimum level of acceptable density -- not in use
@databasename The database to assess and reorganize
This procedure assumes that a partitioned index will not be processed ONLINE.
*/
SET NOCOUNT ON;
-- Make sure database is set to compatibility level 90 (SQL 2005)
IF NOT EXISTS(select * from master.sys.databases where name = @DatabaseName and compatibility_level=90)
BEGIN
PRINT 'This stored procedure can only run on SQL 2005 databases with the compatibility_level set to 90'
RETURN 0
END
DECLARE
@schemaname sysname
, @objectname sysname
, @indexname sysname
, @indexid int
, @currentfrag float
, @currentdensity float
, @partitionnum varchar(10)
, @partitioncount bigint
, @indextype varchar(18)
, @updatecommand varchar(max)
, @command varchar(max)
, @ID int
, @IndexUsage int
, @SQLCommand varchar(1000)
-- ensure the temporary table does not exist
IF (SELECT object_id('tempdb..#work_to_do')) IS NOT NULL
DROP TABLE #work_to_do;
CREATE TABLE #work_to_do(
IndexID int not null
, IndexName varchar(255) null
, TableName varchar(255) null
, Tableid int not null
, SchemaName varchar(255) null
, IndexType varchar(18) not null
, PartitionNumber varchar(18) not null
, PartitionCount int null
, CurrentDensity float not null
, CurrentFragmentation float not null
, IndexUsage INT
);
-- 8/8/2008 Jim Youmans
-- Since this sp can be run from a centeral database against other databases on
-- the same server, we need to make sure we are checking the index information
-- for the target server and not source.
SET @SQLCommand = 'SELECT * INTO ##tempIndex FROM ' + @DatabaseName + '.sys.indexes'
EXEC(@SQLCommand)
-- This will only pick up indexes which are >= the @MaxFrag level
INSERT INTO #work_to_do(
IndexID, Tableid, IndexType, PartitionNumber, CurrentDensity, CurrentFragmentation
)
SELECT
fi.index_id
, fi.object_id
, fi.index_type_desc AS IndexType
, cast(fi.partition_number as varchar(10)) AS PartitionNumber
, fi.avg_page_space_used_in_percent AS CurrentDensity
, fi.avg_fragmentation_in_percent AS CurrentFragmentation
FROM sys.dm_db_index_physical_stats(db_id(@databasename), NULL, NULL, NULL, 'SAMPLED') AS fi
LEFT JOIN ##tempIndex i on fi.object_id = i.object_id AND fi.index_id = i.index_id
WHERE fi.avg_fragmentation_in_percent >= @maxfrag
AND fi.page_count > 1000
AND fi.index_id > 0
AND i.is_disabled = 0
DROP TABLE ##tempIndex
--Assign the index names, schema names, table names and partition counts
SET @updatecommand = 'UPDATE #work_to_do SET TableName = o.name, SchemaName = s.name, IndexName = i.Name
,PartitionCount = (SELECT COUNT(*) pcount
FROM '
+ QUOTENAME(@databasename) + '.sys.Partitions p
where p.Object_id = w.Tableid
AND p.index_id = w.Indexid)
FROM '
+ QUOTENAME(@databasename) + '.sys.objects o INNER JOIN '
+ QUOTENAME(@databasename) + '.sys.schemas s ON o.schema_id = s.schema_id
INNER JOIN #work_to_do w ON o.object_id = w.tableid INNER JOIN '
+ QUOTENAME(@databasename) + '.sys.indexes i ON w.tableid = i.object_id and w.indexid = i.index_id';
EXEC(@updatecommand)
-- Look at the indexes selected to see how much they are being used.
UPDATE #Work_To_Do
SET IndexUsage = (us.User_Seeks + us.User_Scans + us.User_Lookups)
FROM sys.dm_db_index_usage_stats us
RIGHT JOIN #work_to_do wtd ON us.index_id = wtd.IndexID and us.object_id = wtd.tableID
WHERE us.Database_ID = DB_ID(@DatabaseName)
--Declare the cursor for the list of tables, indexes and partitions to be processed.
--If the index is a clustered index, rebuild all of the nonclustered indexes for the table.
--If we are rebuilding the clustered indexes for a table, we can exclude the
--nonclustered and specify ALL instead on the table
DECLARE rebuildindex CURSOR FOR
SELECT QUOTENAME(IndexName) AS IndexName
, TableName
, SchemaName
, IndexType
, PartitionNumber
, PartitionCount
, CurrentDensity
, CurrentFragmentation
, IndexUsage
FROM #work_to_do i
ORDER BY TableName, IndexID;
-- Open the cursor.
OPEN rebuildindex;
-- Loop through the tables, indexes and partitions.
FETCH NEXT
FROM rebuildindex
INTO @indexname, @objectname, @schemaname, @indextype, @partitionnum, @partitioncount, @currentdensity, @currentfrag, @IndexUsage;
WHILE @@FETCH_STATUS = 0
BEGIN
-- jdy: 9/15/2007 - Changed this to only do rebuild and not reorganize. Also got rid of hard coded number
-- and used passed in parameter. Also write a log to a logging table to keep track of whats done,
-- when, and how long it took.
SELECT @command = 'ALTER INDEX ' + @indexname + ' ON ' + QUOTENAME(@databasename) +'.' + QUOTENAME(@schemaname) + '.' + QUOTENAME(@objectname) + ' REBUILD';
IF @partitioncount > 1
SELECT @command = @command + ' PARTITION=' + @partitionnum;
INSERT dbo.IndexLog(Databasename, TableName, SchemaName, IndexName, CurrentDensity, CurrentFragmentation,IndexUsage)
Values(@DatabaseName, @objectname, @schemaname, @indexname, @currentdensity, @currentfrag,@IndexUsage)
/*
-- Table structure for the IndexLog table in case you need to create it.
CREATE TABLE [dbo].[IndexLog](
[ID] [int] IDENTITY(1,1) NOT NULL,
[DatabaseName] [varchar](255) NULL,
[TableName] [varchar](255) NULL,
[SchemaName] [varchar](255) NULL,
[IndexName] [varchar](255) NULL,
[CurrentDensity] [float] NULL,
[CurrentFragmentation] [float] NULL,
[StartTime] [datetime] NULL CONSTRAINT [DF_IndexLog_StartTime] DEFAULT (getdate()),
[EndTime] [datetime] NULL,
[IndexUsage] [int] NULL,
[Notes] [varchar](50) NULL
CONSTRAINT [PK_IndexLog] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
*/
-- get the ID number for the newly inserted row.
SET @ID = @@Identity;
-- If index is not used much then make note of it but do not reindex it
IF @IndexUsage < @MinUsage BEGIN -- Update Notes but do not reindex UPDATE dbo.IndexLog SET Notes = 'Index may not be being used' WHERE ID = @ID; END ELSE BEGIN -- execute the reindex command EXEC (@command); END -- update endtime for log table for this command UPDATE dbo.IndexLog SET EndTime = GETDATE() WHERE ID = @ID; SET @ID = NULL; FETCH NEXT FROM rebuildindex INTO @indexname, @objectname, @schemaname, @indextype, @partitionnum, @partitioncount, @currentdensity, @currentfrag, @indexusage; END; -- Close and deallocate the cursor. CLOSE rebuildindex; DEALLOCATE rebuildindex; DROP TABLE #Work_To_DO; SET NOCOUNT OFF;