DIAB icon   DBA In A Box
we save you money

 
  
SQL server database monitoring, troubleshooting, and analyzing software with tools designed to scan, alert, and investigate SQL server failures, performance, SOX compliance, replication failures and much more.
Home
About Us
Screen Shots
Contact Us
Downloads

SQL Scripts
Useful scripts
Auto Manage Indexes
Database Mirroring
SQL Performance
Check Security

SQL Server 2005 How to Auto ReIndex / Defragment your Database Tables.

 The objects below can be created in a database of your choosing.  I personally use a database called AdminData it is a catch all for all things related to database administration. Here is a summary of what you're going to create and how you will use it.

1.  Create a new table called TableStats to store statistical data about the indexes being optimized. It is used to determine what indexes need to be rebuilt and record the last time the index was optimized.

2.  Create the stored procedure Acme_Database_Optimize which will call all the other procedures and control how database optimizing will occur.

3. Create the stored procedure ACME_BuildFragList which will insert into TableStats information about the tables.

4. Create the stored procedure ACME_DeFragTables which will read the TableStats records and rebuild the indexes.

5. Schedule to call Acme_Database_Optimize.

Let's get started: more information will be given as we go. First change references for ACME to something of your choosing maybe your company name.

Create the Table: The table below will be used to record statistical data from the job runs. This data is retrieved from the tables sys.dm_db_index_physical_stats, sysindexes, and sys.objects

 
USE [AdminData]
CREATE TABLE [dbo].[TableStats](
                [TblStatsID] [int] IDENTITY(1,1) NOT NULL,
                [Database_Name] [varchar](50) NOT NULL,
                [Table_Name] [varchar](100) NOT NULL,
                [Index_Name] [varchar](100) NOT NULL,
                [Index_ID] [int] NOT NULL,
                [Partition_Number] [int] NULL,
                [Database_ID] [int] NOT NULL,
                [Object_ID] [int] NOT NULL,
                [Load_Time] [smalldatetime] NULL,
                [DeFrag_Time] [smalldatetime] NULL,
                [Index_Type_Desc] [varchar](25) NULL,
                [Index_Depth] [int] NULL,
                [Index_Level] [int] NULL,
                [AVG_Fragmentation_In_Percent] [numeric](18, 12) NULL,
                [Fragment_Count] [int] NULL,
                [AVG_Fragment_Size_In_Pages] [numeric](18, 12) NULL,
                [Page_Count] [int] NULL,
                [AVG_Page_Space_Used_In_Percent] [numeric](18, 12) NULL,
                [Record_Count] [bigint] NULL,
                [MIN_Record_Size_In_Bytes] [int] NULL,
                [MAX_Record_Size_In_Bytes] [int] NULL,
                [AVG_Record_Size_In_Bytes] [int] NULL,
                [schemaID] [int] NULL,
                [Owner] [varchar](25) NULL,
 CONSTRAINT [PK_TableStats] PRIMARY KEY CLUSTERED
(
                [TblStatsID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

 GO

Acme_Database_Optimize: This is the main controlling job and is the only job called from the schedule. It has a number of variables is passes to the other jobs to control how indexing will take place. I will list them here.

1.     Fragmax: Set this to a percentage  that when greater than will execute the reindex of the table. 10 is a good choice.  

2.      FragType:  Lock, NoLock , or DEFAULT  use NoLock  for daytime run it will execute the reorganize option, Lock does a complete rebuild, default can use either and will be based on fragmentation > 30

3.      FastFlag:  Y or N Use for faster processing note record counts and other columns will not be collected this controls the Limited / sampled option of the dm_db_index_physical_stats function.

4.      DatabaseList: List of databases to be reindexed

5.      UpdateUsage: Y or N Use for new databases and use sparingly. Executes dbcc updateusage. Normally does not need to be ran

6.      UpdateStatistics: Y or N Use to update statistics as a last step. Executes sp_updatestats.

 USE [AdminData]
GO
/****** Object:  StoredProcedure [dbo].[Acme_Database_Optimize]    Script Date: 02/29/2012 09:11:56 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER procedure [dbo].[Acme_Database_Optimize]
@Fragmax as int,
@FragType as varchar(7), -- 'Lock','NoLock' or 'DEFAULT' use NoLock for daytime run, Lock does reindex, default can use either based on fragmentation > 30
@FastFlag char(1), -- 'Y' or 'N' Use for faster processing note record couts and other columns will not be collected
@DatabaseList as nvarchar(2000),    -- List of databases
@UpdateUsage char(1),    -- 'Y' or 'N' Use for new databases and use sparingly. Executes dbcc updateusage. Normally will only be ran once.
@UpdateStatistics char(1) -- 'Y' or 'N' Use to update statistics as a last step. Executes sp_updatestats.
as

DECLARE
@DBName varchar(128),
@strTmp varchar(255),
@SQLString as nvarchar(2000),
@ParmDefinition nvarchar(2000)

if @Fragmax = '' set @Fragmax = 10
If @FragType = '' set @FragType = 'NoLock'
If @FastFlag = '' set @FastFlag = 'Y'
If @UpdateUsage = '' set @UpdateUsage = 'N'


If @FragType <> 'NoLock' and @FragType <> 'Lock' and @FragType <> 'Default'
    BEGIN
        print 'Fragtype is not valid program will end'
        GOTO ENDIT
    END

set @strTmp = 'Max Frag Percent set at ' + cast(@Fragmax as varchar(10)) + '%'
PRINT @strTmp

set @databaselist = '(''' + replace(@databaselist,',',''',''') + ''')' -- Replace commas with quote comma quote

set @SQLString = N'select @DBNameOUT = min(Name) from master.dbo.sysdatabases where name in' + @databaselist;    -- create sql string
set @ParmDefinition = N'@DBNameOUT varchar(128) OUTPUT';                                                        -- Create Output parameter
exec sp_executesql @SQLString, @ParmDefinition, @DBNameOut=@DBName OUTPUT;                                        -- Execute SQL

WHILE @DBNAme IS NOT NULL BEGIN
    PRINT '----------------------------------------------------------------------'
   

    SET @strTmp = 'Start processing ' + @DBName + ' At ' + cast(getdate() as varchar(30))
    PRINT @strTmp
    if @UpdateUsage = 'Y'
        BEGIN
        SET @strTmp = '***** Executing DBCC UPDATEUSAGE for ' + @DBName + ' At ' + cast(getdate() as varchar(30))
        PRINT @strTmp
        DBCC UPDATEUSAGE(@DBName)
        END
    ELSE
        BEGIN
        SET @strTmp = '***** DBCC UPDATEUSAGE WILL NOT BE EXECUTED'
        PRINT @strTmp
        END
   
    SET @strTmp = '***** Update Fragmentation Levels for ' + @DBName + ' At ' + cast(getdate() as varchar(30))
    PRINT @strTmp
    exec Acme_BuildFragList @DBName, @FastFlag -- Job updated DBCCStatistics with current Fragmentation statistical data

    SET @strTmp = '***** Reindexing Tables for ' + @DBName + ' At ' + cast(getdate() as varchar(30))
    PRINT @strTmp
    exec Acme_DeFragTables @fragMax, @DBName, @FragType  -- Execute dbcc dbreindex on Tables where the frag% > @fragMax
    If @UpdateStatistics = 'Y'
        BEGIN
        SET @strTmp = '***** Updating statistics for ' + @DBName + ' At ' + cast(getdate() as varchar(30))
        PRINT @strTmp
        exec('use [' + @DBName + '] exec sp_updatestats @Resample = ''resample''') -- Update statistics on all Tables
        END
    ELSE
        BEGIN
        SET @strTmp = '***** Statistics will not be updated '
        PRINT @strTmp
        END
       
    SET @strTmp = 'End processing ' + @DBName + ' At ' + cast(getdate() as varchar(30))
    PRINT @strTmp

    set @SQLString = N'select @DBNameOUT = min(Name) from master.dbo.sysdatabases where name in' + @databaselist + 'and [Name] > @DBNameOut';
    exec sp_executesql @SQLString, @ParmDefinition, @DBNameOut=@DBName OUTPUT;                                        -- Get Next Database

    END

ENDIT:


ACME_BuildFragList: This job will execute the sys.dm_db_index_physical_stats function for the database being passed on all indexes and add new records to the TableStats table in AdminData. The FastFlag will determine how much data is recorded and how long this job will execute. Setting the fastflag to true will record less data but enough to efficiently keep you indexes defragmented. Set to false if you like more detailed data to be recorded and don't mind the speed of execution.  

USE [AdminData]
GO

ALTER PROCEDURE [dbo].[Acme_BuildFragList](@DBName varchar(128), @FastFlag char(1))
AS

Declare
@ScanType nvarchar(20),
@db_id int,
@SQL nvarchar(4000),
@ReturnCode int

If @FastFlag = 'Y'
    BEGIN
    set @ScanType = 'LIMITED'
    END
ELSE
    BEGIN
    set @ScanType = 'SAMPLED'
    END
SET @db_id = DB_ID(@DBName);

SET @SQL = N'use ' + @DBName + '; insert into AdminData.dbo.TableStats
    (Database_Name, Table_Name, Index_Name,Index_ID,Partition_Number,Database_ID,
ObJect_ID,Load_Time,Index_Type_Desc,Index_Depth,Index_Level,AVG_Fragmentation_IN_Percent,
Fragment_Count,AVG_Fragment_Size_In_Pages,Page_Count,AVG_Page_Space_Used_In_Percent,
Record_Count,MIN_Record_Size_In_Bytes,MAX_Record_Size_In_Bytes,AVG_Record_Size_In_Bytes, schemaID, Owner)
SELECT ''' + @DBName + ''', object_name(IPS.object_Id), isnull(IDX.name,''HEAP''),  index_id, Partition_number, Database_ID, IPS.Object_ID, getdate(),
    Index_Type_Desc, Index_Depth,
    Index_Level,
    cast(avg_fragmentation_In_Percent as numeric(18,12)), Fragment_Count,
    cast(AVG_Fragment_Size_IN_Pages as numeric(18,12)),
    Page_Count,
    cast(AVG_Page_Space_Used_IN_Percent as numeric(18,12)), Record_Count,
    MIN_record_size_in_bytes, MAX_record_size_in_bytes, AVG_record_size_in_bytes,Ob.schema_id, schema_name(Ob.schema_id)
    FROM sys.dm_db_index_physical_stats(' + cast(@db_id as nvarchar(10)) + ', NULL, NULL, NULL,''' + @ScanType  + ''') IPS,
    sysindexes IDX,
    sys.objects Ob
    where IDX.ID = IPS.object_ID
    and IDX.indid = IPS.index_ID
    and Ob.object_id = IPS.object_ID'

exec dbo.sp_executesql @statement = @SQL
set @ReturnCode = @@ERROR
return @ReturnCode

Acme_DeFragTables: This job does the actual defraging of the indexes. It uses a few options to determine how this is accomplished.

 1.      MaxFrag:         Set to a percentage that when fragmentation is greater than will defrag the index.

2.      DBName:         Database to be defragmented.

3.      FragType:        If set to Lock it will rebuild the index. if set to nolock will reorganize the index. If set to default will reorganize if less that 30% fragmented and will rebuild if > 30% fragmented. This is based on my own personal testing on which is faster.

 USE [AdminData]
GO
/****** Object:  StoredProcedure [dbo].[Acme_DeFragTables]    Script Date: 02/29/2012 09:14:39 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[Acme_DeFragTables]
(@MaxFrag int = 10, @DBName varchar(128) = NULL, @FragType varchar(7))
as
DECLARE @TblStatsID int,
    @Database_Name varchar(50),
    @Owner varchar(25),
    @Table_Name varchar(100),
    @Index_Name varchar(100),
    @Object_ID int,
    @Index_ID int,
    @Partition_Number int,
    @Partition_Count int,
    @AVG_Fragmentation_IN_Percent numeric(18,12),
    @SQL nvarchar(2000),
       @ParmDefinition nvarchar(2000),
    @MSG varchar(200),
    @ERR int

DECLARE FragmentedTables CURSOR FAST_FORWARD
    FOR
    SELECT     TblStatsID,
        Database_Name,
        Owner,
        Table_Name,
        Index_Name,
        [object_ID],
        index_ID,
        Partition_Number,
        AVG_Fragmentation_IN_Percent
        from admindata.dbo.TableStats  
        where     AVG_Fragmentation_In_Percent > @MaxFrag
        AND Database_Name = @DBName
        AND    Load_Time > GetDate() -.5
        AND    DeFrag_Time IS NULL
        order by AVG_Fragmentation_In_Percent desc
Set @ERR = 0
OPEN FragmentedTables

FETCH NEXT
FROM FragmentedTables
INTO @TblStatsID, @Database_Name, @Owner, @Table_Name, @Index_Name, @object_ID, @index_ID, @Partition_Number, @AVG_Fragmentation_IN_Percent  


WHILE @@FETCH_STATUS = 0 BEGIN

    -- Determine if partitioned
    set @SQL = N'select @Partition_CountOUT = Count(*)
            From [' + @Database_Name + '].sys.partitions
            Where object_id = ' + cast(@Object_ID as varchar(20)) + ' And index_id = ' + cast(@Index_ID as varchar(20)) + ';'
    set @ParmDefinition = N'@Partition_countOUT int OUTPUT';
    exec dbo.sp_executesql @SQL, @ParmDefinition, @Partition_CountOUT=@Partition_Count OUTPUT;

    IF @FragType = 'Lock'
        BEGIN    -- REBUILD INDEX
            if @Partition_Count = 1  
                BEGIN -- non partitioned index
                set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                REBUILD WITH(FILLFACTOR = 90, SORT_IN_TEMPDB = ON, STATISTICS_NORECOMPUTE = ON);'
                set @MSG = 'Rebuilding Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name
                END
            else
                BEGIN -- partitioned index
                set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                REBUILD Partition = ' + cast(@Partition_Number as varchar(20)) + ';'
                set @MSG = 'Rebuilding Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name + ' Partition: ' + cast(@Partition_Number as varchar(20))
                END
        END    

    IF @FragType = 'NoLock' -- REORGANIZE INDEX
        BEGIN
            IF  @Partition_Count = 1
                BEGIN -- non partitioned index
                set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                REORGANIZE;'
                set @MSG = 'Reorganizing Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name
                END
            ELSE
                BEGIN -- partitioned index
                set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                REORGANIZE Partition = ' + cast(@Partition_Number as varchar(20)) + ';'
                set @MSG = 'Reorganizing Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name + ' Partition: ' + cast(@Partition_Number as varchar(20))
                END
        END
    
    IF @FragType = 'DEFAULT'  -- Default based on Fragmentation Percent% > 30
        BEGIN
            IF @AVG_Fragmentation_IN_Percent > 30
                BEGIN    -- Rebuild Index
                    if @Partition_Count = 1  
                        BEGIN -- non partitioned index
                        set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                        REBUILD WITH(FILLFACTOR = 90, SORT_IN_TEMPDB = ON, STATISTICS_NORECOMPUTE = ON);'
                        set @MSG = 'Rebuilding Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name
                        END
                    else
                        BEGIN -- partitioned index
                        set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                        REBUILD Partition = ' + cast(@Partition_Number as varchar(20)) + ';'
                        set @MSG = 'Rebuilding Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name + ' Partition: ' + cast(@Partition_Number as varchar(20))
                        END
                END    -- END Rebuild Index
            ELSE
                BEGIN -- Reorganize Index
                    IF  @Partition_Count = 1
                        BEGIN -- non partitioned index
                        set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                        REORGANIZE;'
                        set @MSG = 'Reorganizing Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name
                        END
                    ELSE
                        BEGIN -- partitioned index
                        set @SQL = N'alter index [' + @Index_Name + '] on [' + @Database_Name + '].' + @Owner + '.' + @Table_Name + '
                        REORGANIZE Partition = ' + cast(@Partition_Number as varchar(20)) + ';'
                        set @MSG = 'Reorganizing Index: ' + @Table_Name + '.' + @Owner + '.' + @Index_Name + ' Partition: ' + cast(@Partition_Number as varchar(20))
                        END
                END  -- End Reorganize Index
        END -- END DEFAULT    

    print @MSG
    BEGIN TRY
        exec dbo.sp_executesql @statement = @SQL
    END TRY
    BEGIN CATCH
        GOTO ERRORS
    END CATCH
    UPDATE AdminData.dbo.TableStats
        set Defrag_Time = getdate()
        where TblStatsID = @TblStatsID
    Print 'Status = OK'
    Print ' '
    
    FETCH NEXT
    FROM FragmentedTables
    INTO @TblStatsID, @Database_Name, @Owner, @Table_Name, @Index_Name, @object_ID, @index_ID, @Partition_Number, @AVG_Fragmentation_IN_Percent  

END -- While
GOTO ENDIT
ERRORS:
          Print 'Program failed'
        print 'ErrorNumber: ' + Cast(ERROR_NUMBER() as varchar(50))
        print 'ERROR_SEVERITY:' + Cast(ERROR_SEVERITY() as varchar(100))
        print 'ErrorState:' + cast(ERROR_STATE() as varchar(100))
        print 'ErrorProcedure:' + cast(ERROR_PROCEDURE() as varchar(200))
        print 'ErrorLine:' + cast(ERROR_LINE() as varchar(200))
        Print 'ErrorMessage:' + cast(ERROR_MESSAGE() as varchar(2000))
        set @ERR = ERROR_NUMBER()
ENDIT:
CLOSE FragmentedTables
DEALLOCATE FragmentedTables

RETURN @ERR    

Script to call Acme_Database_Optimize
USE [AdminData]
GO
DECLARE    @return_value int
EXEC        @return_value = [dbo].[Acme_Database_Optimize]
        @Fragmax = 10,
        @FragType = N'DEFAULT',    -- Lock = dbcc dbreindex, NoLock = dbcc indexdefrag, DEFAULT = > 30 index else reorganize    
        @FastFlag = N'N',        -- DBCC SHOWCONTIG with Fast option Less data recorded but faster.
        @DatabaseList = N'HR,Sales' ,     -- Comma separated list of databases to be re-indexed
        @UpdateUsage = N'N',        -- will execute DBCC UPDATEUSAGE(@DBName)
        @UpdateStatistics = N'Y'    -- will execute  exec sp_updatestats


PRINT 'Acme_Database_Optimize has completed. Return Code = ' + cast(@return_value as varchar(20))

Home | About Us | Screen Shots | Contact Us | Downloads