Working with DBCC INDEXDEFRAG

by Bill Wunder

SQL Server fragmentation comes in two basic flavors: Physical and Logical. Physical fragmentation - or more traditionally external fragmentation -  happens at the file allocation level. Logical fragmentation - also know as Internal fragmentation - occurs primarily at the table and index level.

Physical fragmentation can become problematic on a server with many databases where databases have been left to auto grow over time or when a disk or array has been used as both a file server/store and as a SQL Server physical device container. If you have slow performance that you are unable to explain by other means, it may be wise to take a good hard look at the physical fragmentation on a server. See my recent article Benchmarking Techniques using T-SQL Part 2- Performance Counters via sysperfinfo for some tips on using the ::fn_virtualfilestats() function to identify slowness at the file level. But even before you attempt any sort of physical fragmentation cleanup, be sure that there are no logical fragmentation issues to resolve. It is a generally accepted best practice to regularly remove logical fragmentation. 

Generally to identify and repair physical fragmentation a disk utility is used. Depending upon the hardware and the disk utility used maintaining a SQL server to reduce physical fragmentation can be problematic and highly specialized to the environment. Sometimes an approach such as backup, drop, then restore of a database can alleviate fragmentation, sometimes stopping the SQL Server service and allowing the disk utility to do it's thing can help, and high end disk systems may manage their own physical fragmentation with no user intervention required or even possible. A best practices approach to physical fragmentation is to create your databases at the size the need to be, if you do need to grow them do it once to the expected required size rather than letting them creep up in size over time, and do not allow file server storage and database physical devices to interleave on the same direct attached disk sets.    

Logical fragmentation is for the most part the result of rows being inserted into a non-monotonically increasing clustering index, deletes, and updates. Under the covers SQL Server handles updates in two basic ways. If storage allocation requirements can remain satisfied the update will occur in place and fragmentation will not occur or at least will be limited to a few bytes as in the case where a varchar column is modified to a string with fewer characters. If storage allocation requirements cannot support the update "in place" the update will actually be processed as a delete and an insert. If you want to spend the time looking you can actually see this behavior happen in the transaction log using the undocumented DBCC LOG() utility. Fortunately, there is a good demonstration of using DBCC LOG() and the behavior of two types of updates in the sswug.org article: "Update Methods Used in SQL Server 7.0" (sorry, I don't know who wrote it so I can't tell you).  If an update is processed as a delete/insert logical fragmentation will occur. Tables clustered on anything other than a monotonically increasing column are likely to see a lower scan density value - approaching 0% - and a higher logical scan fragmentation value over time while tables clustered on an identity are likely to see a very high scan density value - approaching 100% - and a relatively low logical scan fragmentation over time and may never require maintenance to assure consistent and satisfactory query performance.

The Basics of Logical Fragmentation Maintenance

Taken straight from Books Online:

Logical Scan Fragmentation - Percentage of out-of-order pages returned from scanning the leaf pages of an index. This number is not relevant to heaps and text indexes. An out of order page is one for which the next page indicated in an IAM is a different page than the page pointed to by the next page pointer in the leaf page.

Scan Density - Best count is the ideal number of extent changes if everything is contiguously linked. Actual count is the actual number of extent changes. The number in scan density is 100 if everything is contiguous; if it is less than 100, some fragmentation exists. Scan density is a percentage.

my note: (IAM = Index Allocation Map)

You can measure logical fragmentation using the DBCC SHOWCONTIG utility. Be aware that DBCC SHOWCONTIG will hold locks on a table while it scans through the pages and extents to measure the fragmentation level of the table. Consequently, for large tables in busy environments even the action of checking for fragmentation can cause blocking durations that are unacceptable. It is possible to measure fragmentation one index at a time to reduce the time that locks are held or to use the WITH FAST option to perform an abbreviated scan. In my experience the clustered index of a table is likely to be the one that takes the longest to scan and to be the most important to monitor and maintain in terms of fragmentation. Because of this I find the WITH FAST option - even though it does not provide logical page density results - to be the most useful because it is the least disruptive method of checking for logical fragmentation. Another useful DBCC SHOWCONTIG option is the ability to return the results of the scan in a tabular format - WITH TABLERESULTS - making it very easy to capture the results to a table and thereby track fragmentation over time. This can be useful in determining the necessary frequency of maintenance and can even help identify table indexing designs that may warrant review and revision.

