April 13, 2009 | SQL Server

Working with SQLFool's Index Defrag Script

Last week I decided to play with SQLFool's Automated Index Defrag Script and found it very usable and worthwhile.  Terrific work Michelle!  The script rivals Ola Hallengren's broader maintenance scripts that I use often.

On smaller databases it ran quite quickly and with the correct and desired effects.  On larger databases, due to the amount of time it takes to run, I found a couple of things lacking on the reporting side.  Not serious things, mind you, and nothing that is really all that hard to work around.  But I thought I would share them anyway, in case you were using the script and coming across similar issues.

What index is currently being reorganized?

There is a great output table that records the index reorganize tasks, called dbo.dba_indexDefragLog.  This records an entry as each index reorganize statement completes.  A great enhancement would be to record an entry when each index reorganize command *starts*, so that I can track its progress (for log entries like this I typically have a Start/End column pair, with Start populated by default, and End updated after the task is complete).  In the meantime, assuming you know the session_id (SPID) that is running the script, you can figure it out easily by using the following query… which not only tells you which index is being reorganized, but also an estimated completion time (lucky us, ALTER INDEX REORGANIZE is one of the commands that shows percent_complete in sys.dm_exec_requests):

SELECT
   r.percent_complete, 
   estimated_finish_time = DATEADD(MILLISECOND, estimated_completion_time, CURRENT_TIMESTAMP),
   t.[text]
FROM
   sys.dm_exec_requests AS r
CROSS APPLY
   sys.dm_exec_sql_text(r.[sql_handle]) AS t 
WHERE
   r.session_id = <SPID>;

Before vs. After Fragmentation

The output table dbo.dba_indexDefragLog shows the fragmentation of the index *before* the reorganize happened.  An excellent addendum to the logging table would be to show the fragmentation level *after* the reorganize was complete.  Currently, to see the before and after picture, I would need to go look at sys.dm_db_index_physical_stats for each index (since you cannot use CROSS APPLY against this particular DMV).  Paul Randal to the rescue: he explains how to work around this issue.  So taking that information, I created the same function, and then wrote the following, which queries the dbo.dba_indexDefragLog table and then runs a CROSS APPLY against the new function to get the *after* picture.

IF OBJECT_ID('dbo.dba_index_physical_stats') IS NOT NULL
   DROP FUNCTION dbo.dba_index_physical_stats;
GO
 
CREATE FUNCTION dbo.dba_index_physical_stats
(
   @database_id        INT,
   @object_id          INT,
   @index_id           INT,
   @partition_number   INT,
   @mode               SYSNAME
)
RETURNS @result TABLE
(
   database_id                     SMALLINT,
   [object_id]                     INT, 
   index_id                        INT, 
   partition_number                INT,
   index_type_desc                 NVARCHAR(60),
   alloc_unit_type_desc            NVARCHAR(60),
   index_depth                     TINYINT,
   index_level                     TINYINT,
   avg_fragmentation_in_percent    FLOAT,
   fragment_count                  BIGINT,
   avg_fragment_size_in_pages      FLOAT,
   page_count                      BIGINT, 
   avg_page_space_used_in_percent  FLOAT,
   record_count                    BIGINT,
   ghost_record_count              BIGINT,
   version_ghost_record_count      BIGINT,
   min_record_size_in_bytes        INT,
   max_record_size_in_bytes        INT,
   avg_record_size_in_bytes        FLOAT,
   forwarded_record_count          BIGINT
)
BEGIN
   INSERT INTO @result 
       SELECT * FROM sys.dm_db_index_physical_stats
       (
           @database_id,
           @object_id,
           @index_id, 
           @partition_number,
           @mode
       );
   RETURN;
END;
GO
 
-- now I can run queries like:
SELECT TOP (10) 
   l.databaseName,
   l.objectName,
   l.indexName,
   l.dateTimeStart,
   dateTimeFinish = DATEADD(SECOND, l.durationSeconds, l.dateTimeStart),
   l.durationSeconds,
   fragmentation_before = l.fragmentation,
   fragmentation_after = s.avg_fragmentation_in_percent,
   page_count_before = l.page_count,
   page_count_after = s.page_count
FROM 
   dbo.dba_indexDefragLog AS l
CROSS APPLY
   dbo.dba_index_physical_stats
   (
       l.DatabaseID,
       l.ObjectID,
       l.IndexID,
       l.PartitionNumber,
       NULL
   ) AS s
ORDER BY
   indexDefrag_id DESC;

Obviously this function is pretty expensive, which is why it would be better if it were called once per index at the end of the task, instead of querying every time.  But I don't want to touch Michelle's script… if she thinks these enhancements are worthwhile, I am sure she will add them.

3 comments on this post

    • Michelle Ufford - April 13, 2009, 6:49 PM

      Thanks, Aaron!  I'm currently compiling a list of feature requests to add to the next release.  I'll be sure to add these to the to-do list.  Thanks for the great input!  🙂

    • Uri Dimant - April 14, 2009, 9:45 AM

      Great article Aaron
      If I need a proof that the
      pages do not necessarily reside in the file in the same order as in the linked list, I can use DBCC IND to show the actual pointers between the pages in the linked list:
      DBCC IND('db', 'dbo.Tblname', 0);

    • db ramesh - April 21, 2016, 9:22 AM

      Thanks for the function Aaron as one must definitely validate the frags after rebuild/reorg.
      Please add one more column compressed_page_count  BIGINT to @results table to support this function in sql2014.

Comments are closed.