Blog Post

Rebuild indexes based on fragmentation – sp_RebuildIndex

In my previous post here, I’ve discussed how we can detect fragmentation in SQL Server databases indexes using dynamic management view function sys.dm_db_index_physical_stats. In this post, I’m sharing my stored procedure which I’ve created a while ago to rebuild fragmented indexes based on the fragmentation level. This stored procedure is based on sys.dm_db_index_physical_stats and resides in master database. This procedure automatically rebuilds indexes if the fragmentation level is above 30% and reorganises indexes if fragmentation is less than 30%.

Code for the procedure is as follow:

USE [master]
GO
SET ANSI_NULLS ON
GO  
CREATE PROC [dbo].[sp_RebuildIndex] @FragmentationLevel [int] = 30
, @SortInTempDB [varchar](8) = 'OFF' , @Online [varchar](8) = 'OFF'
, @LOBCompaction [varchar](8) = 'ON' , @ScanMode [varchar](16) = 'DETAILED'
, @MinRowsInTable [int] = 1000  
--Stored Procedure: [Internal].[sp_RebuildIndex]
--Author: Basit A. Farooq
--Version #: v1.0.0
--******************** 
-- Purpose/Comments
--********************
--Stored procedure to rebuild the indexes of database. 
--Indexes will be rebuild based on fragmentation criteria and scan mode.
AS BEGIN  
SET NOCOUNT ON;  
--***********
--Parameters
--***********
--@FragmentationLevel [int]
--Specifies index fragementation level search criteria. 
--@SortInTempDB [varchar](8) 
--Specifies whether to store the sort results in tempdb. The default is OFF. 
--@LOBCompaction [varchar](8)
--Specifies that all pages that contain large object (LOB) data are compacted. 
--The default is ON 
--@Online [varchar](8) 
--Specifies whether underlying tables and associated indexes are available for queries and
--data modification during the index operation. The default is OFF. 
--@ScanMode [varchar](16) Is the name of the mode. 
--@ScanMode specifies the scan level that is used to obtain statistics. 
--Valid inputs are DEFAULT, NULL, LIMITED, SAMPLED or DETAILED. 
--The default (NULL) is LIMITED. 
--The DETAILED mode scans all pages and returns all statistics. 
--The SAMPLED mode returns statistics based on a 1 percent sample of all the pages 
--in the index or heap. The modes are progressivelyslower from LIMITED to DETAILED, 
--because more work is performed in each mode. 
--@MinRowsInTable [int]
--Specifies minimum rows in table for search criteria. 
--********
--Usage
--********
--EXEC [<Database_Name>].[dbo].[sp_RebuildIndex] 
--EXEC [<Database_Name>].[dbo].[sp_RebuildIndex] 30, 'OFF', 'OFF', 'ON', 'LIMITED', 10 
DECLARE @StatementID [int]
,@SQLStatement [varchar](max)
,@ErrorText [varchar](256)
,@SQLProcedureName [varchar](64)
SET @SQLProcedureName = 'sRebuildIndex'
BEGIN TRY
--Validating store procedure parameters
SET @ErrorText = 'Invalid argument was supplied for @FragementationLevel parameter.'
+ ' Valid inputs for this parameter should be in the between 1 and 100'
IF @FragmentationLevel NOT BETWEEN 1 AND 100
BEGIN
RAISERROR (@ErrorText
,11
,1
,@FragmentationLevel)
END
SET @ErrorText = 'Invalid argument was supplied for @SortInTempDB parameter.'
+ ' Valid inputs for this parameter are ON or OFF.'
IF @SortInTempDB != 'ON' AND @SortInTempDB != 'OFF'
BEGIN
RAISERROR (@ErrorText
,11
,1
,@SortInTempDB)
END
SET @ErrorText = 'Invalid argument was supplied for @LOBCompaction parameter.'
+ ' Valid inputs for this parameter are ON or OFF.'
IF @LOBCompaction != 'ON' AND @LOBCompaction != 'OFF'
BEGIN
RAISERROR (@ErrorText
,11
,1
,@LOBCompaction)
END
SET @ErrorText = 'Invalid argument was supplied for @Online parameter.'
+ ' Valid inputs for this parameter are ON or OFF.'
IF @Online != 'ON' AND @Online != 'OFF'
BEGIN
RAISERROR (@ErrorText
,11
,1
,@Online)
END
SET @ErrorText = 'Invalid argument was supplied for @ScanMode parameter.'
+ ' Valid inputs for this parameter are DEFAULT, NULL, LIMITED, SAMPLED or DETAILED.'
IF @ScanMode != 'LIMITED' 
    AND @ScanMode != 'NULL' 
    AND @ScanMode != 'SAMPLED' 
    AND @ScanMode != 'DETAILED' 
    AND @ScanMode != 'DEFAULT'