If you run DBCC INDEXDEFRAG on a large logically fragmented table you can expect your transaction log file size to explode during the process. If you use DBCC DBREINDEX or you use a scripted approach to drop and recreate the indexes on a large table - even if it is hardly fragmented - you can expect your table to be unavailable for other use during the process. If the table is small none of the de-fragmentation operations are likely to be a huge concern but then again if the table is small fragmentation is not likely to affect query performance on that table to begin. It's also worth noting that if a query is conditioned on the clustering index of a table or is looking for only a small number of rows the performance impact of fragmentation is much less than if the query is considering a large number of rows, for example from an index access path with low selectivity or a join operation that returns a set of rows. It's quite possible that some applications will perform satisfactorily for extended periods of time with no attention to removing logical fragmentation. However, even when performance is not impacted fragmentation can result in an inordinately large number of pages being consumed by a table relative to the true date set size.    

In the case of DBCC INDEXDEFRAG even if the table is huge the log growth problems during the maintenance de-fragmentation operation are manifest much more severely as the table is more fragmented. If the maintenance operation doesn't have to move data it doesn't have to log the move. It's that simple, but it is also more complicated than that (man does that sound political). Index defrag cannot resolve problems where different indexes are interleaved. For example if one index allocates the next extent in the IAM sequence, then another table's index comes along to grab the next, then the first table gets the next extent you have a situation where the first index will always have to hop over the extent used by another table when scanning the first tables index. DBCC INDEXDEFRAG cannot resolve this problem. Dropping and recreating the index can. I believe you can identify this problem if you observe that the actual count component of the scan density does not closely approximate the best count component after a DBCC INDEXDEFRAG.

For DBCC DBREINDEX or dropping and recreating indexes the impact of the maintenance operation is likely to have about the same impact regardless of the fragmentation state of the table. The time to run a DBCC DBREINDEX or drop the clustered index of a table seems to vary only slightly depending on the level of  fragmentation of the clustered index. Of course if it's not fragmented why bother to run the fragmentation maintenance operation?

The Plan for Managing Logical Fragmentation

The Database Maintenance Planner wizard is one possible tool you can use to maintain indexes. The wizard is most assuredly a better-than-nothing tool most suitable for a DBA-less database environment. Only DBCC DBREINDEX will be used by the wizard and it supports only a database level granularity: all or none. For me it doesn't provide the necessary flexibility necessary to assure my data remains available nor does it allow me to monitor the and track fragmentation patterns in the data set.

 Instead I prefer to use a stored procedure that I run from the SQL Agent Scheduler that you can investigate: admin db - index (fragmentation) maintenance


use admin
GO
SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS ON 
GO

if exists (select * from dbo.sysobjects 
           where id = object_id(N'[dbo].[IndexMaintenanceForDB]') 
           and OBJECTPROPERTY(id, N'IsProcedure') = 1)
drop procedure [dbo].[IndexMaintenanceForDB]
GO

CREATE procedure dbo.IndexMaintenanceForDB
	@DatabaseName varchar(128)
	, @NotifyOnFail varchar(100) = '' 
	, @LogUsedThresholdKB int = 4000000 -- 4GB
