It is very important to keep your indexes in a healthy and defrage’d state to keep performance of the database to an optimum. In my experience this is an area that doesn’t get the attention as it should (honest I’ve seen tables fragmented for more then 99%)
Microsoft presents us with a maintenance-plan option to defrag the SQL Server tables, but the downside of their approach is that all tables within the db –including the big and not so defrag’ed ones are reorganized.For larger db’s this could lead to less availability of the db (processed tables can be locked), and unnecessary transaction log growth.
Why I think the code presented here is better:
- It only defrags indexes that suffer from fragmentation
- Small –base- tables (<8kb)>
- Databases that are not on-line, or are being used as fail-over db’s in transaction-log shipping or database mirroring are skipped
- System db’s (master, msdb, distribution) are included
- Not so heavily defrag’ed tables are reorganized instead of rebuild which takes less time to perform and improves uptime of the database
- Statistics are updated when required
- Newly created db’s are automatically included
- Through the usage off globally (##)temp-db tables the code can be stored as stored-procs in one central location per instance, I use a CodeCatalog db for this.
I normally schedule this script in SQL Server Agent once a day as a separate job.
Here’s T-SQL Code (store in CodeCatalog)
-- =========================================================================
-- Author: SQL Server Advisor
-- Create date: jan/2009
-- Description: Loops through database collection and calls
-- script to de-fragment
-- =========================================================================
CREATE PROCEDURE [dbo].[usp_SetupReorgDatabase] AS
DECLARE @dbname VARCHAR(100)
DECLARE dbnames_cursor CURSOR FAST_FORWARD LOCAL FOR
SELECT name FROM sys.databases
WHERE name NOT IN ('tempdb','Northwind','model') -- skip unwanted DBs
AND DATABASEPROPERTYEX([name], 'IsInStandBy') = 0
AND DATABASEPROPERTYEX(name, 'Status') = 'ONLINE'
ORDER BY name
OPEN dbnames_cursor
FETCH NEXT FROM dbnames_cursor INTO @dbname
WHILE @@FETCH_STATUS = 0
BEGIN
-- actual reorganization goes here...
EXEC usp_ReorgDatabase @dbname
FETCH NEXT FROM dbnames_cursor INTO @dbname
END
CLOSE dbnames_cursor
DEALLOCATE dbnames_cursor
-- Author: SQL Server Advisor
-- Create date: jan/2009
-- Description: Loops through database collection and calls
-- script to de-fragment
-- =========================================================================
CREATE PROCEDURE [dbo].[usp_SetupReorgDatabase] AS
DECLARE @dbname VARCHAR(100)
DECLARE dbnames_cursor CURSOR FAST_FORWARD LOCAL FOR
SELECT name FROM sys.databases
WHERE name NOT IN ('tempdb','Northwind','model') -- skip unwanted DBs
AND DATABASEPROPERTYEX([name], 'IsInStandBy') = 0
AND DATABASEPROPERTYEX(name, 'Status') = 'ONLINE'
ORDER BY name
OPEN dbnames_cursor
FETCH NEXT FROM dbnames_cursor INTO @dbname
WHILE @@FETCH_STATUS = 0
BEGIN
-- actual reorganization goes here...
EXEC usp_ReorgDatabase @dbname
FETCH NEXT FROM dbnames_cursor INTO @dbname
END
CLOSE dbnames_cursor
DEALLOCATE dbnames_cursor
-- ==========================================================================
-- Author: SQL Server Advisor
-- Create date: jan/2009
-- Description: Reorganizes fragmented tables
-- called from usp_SetupReorgDatabase
-- ==========================================================================
CREATE PROCEDURE [dbo].[usp_ReorgDatabase] @DBname AS VARCHAR(100)
AS
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);
DECLARE @DB_ID SMALLINT;
SET @DB_ID = DB_ID(@DBname)
-- drop any tempdb objects that might exist
IF OBJECT_ID('tempdb..#ReorgCandidates') IS NOT NULL
DROP TABLE #ReorgCandidates
IF OBJECT_ID('tempdb..##DBobjects') IS NOT NULL
DROP TABLE ##DBobjects
IF OBJECT_ID('tempdb..##DBschemas') IS NOT NULL
DROP TABLE ##DBschemas
IF OBJECT_ID('tempdb..##DBindexes') IS NOT NULL
DROP TABLE ##DBindexes
IF OBJECT_ID('tempdb..##DBpartitions') IS NOT NULL
DROP TABLE ##DBpartitions
-- use temp global ## tables to get objects/schemas/indexes info for the requested db
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBobjects FROM sys.objects'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBschemas FROM sys.schemas'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBindexes FROM sys.indexes'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBpartitions FROM sys.partitions'
EXEC SP_EXECUTESQL @command
SELECT OBJECT_ID AS objectid,
index_id AS indexid,
partition_number AS partitionnum,
avg_fragmentation_in_percent AS frag
INTO #ReorgCandidates
FROM sys.dm_db_index_physical_stats (@DB_ID, NULL, NULL , NULL, 'DETAILED')-- LIMITED DETAILED
WHERE index_id > 0--Only clustered and nonclustered indexes
AND (avg_fragmentation_in_percent >= 10 OR avg_page_space_used_in_percent < 60)
AND page_count > 8 -- We do not want indexes less than 1 extent in size
-- Declare the cursor for the list of partitions to be processed.
DECLARE ReIndexCursor CURSOR FAST_FORWARD FOR
SELECT DISTINCT objectid,indexid,partitionnum – ONLY GET UNIQUE objects
FROM #ReorgCandidates;
-- Open the cursor.
OPEN ReIndexCursor;
-- Loop through the objects.
WHILE (1=1)
BEGIN;
FETCH NEXT FROM ReIndexCursor
INTO @objectid, @indexid, @partitionnum;
IF @@FETCH_STATUS < 0
BREAK;
SELECT @frag = MAX(frag)-- get the max fragmentation level of this table
FROM #ReorgCandidates
WHERE objectid = @objectid
AND indexid = @indexid
-- Conditionally select tables and indexes from the sys.dm_db_index_physical_stats function
-- and convert object and index IDs to names.
SELECT @objectname = QUOTENAME(o.name), @schemaname = QUOTENAME(s.name)
FROM ##DBobjects AS o
JOIN ##DBschemas AS s ON s.schema_id = o.schema_id
WHERE o.OBJECT_ID = @objectid;
SELECT @indexname = QUOTENAME(name)
FROM ##DBindexes
WHERE OBJECT_ID = @objectid AND index_id = @indexid;
SELECT @partitioncount = COUNT (*)
FROM ##DBpartitions
WHERE OBJECT_ID = @objectid AND index_id = @indexid;
-- 20 is an arbitrary decision point at which to switch between reorganizing and rebuilding.
IF @frag < 20.0
BEGIN
SET @command = N'USE ' + @DBname + N' UPDATE STATISTICS ' + @schemaname + N'.' + @objectname;
PRINT N'Executed: ' + @command
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
END
IF @frag >= 20.0
BEGIN
SET @command = N'USE ' + @DBname + N' ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90, SORT_IN_TEMPDB = ON)';
END
IF @partitioncount > 1
BEGIN
SET @command = @command + N'USE ' + @DBname + N' PARTITION=' + CAST(@partitionnum AS NVARCHAR(10));
END
EXEC SP_EXECUTESQL @command
PRINT N'Executed: ' + @objectname + '-' + CAST(@objectid AS NVARCHAR(30)) + '-' + @indexname + '-' + CAST(@frag AS NVARCHAR(30)) + '/' + @command;
END;
-- Close and deallocate the cursor.
CLOSE ReIndexCursor;
DEALLOCATE ReIndexCursor;
-- Drop the temporary tables.
IF OBJECT_ID('tempdb..#ReorgCandidates') IS NOT NULL
DROP TABLE #ReorgCandidates
IF OBJECT_ID('tempdb..##DBobjects') IS NOT NULL
DROP TABLE ##DBobjects
IF OBJECT_ID('tempdb..##DBschemas') IS NOT NULL
DROP TABLE ##DBschemas
IF OBJECT_ID('tempdb..##DBindexes') IS NOT NULL
DROP TABLE ##DBindexes
IF OBJECT_ID('tempdb..##DBpartitions') IS NOT NULL
DROP TABLE ##DBpartitions
-- Author: SQL Server Advisor
-- Create date: jan/2009
-- Description: Reorganizes fragmented tables
-- called from usp_SetupReorgDatabase
-- ==========================================================================
CREATE PROCEDURE [dbo].[usp_ReorgDatabase] @DBname AS VARCHAR(100)
AS
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);
DECLARE @DB_ID SMALLINT;
SET @DB_ID = DB_ID(@DBname)
-- drop any tempdb objects that might exist
IF OBJECT_ID('tempdb..#ReorgCandidates') IS NOT NULL
DROP TABLE #ReorgCandidates
IF OBJECT_ID('tempdb..##DBobjects') IS NOT NULL
DROP TABLE ##DBobjects
IF OBJECT_ID('tempdb..##DBschemas') IS NOT NULL
DROP TABLE ##DBschemas
IF OBJECT_ID('tempdb..##DBindexes') IS NOT NULL
DROP TABLE ##DBindexes
IF OBJECT_ID('tempdb..##DBpartitions') IS NOT NULL
DROP TABLE ##DBpartitions
-- use temp global ## tables to get objects/schemas/indexes info for the requested db
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBobjects FROM sys.objects'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBschemas FROM sys.schemas'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBindexes FROM sys.indexes'
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' SELECT * INTO ##DBpartitions FROM sys.partitions'
EXEC SP_EXECUTESQL @command
SELECT OBJECT_ID AS objectid,
index_id AS indexid,
partition_number AS partitionnum,
avg_fragmentation_in_percent AS frag
INTO #ReorgCandidates
FROM sys.dm_db_index_physical_stats (@DB_ID, NULL, NULL , NULL, 'DETAILED')-- LIMITED DETAILED
WHERE index_id > 0--Only clustered and nonclustered indexes
AND (avg_fragmentation_in_percent >= 10 OR avg_page_space_used_in_percent < 60)
AND page_count > 8 -- We do not want indexes less than 1 extent in size
-- Declare the cursor for the list of partitions to be processed.
DECLARE ReIndexCursor CURSOR FAST_FORWARD FOR
SELECT DISTINCT objectid,indexid,partitionnum – ONLY GET UNIQUE objects
FROM #ReorgCandidates;
-- Open the cursor.
OPEN ReIndexCursor;
-- Loop through the objects.
WHILE (1=1)
BEGIN;
FETCH NEXT FROM ReIndexCursor
INTO @objectid, @indexid, @partitionnum;
IF @@FETCH_STATUS < 0
BREAK;
SELECT @frag = MAX(frag)-- get the max fragmentation level of this table
FROM #ReorgCandidates
WHERE objectid = @objectid
AND indexid = @indexid
-- Conditionally select tables and indexes from the sys.dm_db_index_physical_stats function
-- and convert object and index IDs to names.
SELECT @objectname = QUOTENAME(o.name), @schemaname = QUOTENAME(s.name)
FROM ##DBobjects AS o
JOIN ##DBschemas AS s ON s.schema_id = o.schema_id
WHERE o.OBJECT_ID = @objectid;
SELECT @indexname = QUOTENAME(name)
FROM ##DBindexes
WHERE OBJECT_ID = @objectid AND index_id = @indexid;
SELECT @partitioncount = COUNT (*)
FROM ##DBpartitions
WHERE OBJECT_ID = @objectid AND index_id = @indexid;
-- 20 is an arbitrary decision point at which to switch between reorganizing and rebuilding.
IF @frag < 20.0
BEGIN
SET @command = N'USE ' + @DBname + N' UPDATE STATISTICS ' + @schemaname + N'.' + @objectname;
PRINT N'Executed: ' + @command
EXEC SP_EXECUTESQL @command
SET @command = N'USE ' + @DBname + N' ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
END
IF @frag >= 20.0
BEGIN
SET @command = N'USE ' + @DBname + N' ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90, SORT_IN_TEMPDB = ON)';
END
IF @partitioncount > 1
BEGIN
SET @command = @command + N'USE ' + @DBname + N' PARTITION=' + CAST(@partitionnum AS NVARCHAR(10));
END
EXEC SP_EXECUTESQL @command
PRINT N'Executed: ' + @objectname + '-' + CAST(@objectid AS NVARCHAR(30)) + '-' + @indexname + '-' + CAST(@frag AS NVARCHAR(30)) + '/' + @command;
END;
-- Close and deallocate the cursor.
CLOSE ReIndexCursor;
DEALLOCATE ReIndexCursor;
-- Drop the temporary tables.
IF OBJECT_ID('tempdb..#ReorgCandidates') IS NOT NULL
DROP TABLE #ReorgCandidates
IF OBJECT_ID('tempdb..##DBobjects') IS NOT NULL
DROP TABLE ##DBobjects
IF OBJECT_ID('tempdb..##DBschemas') IS NOT NULL
DROP TABLE ##DBschemas
IF OBJECT_ID('tempdb..##DBindexes') IS NOT NULL
DROP TABLE ##DBindexes
IF OBJECT_ID('tempdb..##DBpartitions') IS NOT NULL
DROP TABLE ##DBpartitions