BEGIN
RAISERROR (@ErrorText
,11
,1
,@ScanMode)
END
SET @ErrorText = 'Invalid argument was supplied for @MinRowsInTable parameter.'
+ ' Valid inputs for this parameter should be in the between 1 and 1000000000000000'
IF @MinRowsInTable NOT BETWEEN 1 AND 1000000000000000
BEGIN
RAISERROR (@ErrorText
,11
,1
,@MinRowsInTable)
END
 
--Creating temporary tables.
IF OBJECT_ID('Tempdb.dbo.#SQLStatementsStore') IS NOT NULL
DROP TABLE #SQLStatementsStore
CREATE TABLE #SQLStatementsStore ([SQLStatementID] [int] IDENTITY (1, 1)
,[SQLstatement] [varchar](1024))
IF OBJECT_ID('Tempdb.dbo.#PartitionTables ') IS NOT NULL
DROP TABLE #PartitionTables
CREATE TABLE #PartitionTables ([ObjectId] [int]
,[SchemaId] [int]
,[TableName] [varchar](512))
IF OBJECT_ID('Tempdb.dbo.#Tables') IS NOT NULL
DROP TABLE #Tables
CREATE TABLE #Tables ([ObjectId] [int]
,[SchemaId] [int]
,[TableName] [varchar](512))
IF OBJECT_ID('Tempdb.dbo.#Index_Physical_Stats') IS NOT NULL
DROP TABLE #Index_Physical_Stats
CREATE TABLE #Index_Physical_Stats ( [ObjectId] [int] NOT NULL
,[TableName] [varchar](100) NOT NULL
,[IndexId] [int] NOT NULL
,[IndexName] [varchar](500) NOT NULL
,[IndexPartitionNumber] [int] NOT NULL
,[IndexType] [varchar](500) NOT NULL
,[PadIndex] [varchar](256) NOT NULL
,[IndexFillFactor] [int] NOT NULL
,[StatisticsNoRecompute] [varchar](256) NOT NULL
,[IgnoreDuplicateKey] [varchar](256) NOT NULL
,[AllowRowLocks] [varchar](256) NOT NULL
,[AllowPageLocks] [varchar](256) NOT NULL
,[AvgFragmentationInPercent] [float] NOT NULL
,[CountRows] [int] NOT NULL)
 
INSERT INTO #PartitionTables ([ObjectId]
,[SchemaId]
,[TableName])
SELECT DISTINCT
t.[object_id]
,t.[schema_id]
,t.[name]
FROM [sys].[partitions] p
INNER JOIN [sys].[tables] t
ON t.[object_id] = p.[object_id]
AND p.[partition_number] > 1
 
