Index Maintenance across all Databases in an Instance

I needed to make a job for a customer that rebuilds all indexes in an instance that have a fragmentation of more than 10%. Usually we use a maintenance plan for rebuilding indexes. Although that is a fine approach is has one disadvantage: it rebuilds all indexes in a database regardless of the level of fragmentation. I do not like that because it takes up resources that are not necessary. It also takes far longer to complete than my job.

As a basis I used the code from the BOL on the sys.dm_db_index_physical_stats page. Here you can find the following piece of code:

-- Ensure a USE <databasename> statement has been executed first.

SET
NOCOUNT
ON;

DECLARE @objectid int;

DECLARE @indexid int;

DECLARE @partitioncount bigint;

DECLARE @schemaname nvarchar(130);

DECLARE @objectname nvarchar(130);

DECLARE @indexname nvarchar(130);

DECLARE @partitionnum bigint;

DECLARE @partitions bigint;

DECLARE @frag float;

DECLARE @command nvarchar(4000);

-- Conditionally select tables and indexes from the sys.dm_db_index_physical_stats function

-- and convert object and index IDs to names.

SELECT

object_id
AS objectid,

index_id AS indexid,

partition_number AS partitionnum,

avg_fragmentation_in_percent AS frag

INTO #work_to_do

FROM
sys.dm_db_index_physical_stats
(DB_ID(),
NULL,
NULL
,
NULL,
'LIMITED')

WHERE avg_fragmentation_in_percent > 10.0 AND index_id > 0;

 

-- Declare the cursor for the list of partitions to be processed.

DECLARE
partitions
CURSOR
FOR
SELECT
*
FROM #work_to_do;

 

-- Open the cursor.

OPEN
partitions;

 

-- Loop through the partitions.

WHILE (1=1)

BEGIN;

FETCH
NEXT

FROM
partitions

INTO @objectid, @indexid, @partitionnum, @frag;

IF
@@FETCH_STATUS
< 0 BREAK;

SELECT @objectname =
QUOTENAME(o.name), @schemaname =
QUOTENAME(s.name)

FROM
sys.objects
AS o

JOIN
sys.schemas
as s ON s.schema_id
= o.schema_id

WHERE o.object_id
= @objectid;

SELECT @indexname =
QUOTENAME(name)

FROM
sys.indexes

WHERE
object_id
= @objectid AND index_id = @indexid;

SELECT @partitioncount =
count
(*)

FROM
sys.partitions

WHERE
object_id
= @objectid AND index_id = @indexid;

 

-- 30 is an arbitrary decision point at which to switch between reorganizing and rebuilding.

IF @frag < 30.0

SET @command =
N'ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REORGANIZE';

IF @frag >= 30.0

SET @command =
N'ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REBUILD';

IF @partitioncount > 1

SET @command = @command +
N' PARTITION='
+
CAST(@partitionnum AS
nvarchar(10));

EXEC (@command);

PRINT
N'Executed: '
+ @command;

END;

 

-- Close and deallocate the cursor.

CLOSE
partitions;

DEALLOCATE
partitions;

 

-- Drop the temporary table.

DROP
TABLE #work_to_do;

GO

This code runs in only one database so I needed to add a cursor to run through all databases on the instance. The code above also differentiates partitions in an index. I didn't need that, so I stripped it out. I als stripped out the reorganize code. On an enterprise edition I prefer online rebuilds.

 

I came up with the following code:

-- Index rebuild online on all databases on an instance

SET
NOCOUNT
ON

DECLARE @db as
sysname;

DECLARE @db_id as
int;

DECLARE @cmd1 as
nvarchar(1000);

 

DECLARE @objectid int;

DECLARE @indexid int;

DECLARE @schemaname nvarchar(130);

DECLARE @objectname nvarchar(130);

DECLARE @indexname nvarchar(130);

DECLARE @partitionnum bigint;

DECLARE @command nvarchar(4000);

 

DECLARE @errornumber int;

 

DECLARE DatabaseList CURSOR
FAST_FORWARD
FOR

SELECT [name], [database_id] FROM
sys.databases

WHERE [name] NOT
IN
('tempdb','model')

AND state_desc =
'ONLINE'

ORDER
BY [name]

 

OPEN DatabaseList

    FETCH
NEXT
FROM DatabaseList INTO @db, @db_id

        WHILE (@@fetch_status
= 0)

            BEGIN

                PRINT
N' ';

                PRINT