as
/******************************************************************************
* dbo.indexMaintenanceForDB.PRC
* Bill Wunder	
* Date: 5/24/2002
*
* Outline: Identify which indexes need to be maintained and which maintenance 
*   operation to use. If scan density is less than 90% or logical scan 
*   fragmentation is more than 10% maintain the index. If index has over 10000
*   pages use DBCC INDEXDEFRAG to maintain otherwise use DBCC DBREINDEX. If 
*   table has a clustered index that will be maintained by a DBCC DBREINDEX do
*   not maintain other indexes because they will be rebuilt when the clustered
*   index is reindexed. Pause any active DBCC INDEXDEFRAG if a backup or 
*   restore is running or if log used size has exceeded @LogUsedThresholdKB.
*   Keep all fragmentation study history for trend analysis.
*
* Statistic                   Description 
* Count Pages                 Number of pages in the table or index. 
* Extents Scanned             Number of extents in the table or index. 
* Extent Switches             Number of times the DBCC statement moved from 
*                             one extent to another while it traversed the 
*                             pages of the table or index. 
* Scan Density                [Best Count: Actual Count] Best count is the 
*                             ideal number of extent changes if everything is
*                             contiguously linked. Actual count is the actual
*                             number of extent changes. The number in scan
*                             density is 100 if everything is contiguous; if 
*                             it is less than 100, some fragmentation exists.
*                             Scan density is a percentage. 
* Logical Scan Fragmentation 	Percentage of out-of-order pages returned from 
*                             scanning the leaf pages of an index. This number 
*                             is not relevant to heaps and text indexes. An 
*                             out of order page is one for which the next page
*                             indicated in an IAM is a different page than the 
*                             page pointed to by the next page pointer in the 
*                             leaf page. 
*!---->following not included in DBCC SHOWCONTIG() WITH FAST mode<-----!
* CountRows                   Number of rows in the table or index.
* Avg. Pages per Extent       Number of pages per extent in the page chain. 
* Extent Scan Fragmentation   Percentage of out-of-order extents in scanning 
*                             the leaf pages of an index. This number is not 
*                             relevant to heaps. An out-of-order extent is one 
*                             for which the extent containing the current page 
*                             for an index is not physically the next extent 
*                             after the extent containing the previous page 
*                             for an index. 
* Avg. Bytes free per page    Average number of free bytes on the pages 
*                             scanned. The higher the number, the less full 
*                             the pages are. Lower numbers are better. This 
*                             number is also affected by row size; a large 
*                             row size can result in a higher number. 
* Avg. Page density (full)    Average page density (as a percentage). This 
*                             value takes into account row size, so it is a 
*                             more accurate indication of how full your pages
*                             are. The higher the percentage, the better. 
*
* status 
*   0 - under consideration
*   1 - defrag
*   2 - defrag in progress
*   3 - defrag complete
*   4 - reindex
*   5 - reindex in progress
*   6 - reindex complete	
*   7 - After defrag showcontig
*   8 - manually populate showcontig
*   9 - eliminated from consideration
*   10 - 19 - historical 
*
* usage:		
    EXECUTE admin.dbo.indexMaintenanceForDB 'admin'
* Notes:		
*			
* Modifications   
* bw 04-25-03 Change showcontig to fast mode and make defrags interruptable 
*             based on log space                        
* bw 04-27-03 Speed up the showcontig and add a duration of showcontig column
* bw 12-22-03 Add logic to reindex if LogicalFrag > 10 in addition to case 
*             where ScanDensity < 90
* bw 03-13-04 Add showcontig to fraglist immediately AFTER maint to see how it 
*             went. Run indexdefrags through called proc and manage log growth
*             try not to run INDEXDEFRAG during backup operation on the server
*******************************************************************************/
declare @SQLStr nvarchar(4000)
 , @ObjectOwner varchar(128)
 , @ObjectName varchar(128)
 , @IndexName varchar(128)
 , @status int
 , @DBCCStartDt datetime
 , @JobCategory varchar(128)
 , @JobName varchar(1024)   
 , @JobDesc varchar(128)
 , @JobCmd varchar(2048)
 , @JobCmd2 varchar(2048)
 , @JobOwner sysname
 , @OutputFile varchar(1024)
 , @rc int
 , @ec int
 , @ErrDesc varchar(1024)
 , @Err varchar(300)

-- Verify requested db does exist, if not then notify and keep moving.
if db_id(@DatabaseName) is null
 begin
  select @Err = 'Database (' + @DatabaseName 
              + ') does not exist.  Resuming processing on next DB.'
  exec sysmon.dbo.safe_sendmail @recipients = @NotifyOnFail 
  return
 end

-- create the temp table once for reuse
CREATE TABLE #fraglist 
 ( ObjectName varchar (255)
 , ObjectId INT
 , IndexName varchar (255)
 , IndexId INT
 , Lvl INT
 , CountPages INT
 , CountRows INT
 , MinRecSize INT
 , MaxRecSize INT
 , AvgRecSize INT
 , ForRecCount INT
 , Extents INT
 , ExtentSwitches INT
 , AvgFreeBytes INT
 , AvgPageDensity INT
 , ScanDensity DECIMAL
 , BestCount INT
 , ActualCount INT
 , LogicalFrag DECIMAL
 , ExtentFrag DECIMAL)

-- Table structure will work with or with out WITH FAST showcontig option
if object_id('admin.dbo.fraglist','U') is null
 CREATE TABLE admin.dbo.fraglist 
  ( DatabaseName varchar (128) NOT NULL
  , ObjectOwner varchar(128) NOT NULL		
  , ObjectName varchar (128) NOT NULL
  , ObjectId int NULL
  , IndexName varchar(128) NOT NULL
  , IndexId int NULL
  , Lvl int NULL
  , CountPages int NULL
  , CountRows int NULL
  , MinRecSize int NULL
  , MaxRecSize int NULL
  , AvgRecSize int NULL
  , ForRecCount int NULL
  , Extents int NULL
  , ExtentSwitches int NULL
  , AvgFreeBytes int NULL
  , AvgPageDensity int NULL
  , ScanDensity decimal NULL
  , BestCount int NULL
  , ActualCount int NULL
  , LogicalFrag decimal NULL
  , ExtentFrag decimal NULL
  , RecCreatedDt datetime NOT NULL
    constraint dft__fraglist__RecCreatedDt DEFAULT (getdate())
  , Status tinyint NULL
    constraint dft__fraglist__Status DEFAULT (0)
  , SecondsToMaintain int	NULL
  , SecondsToShowContig int NULL
  , constraint 
    pkc_fraglist__DbName__ObjOwner__ObjName__IdxName__RecCreatedDt 
    primary key clustered 
     ( DatabaseName
     , ObjectOwner
     , ObjectName
     , IndexName
     , RecCreatedDt) on [PRIMARY]
    ) on [PRIMARY]

-- build a utiltiy proc to handle showcontig population of fraglist
set @SQLStr = 
'Create procedure dbo.GetShowContigForIndex
   @DatabaseName varchar(128)
 , @ObjectOwner varchar(128) = ''dbo''
 , @ObjectName varchar(128) = null
 , @IndexName varchar(128) = null
 , @PlanMaintenance tinyint = 8
as
/******************************************
* see IndexMaintenanceForDB
*
* status 
*  0 - under consideration
*  1 - defrag
*  2 - defrag in progress
*  3 - defrag complete
*  4 - reindex
*  5 - reindex in progress
*  6 - reindex complete	
*  7 - After defrag showcontig       (no-op)
*  8 - manually populate showcontig  (no-op)
*  9 - eliminated from consideration (no-op)
*  10 - 19 - historical 
*******************************************/
declare @SQLStr nvarchar(4000)
 , @ShowContigStartDt datetime

-- table should alrady exist unless call is one off
if object_id(''tempdb.dbo.#fraglist'',''U'') is null
 CREATE TABLE #fraglist 
  ( ObjectName varchar (255)
  , ObjectId INT
  , IndexName varchar (255)
  , IndexId INT
  , Lvl INT
  , CountPages INT
  , CountRows INT
  , MinRecSize INT
  , MaxRecSize INT
  , AvgRecSize INT
  , ForRecCount INT
  , Extents INT
  , ExtentSwitches INT
  , AvgFreeBytes INT
  , AvgPageDensity INT
  , ScanDensity DECIMAL
  , BestCount INT
  , ActualCount INT
  , LogicalFrag DECIMAL
  , ExtentFrag DECIMAL)

set @SQLStr = ''use '' 
            + @DatabaseName 
            + '' DBCC SHOWCONTIG(['' 
            + @ObjectName 
            + ''],['' 
            + @IndexName 
            + '']) WITH TABLERESULTS, FAST''

set @ShowContigStartDt = getdate()

insert #fraglist 
exec sp_executesql @SQLStr

insert admin.dbo.fraglist 
 ( DatabaseName
 , ObjectOwner
 , ObjectName
 , ObjectId
 , IndexName
 , IndexId
 , Lvl
 , CountPages
 , CountRows
 , MinRecSize
 , MaxRecSize
 , AvgRecSize
 , ForRecCount
 , Extents
 , ExtentSwitches
 , AvgFreeBytes
 , AvgPageDensity
 , ScanDensity
 , BestCount
 , ActualCount
 , LogicalFrag
 , ExtentFrag
 , Status
 , SecondsToShowContig)
select @DatabaseName
 , @ObjectOwner
 , ObjectName
 , ObjectId
 , IndexName
 , IndexId
 , Lvl
 , CountPages
 , CountRows
 , MinRecSize
 , MaxRecSize
 , AvgRecSize
 , ForRecCount
 , Extents
 , ExtentSwitches
 , AvgFreeBytes
 , AvgPageDensity
 , ScanDensity
 , BestCount
 , ActualCount
 , LogicalFrag
 , ExtentFrag
 , Status = 
   case	
    when @PlanMaintenance = 0 then
     case
      when (ObjectName = ''fraglist''
            or ScanDensity > 90
            or LogicalFrag < 10
            or (Indexid > 1 
                and exists(select 1 
                           from admin.dbo.fraglist 
                           where DatabaseName = @DatabaseName
                           and ObjectOwner = @ObjectOwner
                           and ObjectName = @ObjectName
                           and IndexId = 1 
                           and status = 4))) -- reindex clustered does all
       then 9  -- noop
      when CountPages < 10000
       then 4  -- reindex 
       else 1  -- defrag
       end
   else @PlanMaintenance
   end 
 , datediff(second,@ShowContigStartDt,getdate())
from #fraglist 

delete #fraglist'

if object_id('dbo.GetShowContigForIndex','P') is not null
 begin
  exec @rc = sp_executesql N'drop procedure dbo.GetShowContigForIndex'
  set @ec = @@error
  if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'drop procedure dbo.GetShowContigForIndex failed'
    goto ErrorHandler
   end
 end
exec @rc = sp_executesql @SQLStr 
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'create procedure dbo.GetShowContigForIndex failed'
  goto ErrorHandler
 end

-- add components to prevent log growth for all INDEXDEFRAGs
-- do not DEFRAG if a backup or restore is active
set @SQLStr = 
'CREATE procedure dbo.DefragLargeTables
 @DatabaseName varchar(128)
as
/*
see IndexMaintenanceForDB
*/
declare @SQLStr nvarchar(4000)
 , @ObjectOwner varchar(128) 
 , @ObjectName varchar(255)
 , @IndexName varchar(255)
 , @status int
 , @DBCCStartDt datetime

-- one at a time until someone says stop, 2s before 1s
select top 1 @DatabaseName = Databasename
 , @ObjectOwner = ObjectOwner
 , @ObjectName = ObjectName
 , @IndexName = IndexName
 , @status = status
from admin.dbo.fraglist
where status in (1,2)
and DatabaseName = @DatabaseName
order by status desc

-- mark as current maintenance candidate if is a 1
update admin.dbo.fraglist		
set status = 2
where DatabaseName = @DatabaseName
and ObjectOwner = @ObjectOwner
and ObjectName = @ObjectName
and IndexName = @IndexName
and status = 1

set @SQLStr = ''DBCC INDEXDEFRAG('''''' 
               + @DatabaseName 
               + '''''',''''''
               + @ObjectOwner 
               + ''.''
               + @ObjectName 
               + '''''',''''''
               + @IndexName 
               + '''''') '' +
               + ''UPDATE STATISTICS ['' 
               + @DatabaseName 
               + ''].['' 
               + @ObjectOwner 
               + ''].['' + @ObjectName 
               + ''] ['' 
               + @IndexName 
               + '']''

set @DBCCStartDt = getdate()

exec sp_executesql @SQLStr 

-- mark as maintenance complete
update admin.dbo.fraglist		
set status = 3
 , SecondsToMaintain = Datediff(second,@DBCCStartDt,getdate()) 
where DatabaseName = @DatabaseName
and ObjectOwner = @ObjectOwner
and ObjectName = @ObjectName
and IndexName = @IndexName
and status = 2

-- add a fraglist row to show the table/index state after the maintenance
exec admin.dbo.GetShowContigForIndex 
   @DatabaseName = @DatabaseName
 , @ObjectOwner = @ObjectOwner
 , @ObjectName = @ObjectName
 , @IndexName = @IndexName
 , @PlanMaintenance = 7'

set @JobCategory = 'Database Maintenance'
set @JobName = 'w Defrag large ' + @DatabaseName + ' tables'
set @JobDesc = 'defrag indexes - stopped if log space reaches threshold size.'  
set @JobCmd = 'EXEC admin.dbo.DefragLargeTables ' 
            + char(39) + @DatabaseName + char(39)
set @JobCmd2 = 'exec admin.dbo.log_job_error ' 
             + char(39) + @NotifyOnFail + char(39) 
set @JobOwner = 'sa'
set @OutputFile = 'F:\MSSQL\Log\' + @JobName + '.out'
		
if object_id('dbo.DefragLargeTables','P') is not null
 begin
  exec @rc = sp_executesql N'drop procedure dbo.DefragLargeTables'
  set @ec = @@error
  if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'drop procedure dbo.DefragLargeTables failed'
    goto ErrorHandler
   end
 end
exec @rc = sp_executesql @SQLStr 
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'create procedure dbo.DefragLargeTables failed'
  goto ErrorHandler
 end

if exists (select 1 from msdb.dbo.sysJobs where name = @JobName)  
 begin
  exec @rc = msdb.dbo.sp_delete_job @job_name = @JobName
  set @ec = @@error
  if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'sp_delete_job failed'
    goto ErrorHandler
   end
 end

if not exists (select 1 
               from msdb.dbo.syscategories 
               where name = @JobCategory)  
 begin
  exec @rc = msdb.dbo.sp_add_category  
     @class = 'Job'
   , @type = 'local'
   , @name = @JobCategory
 set @ec = @@error
 if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'sp_add_category failed'
    goto ErrorHandler
   end
 end
	
exec @rc = msdb.dbo.sp_add_job 
   @job_name = @JobName
 , @owner_login_name = @JobOwner
 , @description = @JobDesc
 , @category_name = @JobCategory
 , @enabled = 1
 , @notify_level_email = 0
 , @notify_level_page = 0
 , @notify_level_netsend = 0
 , @notify_level_eventlog = 2
 , @delete_level= 0
set @ec = @@error

if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_add_job failed'
  goto ErrorHandler
 end

exec @rc = msdb.dbo.sp_add_jobstep 
   @job_name = @JobName
 , @step_id = 1
 , @step_name = 'Defrag large tables'
 , @command = @JobCmd
 , @database_name = 'admin'
 , @database_user_name = ''
 , @subsystem = 'TSQL'
 , @cmdexec_success_code = 0
 , @flags = 4
 , @retry_attempts = 0
 , @retry_interval = 1
 , @output_file_name = @OutputFile
 , @on_success_step_id = 0
 , @on_success_action = 1
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_add_jobstep 1 failed'
  goto ErrorHandler
 end

exec @rc = msdb.dbo.sp_add_jobstep 
   @job_name = @JobName
 , @step_id = 2
 , @step_name = 'failure notification'
 , @command = @JobCmd2
 , @database_name = 'admin'
 , @database_user_name = ''
 , @subsystem = 'TSQL'
 , @cmdexec_success_code = 0
 , @flags = 2
 , @retry_attempts = 0
 , @retry_interval = 1
 , @output_file_name = @OutputFile
 , @on_success_step_id = 0
 , @on_success_action = 2
 , @on_fail_step_id = 0
 , @on_fail_action = 2
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_add_jobstep 2 failed'
  goto ErrorHandler
 end

-- add step 1 notification link after creating step 2 to avoid warning message
exec @rc = msdb.dbo.sp_update_jobstep 
   @job_name = @JobName
 , @step_id = 1
 , @on_fail_step_id = 2
 , @on_fail_action = 4
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_update_jobstep 1 failed'
  goto ErrorHandler
 end

exec @rc = msdb.dbo.sp_add_jobserver 
   @job_name = @JobName
 , @server_name = '(local)' 
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_add_jobserver failed'
  goto ErrorHandler
 end

exec @rc = msdb.dbo.sp_update_job 
   @job_name = @JobName
 , @start_step_id = 1 
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'sp_update_job failed'
  goto ErrorHandler
 end
-- end -- add components for full recovery db

set nocount on

-- delete archive over 90 days
delete admin.dbo.fraglist
where DatabaseName = @DatabaseName
and RecCreatedDt < getdate() - 90

-- archive previous run
update admin.dbo.fraglist
set status = status + 10
where DatabaseName = @DatabaseName
and status < 10

-- fragmentation study (find 1s and 4s)
-- maintain sequential index processing order
select @SQLStr = 'use ' + @DatabaseName + '
declare @ObjectId int
 , @IndexId int
 , @DatabaseName varchar(128)
 , @ObjectOwner varchar(128)
 , @ObjectName varchar(128)
	, @IndexName varchar(128)

set nocount on

select @ObjectId = min(id) 
from dbo.sysobjects 
where type = ''U''

while @ObjectId is not null
 begin
  Select @IndexId = min(indid) 
  from dbo.sysindexes 
  where id = @ObjectId 
  and indid > 0 
  and indid < 255

  while @IndexId is not null
   begin
    select @DatabaseName = db_name()
     , @ObjectOwner = user_name(o.uid)
     , @ObjectName = o.name
     , @IndexName = i.name
    from dbo.sysobjects o
    join dbo.sysindexes i
    on o.id = i.id
    where o.id = @ObjectId
    and i.indid = @IndexId

    -- determine if the index ought to be indexed
    exec admin.dbo.GetShowContigForIndex 
       @DatabaseName = @DatabaseName
     , @ObjectOwner = @ObjectOwner
     , @ObjectName = @ObjectName
     , @IndexName = @IndexName
     , @PlanMaintenance = 0

    Select @IndexId = min(indid) 
    from dbo.sysindexes 
    where id = @ObjectId 
    and indid > @IndexId 
    and indid < 255

   end
  select @ObjectId = min(id) 
  from dbo.sysobjects 
  where type = ''U'' 
  and id > @ObjectId
 end'
exec @rc = sp_executesql @sqlStr
set @ec = @@error
if @rc <> 0 or @ec <> 0
 begin
  set @Err = 'DBCC showcontig failed processing'
  goto ErrorHandler
 end

-- perform all identified required maintenance
-- first reindex all the small tables
declare indexesToMaintainList insensitive cursor 
for
select ObjectOwner, ObjectName, IndexName, status
from admin.dbo.fraglist
where DatabaseName = @DatabaseName
and status = 4

open indexesToMaintainList

fetch next 
from indexesToMaintainList 
into @ObjectOwner, @ObjectName, @IndexName, @status

while @@fetch_status = 0
 begin
  -- mark as current maintenance candidate
  update admin.dbo.fraglist		
  set status = 5
  where DatabaseName = @DatabaseName
  and ObjectOwner = @ObjectOwner
  and ObjectName = @ObjectName
  and IndexName = @IndexName
  and status = 4

  set @SQLStr = 'DBCC DBREINDEX(''' 
              + @DatabaseName 
              + '.' 
              + @ObjectOwner 
              + '.' 
              + @ObjectName 
              + ''',''' 
              + @IndexName 
              + ''')'					
  set @DBCCStartDt = getdate()
  exec @rc = sp_executesql @sqlstr 
  set @ec = @@error
  if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'Defragmentation operation "' + @SQLStr + '" failed'
    goto ErrorHandler
   end

  -- mark as maintenance complete
  update admin.dbo.fraglist		
  set status = 6
   , SecondsToMaintain = Datediff(second,@DBCCStartDt, getdate())
  where DatabaseName = @DatabaseName
  and ObjectOwner = @ObjectOwner
  and ObjectName = @ObjectName
  and IndexName = @IndexName
  and status = 5
		
  exec admin.dbo.GetShowContigForIndex 
     @DatabaseName = @DatabaseName
   , @ObjectOwner = @ObjectOwner
   , @ObjectName = @ObjectName
   , @IndexName = @IndexName
   , @PlanMaintenance = 7
 	
  fetch next 
  from indexesToMaintainList 
  into @ObjectOwner, @ObjectName, @IndexName, @status
 end
close indexesToMaintainList
deallocate indexesToMaintainList

-- now INDEXDEFRAG all the larger tables
while exists (select 1 from admin.dbo.fraglist
              where DatabaseName = @DatabaseName
              and status in (1,2))
 begin
  if (select cntr_value from master..sysperfinfo
      where instance_name = @DatabaseName
      and counter_name = 'Log File(s) Used Size (KB)') > @LogUsedThresholdKB
   begin
    if  exists (select 1 
                from master.dbo.sysprocesses p with(nolock)
                join msdb.dbo.sysjobs j with(nolock)
                on substring(p.program_name
                            , charindex('0x', p.program_name) + 18
                            , 16) 
                 = substring(replace(j.job_id, '-',''),17,16) 
                where j.name = @JobName)
    or exists (select 1 
               from master.dbo.sysprocesses with(nolock)
               where (cmd like 'BACKUP%'
                      or cmd like 'RESTORE%')
               and dbid = db_id(@DatabaseName))
     begin
      exec admin.dbo.IsAgentJobRunnable @IsRunnable OUTPUT, @JobName, @JobOwner				
      if  @IsRunnable = 0
       begin
        -- stop the job
	exec @rc = msdb.dbo.sp_stop_job @JobName
        set @ec = @@error
        if @rc <> 0 or @ec <> 0
         begin
          set @Err = 'sp_stop_job ' + @JobName + ' failed'
          goto ErrorHandler
         end
       end
     end     
     -- Set all job steps to append to log file
     exec @rc = msdb.dbo.sp_update_jobstep 
        @job_name = @JobName
      , @step_id = 1
      , @flags = 2
     set @ec = @@error
     if @rc <> 0 or @ec <> 0
      begin
       set @Err = 'sp_update_jobstep 1 to append log file failed'
       goto ErrorHandler
      end
    --give log backup a chance to catch up
    waitfor delay '00:05:00'
   end
  else -- enough log space to keep going
   begin
    -- start the job if it's not running 
    if not exists (select 1 from master.dbo.sysprocesses p
                   join msdb.dbo.sysjobs j
                   on substring(p.program_name
                               , charindex('0x', p.program_name) + 18
                               , 16) 
                    = substring(replace(j.job_id, '-',''),17,16) 
                   where j.name = @JobName)
     begin
      exec admin.dbo.IsAgentJobRunnable @IsRunnable OUTPUT, @JobName, @JobOwner				
      if  @IsRunnable = 1
       begin
        exec @rc = msdb.dbo.sp_start_job @JobName
        set @ec = @@error
        if @rc <> 0 or @ec <> 0
         begin
          set @Err = 'sp_start_job ' + @JobName + ' failed'
         	goto ErrorHandler
         end
       end
     end
    -- wait a minute before checking again	
    waitfor delay '00:01:00'
   end
 end

-- cleanup
if exists (select 1 from msdb.dbo.sysJobs where name = @JobName)  
 begin
  exec @rc = msdb.dbo.sp_delete_job @job_name = @JobName
  set @ec = @@error
  if @rc <> 0 or @ec <> 0
   begin
    set @Err = 'sp_delete_job during cleanup failed'
    goto ErrorHandler
   end
 end

return
ErrorHandler:

set @ErrDesc = 'admin.dbo.IndexMaintenanceForDB failed ' 
             + '| Return Code %d ' 
             + '| Error Code %d ' 
             + '| @DatabaseName = %s ' 
             + '| @ObjectOwner = %s ' 
             + '| @ObjectName = %s ' 
             + '| @IndexName = %s ' 
             + '| Error: %s' 
raiserror 
 ( @ErrDesc
 , 16
 , 1
 , @rc
 , @ec
 , @DatabaseName
 , @ObjectOwner
 , @ObjectName
 , @IndexName
 , @Err)
return -1

GO
SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS ON 
GO


As a basic rule of thumb I find that re-indexing once a week is a practical starting place for index maintenance. (FWIW I also update statistics nightly even though I run my database with auto update statistic enabled.) If an index has more than 10% logical scan fragmentation or less than 90% scan density I will maintain that index by default. If an index has fewer than 10,000 pages I use DBCC DBREINDEX to reduce/remove fragmentation. It's probably not even necessary to maintain such small indexes, but since I have thousands of tables I find it helps to stabilize my storage requirements if I regularly maintain even the smallest indexes. Generally speaking an index of 10,000 pages can be DBCC DBREINDEX-ed on one of my Compaq DL580 (G1) production servers in 30 seconds or less so that number works well for me at this time.

For anything over 10,000 pages I use DBCC INDEXDEFRAG so that the data can remain available during the maintenance operation. As I mentioned earlier, DBCC INDEXDEFRAG can cause gigantic bursts of log growth. to over come the potential crippling effects of such growth I run all DBCC INDEXDEFRAG operations through a temporary SQL Agent job. This works by allowing the main index maintenance job to watch the "Log File(s) Used Size (KB)" counter in the master.dbo.sysperfinfo table and turn the temporary job on and off as appropriate to keep the log file from growing out of control.

I check the fragmentation using

    DBCC SHOWCONTIG (TablenName, IndexName) WITH TABLERESULTS, FAST

both before and after the index is maintained. This allows me to see not only how fragmented the index was before it was maintained but also how good of a job the maintenance operation did of cleaning things up for me. I didn't always check the fragmentation after maintenance, but I heard a few comments about the efficacy of DBCC INDEXDEFRAG and decided I'd better keep a closer eye on things. As it turns out, the DBCC SHOWCONTIG after de-fragmentation indicates that DBCC INDEXDEFRAG does an excellent job removing fragmentation at the page level. Spot checks also suggest that page scan density is well maintained by the on line DBCC INDEXDEFRAG as well.

One final comment concerning index maintenance is not really about indexes at all (there goes that political nonsense again). When a table has no clustered index there is no way to maintain the fragmentation. Fortunately, most rows are simply thrown at the end of the heap when there is not a clustered index. Unfortunately, before the rows get thrown at the end of the heap an insert will alway search the entire heap for a hole - fragmentation - to stick the new row in before it throws it at the end of the heap. Not only can you not maintain the holes in the heap but there is a performance penalty for all inserts into a heap.

Using the techniques outline here, I am able to maintain tables in excess of 100MM rows and 300,000 pages with no indications of problematic blocking in an environment with query timeouts as low as 5 seconds and typical query timeouts of 60 seconds.

See the excellent white paper by Mike Ruthruff of Microsoft's Scalability Team, "Microsoft SQL Server 2000 Index Defragmentation Best Practices" for addition information and official Microsoft guidance on logical fragmentation.

Bill