INSERT INTO #Tables ([ObjectId]
,[SchemaId]
,[TableName])
SELECT
t.[object_id]
,t.[schema_id]
,t.[name]
FROM [sys].[tables] t
WHERE t.[name] COLLATE Latin1_General_CI_AS
NOT IN (SELECT [TableName] FROM #PartitionTables)
 
INSERT INTO #Index_Physical_Stats ([ObjectId]
,[TableName]
,[IndexId]
,[IndexName]
,[IndexPartitionNumber]
,[IndexType]
,[PadIndex]
,[IndexFillFactor]
,[StatisticsNoRecompute]
,[IgnoreDuplicateKey]
,[AllowRowLocks]
,[AllowPageLocks]
,[AvgFragmentationInPercent]
,[CountRows])
SELECT
dm.[object_id]
,DB_NAME(DB_ID()) + '.' + s.[name] +'.' + o.[name]
,dm.[index_id]
,i.[name]
,dm.[partition_number]
,dm.[index_type_desc]
,[pad_index] = CASE i.[is_padded]
WHEN 0 THEN 'OFF'
WHEN 1 THEN 'ON'
END
,i.[fill_factor]
,[statistics_norecompute] = CASE st.[no_recompute]
WHEN 0 THEN 'OFF'
WHEN 1 THEN 'ON'
END
,[ignore_dup_key] = CASE i.[ignore_dup_key]
WHEN 0 THEN 'OFF'
WHEN 1 THEN 'ON'
END
,[allow_row_locks] = CASE i.[allow_row_locks]
WHEN 0 THEN 'OFF'
WHEN 1 THEN 'ON'
END
,[allow_page_locks] = CASE i.[allow_page_locks]
WHEN 0 THEN 'OFF'
WHEN 1 THEN 'ON'
END
,dm.[avg_fragmentation_in_percent]
,p.[rows]
FROM [sys].[objects] o
INNER JOIN [sys].[indexes] i
ON o.[object_id] = i.[object_id] AND i.name <> 'NULL'
INNER JOIN [sys].[dm_db_index_physical_stats] (DB_ID(), NULL, NULL, NULL, @ScanMode) dm
ON i.[object_id] = dm.[object_id]
AND i.[index_id] = dm.[index_id]
AND dm.[avg_fragmentation_in_percent] >= 5
INNER JOIN [sys].[schemas] s
ON o.[schema_id] = s.[schema_id]
INNER JOIN [sys].[stats] st
ON i.[name] COLLATE Latin1_General_CI_AS = st.[name] COLLATE Latin1_General_CI_AS
AND o.[object_id] = st.[object_id]
AND o.[type] COLLATE Latin1_General_CI_AS = 'U'
INNER JOIN [sys].[partitions] p
ON i.[index_id] = p.[index_id]
AND o.[object_id] = p.[object_id]
AND p.[rows] > @MinRowsInTable
--Changing the fillfactor to 100 where fillfactor is 0
UPDATE #Index_Physical_Stats
SET IndexFillFactor = 100
WHERE IndexFillFactor = 0
 
-- Building quries to re-build partiton tables indexes partitions with
-- fragmentation over 30 percent...
INSERT INTO #SQLStatementsStore (SQLstatement)
SELECT
'ALTER INDEX ' + i.[name] + ' ON '
+ DB_NAME(DB_ID()) + '.' + s.[name] +'.' + pt.[TableName] + ' '
+ 'REBUILD PARTITION = ' + CONVERT ([varchar](10), ips.[IndexPartitionNumber])
FROM #PartitionTables pt
INNER JOIN [sys].[indexes] i
ON pt.[ObjectId] = i.[object_id]
AND i.name COLLATE Latin1_General_CI_AS <> 'NULL'
INNER JOIN #Index_Physical_Stats ips
ON pt.[ObjectId] = ips.[ObjectId]
AND i.[index_id] = ips.[IndexId]
AND ips.[AvgFragmentationInPercent] > @FragmentationLevel
INNER JOIN [sys].[schemas] s
ON pt.[SchemaId] = s.[schema_id]
 
-- Building quries to re-build non-partiton tables indexes with
-- fragmentation over 30 percent...
INSERT INTO #SQLStatementsStore (SQLstatement)
SELECT
'ALTER INDEX ' + i.[name] + ' ON '
+ DB_NAME(DB_ID()) + '.' + s.[name] +'.' + t.[TableName] + ' '
+ 'REBUILD WITH (' + ' PAD_INDEX = ' + CONVERT ([varchar](8), ips.[PadIndex])
+ ', FILLFACTOR = ' + CONVERT ([varchar](8), ips.[IndexFillFactor])
+ ', SORT_IN_TEMPDB = ' + CONVERT ([varchar](8), @SortInTempDB)
+ ', STATISTICS_NORECOMPUTE = ' + CONVERT ([varchar](8), ips.[StatisticsNoRecompute])
+ ', ONLINE = ' + CONVERT ([varchar](8), @Online)
+ ', ALLOW_ROW_LOCKS = ' + CONVERT ([varchar](8), ips.[AllowRowLocks])
+ ', ALLOW_PAGE_LOCKS = ' + CONVERT ([varchar](8), ips.[AllowPageLocks]) + ' )'
FROM #Tables t
INNER JOIN [sys].[indexes] i
ON t.[ObjectId] = i.[object_id]
AND i.name COLLATE Latin1_General_CI_AS <> 'NULL'
INNER JOIN #Index_Physical_Stats ips
ON t.[ObjectId] = ips.[ObjectId]
AND i.[index_id] = ips.[IndexId]
AND ips.[AvgFragmentationInPercent] > @FragmentationLevel
INNER JOIN [sys].[schemas] s
ON t.[SchemaId] = s.[schema_id]
 