N'Database: '
+ @db;

                -- Conditionally select tables and indexes from the sys.dm_db_index_physical_stats function

                -- and convert object and index IDs to names.

                EXEC ('USE '
+ @db +
';

                SELECT    p.object_id AS objectid

                ,        p.index_id AS indexid

                ,        o.name AS objectName

                ,        s.name AS schemaName

                ,        i.name AS indexName

                INTO ##work_to_do

                FROM sys.dm_db_index_physical_stats ('
+ @DB_ID +
', NULL, NULL , NULL, ''LIMITED'') AS p

                    INNER JOIN sys.objects as o

                        ON p.object_id = o.object_id

                    INNER JOIN sys.schemas as s

                        ON s.schema_id = o.schema_id

                    INNER JOIN sys.indexes i

                        ON p.object_id = i.object_id

                        AND i.index_id = p.index_id

                WHERE p.avg_fragmentation_in_percent > 10.0 AND p.index_id > 0

                AND p.index_id < 25000

                AND p.page_count > 1000;')

                

                -- Declare the cursor for the list of fragmented indexes to be processed.

                DECLARE FragmentedIndexes CURSOR
FOR
SELECT
*
FROM ##work_to_do;

 

                -- Open the cursor.

                OPEN FragmentedIndexes;

 

                -- Loop through the indexes.

                WHILE (1=1)

                    BEGIN;

                        FETCH
NEXT

                         FROM FragmentedIndexes

                         INTO @objectid, @indexid, @objectname, @schemaname, @indexname;

                        IF
@@FETCH_STATUS
< 0 BREAK;

 

                        -- Check for invalid datatypes for online rebuild

                        -- If those datatypes are found, do normal rebuild

                        BEGIN
TRY

                            SET @command =
N'USE '
+ @db +
'; ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REBUILD WITH (ONLINE=ON)';

                            EXEC (@command);

                            PRINT
N'Executed: '
+ @command;

                        END
TRY

                        BEGIN
CATCH

                            SELECT @errornumber =
ERROR_NUMBER()

                            IF @errornumber = 2725

                                BEGIN

                                    SET @command =
N'USE '
+ @db +
'; ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REBUILD';

                                    EXEC (@command);

                                    PRINT
N'Executed: '
+ @command;

                                END

                        END
CATCH

                        

                    END;

 

                -- Close and deallocate the cursor.

                CLOSE FragmentedIndexes;

                DEALLOCATE FragmentedIndexes;

 

                -- Drop the temporary table.

                DROP
TABLE ##work_to_do;

                

                FETCH
NEXT
FROM DatabaseList INTO @db, @db_id

            END

            

CLOSE DatabaseList;            

DEALLOCATE DatabaseList;

 

Explanation of the code:

I start with creating a cursor for all databases on the instance:

DECLARE DatabaseList CURSOR
FAST_FORWARD
FOR

SELECT [name], [database_id] FROM
sys.databases

WHERE [name] NOT
IN
('tempdb','model')

AND state_desc =
'ONLINE'

ORDER
BY [name]

 

Of course I exclude the tempdb and the model database. On model I don't expect indexes and tempdb is obvious.

 

After that I open the database cursor and start processing the databases one at the time.

First I collect the fragmented indexes:

EXEC ('USE '
+ @db +
';

                SELECT    p.object_id AS objectid

                ,        p.index_id AS indexid

                ,        o.name AS objectName

                ,        s.name AS schemaName

                ,        i.name AS indexName

                INTO ##work_to_do

                FROM sys.dm_db_index_physical_stats ('
+ @DB_ID +
', NULL, NULL , NULL, ''LIMITED'') AS p

                    INNER JOIN sys.objects as o

                        ON p.object_id = o.object_id

                    INNER JOIN sys.schemas as s

                        ON s.schema_id = o.schema_id

                    INNER JOIN sys.indexes i

                        ON p.object_id = i.object_id

                        AND i.index_id = p.index_id

                WHERE p.avg_fragmentation_in_percent > 10.0 AND p.index_id > 0

                AND p.index_id < 25000

                AND p.page_count > 1000;')

 

I use sys.dm_db_index_physical_stats for finding the fragmented indexes. In order to get the name, schemaname and objectname I join the sys.dm_db_index_physical_stats with sys.objects, sys.schemas and sys.indexes.

The result I insert in a global temp table. Global because the exec statement runs on another thread so I can't use a local temp table. I can't see it in the rest of the procedure if I did that.

In the where clause I check on fragmentation over 10%. I also check if the index_id is smaller than 25000. If it is over it is either a XML index or a Spatial index.

The last check I do is on page_count of the index. SQL Server will not rebuild indexes that are smaller than 1000 pages(8MB). It will not do seeks on such small indexes, only scans. So fragmentation won't matter.

 

With the fragmented indexes in the temp table I can build the statement necessary to defragment the index:

BEGIN
TRY

SET @command =
N'USE '
+ @db +
'; ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REBUILD WITH (ONLINE=ON)';

    EXEC (@command);

    PRINT
N'Executed: '
+ @command;

END
TRY

BEGIN
CATCH

    SELECT @errornumber =
ERROR_NUMBER()

    IF @errornumber = 2725

        BEGIN

SET @command =
N'USE '
+ @db +
'; ALTER INDEX '
+ @indexname +
N' ON '
+ @schemaname +
N'.'
+ @objectname +
N' REBUILD';

            EXEC (@command);

            PRINT
N'Executed: '
+ @command;

        END

END
CATCH

 

Since the customer had an enterprise edition I do the rebuild online. That was necessary since the customer had a 24*7 system. Online rebuilds do have limitations: you can't rebuild an index online if the index contains blob datatypes like text, ntext, xml, varchar(max), nvarchar(max), image, etc….

If those datatypes are used in the underlying table you can't do an online rebuild either. That's why I check for errornumber 2725. If that's returned I build the rebuild statement without the online option.

The rest of the code is cleanup code. We should drop the temp table and close and deallocate the cursors(very important!)

I want to add some more errorhandling later on. The temptable should always be dropped. But that's the only thing missing in the code.

 

If you have any remarks on my code, I'll be delighted to hear it!

Regards,

Menzo