-- Building quries to re-organise partiton tables indexes partitions with
-- fragmentation between 5 and 30 percent...
INSERT INTO #SQLStatementsStore (SQLstatement)
SELECT
'ALTER INDEX ' + i.[name] + ' ON '
+ DB_NAME(DB_ID()) + '.' + s.[name] +'.' + pt.[TableName] + ' '
+ 'REORGANIZE PARTITION = ' + CONVERT ([varchar](8), ips.[IndexPartitionNumber])
FROM #PartitionTables pt
INNER JOIN [sys].[indexes] i
ON pt.[ObjectId] = i.[object_id]
AND i.name COLLATE Latin1_General_CI_AS <> 'NULL'
INNER JOIN #Index_Physical_Stats ips
ON pt.[ObjectId] = ips.[ObjectId]
AND i.[index_id] = ips.[IndexId]
AND ips.[AvgFragmentationInPercent] BETWEEN 5 AND @FragmentationLevel
INNER JOIN [sys].[schemas] s
ON pt.[SchemaId] = s.[schema_id]
 
-- Building quries to re-build non-partiton tables indexes with
-- fragmentation between 5 and 30 percent...
INSERT INTO #SQLStatementsStore (SQLstatement)
SELECT
'ALTER INDEX ' + i.[name] + ' ON '
+ DB_NAME(DB_ID()) + '.' + s.[name] +'.' + t.[TableName] + ' '
+ 'REORGANIZE WITH ( LOB_COMPACTION = ' + @LOBCompaction + ' )' AS 'query'
FROM #Tables t
INNER JOIN [sys].[indexes] i
ON t.[ObjectId] = i.[object_id]
AND i.name COLLATE Latin1_General_CI_AS <> 'NULL'
INNER JOIN #Index_Physical_Stats ips
ON t.[ObjectId] = ips.[ObjectId]
AND i.[index_id] = ips.[IndexId]
AND ips.[AvgFragmentationInPercent] BETWEEN 5 AND @FragmentationLevel
INNER JOIN [sys].[schemas] s
ON t.[SchemaId] = s.[schema_id]
SELECT @StatementID = MIN(SQLStatementID) FROM #SQLStatementsStore
 
WHILE @StatementID IS NOT NULL
BEGIN
SELECT @SQLStatement = SQLstatement
FROM #SQLStatementsStore
WHERE SQLStatementID = @StatementID
PRINT 'Executing...[' + @SQLStatement + '].'
EXEC (@SQLStatement)
DELETE FROM #SQLStatementsStore
WHERE SQLStatementID = @StatementID
SELECT @StatementID = MIN(SQLStatementID)
FROM #SQLStatementsStore
END
 
SELECT
[ObjectId]
,[TableName]
,[IndexId]
,[IndexName]
,[IndexPartitionNumber]
,[IndexType]
,[PadIndex]
,[IndexFillFactor]
,[StatisticsNoRecompute]
,[IgnoreDuplicateKey]
,[AllowRowLocks]
,[AllowPageLocks]
,[AvgFragmentationInPercent]
,[CountRows]
FROM #Index_Physical_Stats
IF OBJECT_ID('Tempdb.dbo.#SQLStatementsStore') IS NOT NULL
DROP TABLE #SQLStatementsStore
IF OBJECT_ID('Tempdb.dbo.#PartitionTables ') IS NOT NULL
DROP TABLE #PartitionTables
IF OBJECT_ID('Tempdb.dbo.#Tables') IS NOT NULL
DROP TABLE #Tables
IF OBJECT_ID('Tempdb.dbo.#Index_Physical_Stats') IS NOT NULL
DROP TABLE #Index_Physical_Stats
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER()
,ERROR_SEVERITY()
,ERROR_STATE()
,ISNULL(ERROR_PROCEDURE(), @SQLProcedureName)
,ERROR_LINE()
,ERROR_MESSAGE()
END CATCH
END
GO

 

Mark this procedure as system object by executing the following command so that this procedure can be accessed from any database:

USE [master]
GO
EXEC sys.sp_MS_marksystemobject sp_RebuildIndex
GO

 

In the next version, I will add a logging feature so that you can see what indexes were defragmented, see how bad the fragmentation was, and perhaps do some trending on the data.

Let me know if you’d like me to add any other features or if you find a bug.

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating