T-SQL: Shrink incremental

First off you don’t ever shrink a database file unless you known it won’t need the space back again. (There’s some caveats to that statement but it mostly holds true) If that’s the space the file has consumed generally that’s the space the file needs. Shrinking just means the file will grow again and SQL Server will take a performance hit while it does that. You’re better off just adding the additional disk space.

If you need to perform a shrink and you’re worried about how long it will take and the performance impact it will have you can perform shrinks in small chunks. This is good practice particularly in a production transactional (OLTP) system as small increments  can make continuous, incremental progress, instead of trying to shrink by a large amount in one command. This makes it easier to shrink a database that must be shrunk by a large amount, and allows the shrink process to interrupted without losing all progress.

The script below can be used to shrink a database file in small increments until it reaches a target free space size based on the current free space percentage.

Simply provide as variables the database name, whether the file to be shrunk is the rows or log file, how much space to be left in terms of a percentage and how many shrinks you wish to perform. If you only want to truncate the file set the flag to 1 and you can ignore providing values for the number of shrinks or the percentage of space to keep.

(TruncateOnly means releasing all free space at the end of the file to the operating system without performing any page movement inside the file)

The script will determine given the number of shrinks and the target size what the shrink increment should be. It will then loop and execute the DBCC SHRINKFILE command to shrink the database file by the calculated increment until it reaches the target free space.

Progress updates are written to the global temp table ##DbStats so you can query this table during the shrink to see how far along the process is. The query to perform this check is commented out at the bottom of the script.

SET NOCOUNT ON;

DECLARE @DbName SYSNAME;
DECLARE @LogicalName VARCHAR(MAX);
DECLARE @DbFileName SYSNAME;
DECLARE @RowsOrLog VARCHAR(4);
DECLARE @SpaceToLeavePercentage VARCHAR(2);
DECLARE @Sql VARCHAR(MAX);
DECLARE @Sql2 VARCHAR(MAX);
DECLARE @ShrinkSql VARCHAR(MAX);
DECLARE @NumberOfShrinks INT;
DECLARE @NumberOfShrinksPre INT;
DECLARE @FreeMbRounded INT;
DECLARE @TargetFreeMbRounded INT;
DECLARE @ShrinkIncrementInMb INT;
DECLARE @ShrinkToInMb INT;
DECLARE @TruncateOnly BIT;



/*SET USER INPUT VARIABLES*/
SET @DbName = 'MY_DATABASE';
SET @RowsOrLog = 'Rows';
SET @SpaceToLeavePercentage = '10';
SET @NumberOfShrinks = 5;


--SET @TruncateOnly = 1;

IF OBJECT_ID('tempdb..##DbStatsCursor') IS NOT NULL
BEGIN
    DROP TABLE ##DbStatsCursor
END;

IF OBJECT_ID('tempdb..##DbStatsPre') IS NOT NULL
BEGIN
    DROP TABLE ##DbStatsPre
END;

IF OBJECT_ID('tempdb..##DbStats') IS NOT NULL
BEGIN
    DROP TABLE ##DbStats
END;


CREATE TABLE ##DbStats (
    Id_DbStats INT NOT NULL IDENTITY(1, 1) PRIMARY KEY
    ,LogicalName SYSNAME
    ,FreeMbRounded INT
    ,TargetFreeMbRounded INT
    ,DiffMb INT
    ,ShrinkIncrementInMb INT
    ,NumberOfShrinksLeft INT
    );
CREATE TABLE ##DbStatsPre (
    Id_DbStats INT NOT NULL IDENTITY(1, 1) PRIMARY KEY
    ,LogicalName SYSNAME
    );
	
CREATE TABLE ##DbStatsCursor (
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY
    ,LogicalName SYSNAME
    );


IF @SpaceToLeavePercentage IS NULL
BEGIN
    SET @SpaceToLeavePercentage = '10'
END

SET @Sql = 'USE ' + QUOTENAME(@DbName) + '; SELECT Name FROM ' + QUOTENAME(@DbName) + '.sys.database_files WHERE   type_desc = ''' + @RowsOrLog + ''''
/*print @Sql;  */


INSERT INTO ##DbStatsPre (
     LogicalName
    )
EXEC (@Sql);

DECLARE THECURSOR CURSOR 
  LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR select  LogicalName from ##DbStatsPre order by Id_DbStats desc

OPEN THECURSOR
FETCH NEXT FROM THECURSOR INTO @LogicalName
WHILE @@FETCH_STATUS = 0
BEGIN 
 set @NumberOfShrinksPre = @NumberOfShrinks;
 print '-----------' + @LogicalName;

  /* empty in case of loop */
  IF OBJECT_ID('tempdb..##DbStats') IS NOT NULL
BEGIN
     DELETE FROM ##DbStats;
END;


SET @Sql = '
USE ' + QUOTENAME(@DbName) + ';
SELECT LogicalName
,FreeMbRounded
,TargetFreeMbRounded
,FLOOR(FreeMbRounded) - FLOOR(TargetFreeMbRounded) AS DiffMb
FROM (
SELECT NAME AS LogicalName
    ,CAST(ROUND(CONVERT(DECIMAL(12, 1), (size - FILEPROPERTY(NAME, ''SpaceUsed'')) / 128.0), 0) AS INT) AS FreeMbRounded
    ,CAST(ROUND(CONVERT(DECIMAL(12, 1),((((size - FILEPROPERTY(NAME, ''SpaceUsed'')) / 128.0) / 100.0) * ' + @SpaceToLeavePercentage + ')), 0) AS INT) AS TargetFreeMbRounded
FROM ' + QUOTENAME(@DbName) + '.sys.database_files 
WHERE type_desc = ''' + @RowsOrLog + '''
) AS a
where LogicalName = ''' + @LogicalName + ''' 
';

INSERT INTO ##DbStats (
    LogicalName
    ,FreeMbRounded
    ,TargetFreeMbRounded
    ,DiffMb
    )
EXEC (@Sql);

/* select * from  ##DbStats; */

INSERT INTO ##DbStatsCursor (LogicalName) values (@LogicalName)

SET @TargetFreeMbRounded = (
        SELECT TargetFreeMbRounded
        FROM ##DbStats
        )
SET @DbFileName = (
        SELECT LogicalName
        FROM ##DbStats
        );

IF @TruncateOnly = 1
BEGIN
    SET @ShrinkSql = '
USE ' + QUOTENAME(@DbName) + ';

DBCC SHRINKFILE (
        N' + '''' + @DbFileName + '''' + '
        , 0
        , TRUNCATEONLY
        ) WITH NO_INFOMSGS;
';

    EXEC (@ShrinkSql);
END
ELSE
BEGIN

				
    SET @ShrinkIncrementInMb = (
            (
                SELECT (FLOOR(MAX(FreeMbRounded)) - @TargetFreeMbRounded)
                FROM ##DbStats
                ) / @NumberOfShrinksPre
            );

    UPDATE ##DbStats
    SET ShrinkIncrementInMb = @ShrinkIncrementInMb
        ,NumberOfShrinksLeft = @NumberOfShrinksPre

    SELECT TOP 1 *
    FROM ##DbStats
    ORDER BY Id_DbStats DESC;

    IF @ShrinkIncrementInMb > 0
    BEGIN

     /* problem?*/
	    print @NumberOfShrinksPre;
        WHILE @NumberOfShrinksPre > 0
        BEGIN
			  SET @FreeMbRounded = (
                    SELECT FreeMbRounded
                    FROM ##DbStats
                    WHERE Id_DbStats = (
                            SELECT MAX(Id_DbStats)
                            FROM ##DbStats
                            )
                    );

            IF @TargetFreeMbRounded > (@FreeMbRounded - @ShrinkIncrementInMb)
            BEGIN
                SET @ShrinkToInMb = @TargetFreeMbRounded;
            END
            ELSE
            BEGIN
                SET @ShrinkToInMb = (@FreeMbRounded - @ShrinkIncrementInMb);
            END


            SET @ShrinkSql = '
USE ' + QUOTENAME(@DbName) + ';

DBCC SHRINKFILE (
        N' + '''' + @DbFileName + '''' + '
        ,' + CONVERT(VARCHAR(12), @ShrinkToInMb) + '
        ) WITH NO_INFOMSGS;
';

            EXEC (@ShrinkSql);

            SET @NumberOfShrinksPre = @NumberOfShrinksPre - 1;
            SET @Sql = '
USE ' + QUOTENAME(@DbName) + ';
SELECT LogicalName
,FreeMbRounded
,' + CONVERT(VARCHAR(255), @TargetFreeMbRounded) + '
,FLOOR(FreeMbRounded) - ' + CONVERT(VARCHAR(255), @TargetFreeMbRounded) + ' AS DiffMb
,' + CONVERT(VARCHAR(255), @ShrinkIncrementInMb) + '
,' + CONVERT(VARCHAR(255), @NumberOfShrinksPre) + '
FROM (
SELECT NAME AS LogicalName
    ,CAST(ROUND(CONVERT(DECIMAL(12, 1), (size - FILEPROPERTY(NAME, ''SpaceUsed'')) / 128.0), 0) AS INT) AS FreeMbRounded
    ,CAST(ROUND(CONVERT(DECIMAL(12, 1),((((size - FILEPROPERTY(NAME, ''SpaceUsed'')) / 128.0) / 100.0) * ' + @SpaceToLeavePercentage + ')), 0) AS INT) AS TargetFreeMbRounded
FROM ' + QUOTENAME(@DbName) + '.sys.database_files 
WHERE type_desc = ''' + @RowsOrLog + ''' and Name = ''' + @LogicalName + ''' 
) AS a
';

            INSERT INTO ##DbStats (
                LogicalName
                ,FreeMbRounded
                ,TargetFreeMbRounded
                ,DiffMb
                ,ShrinkIncrementInMb
                ,NumberOfShrinksLeft
                )
            EXEC (@Sql);

            SELECT TOP 1 *
            FROM ##DbStats
            ORDER BY Id_DbStats DESC;
        END
    END
    ELSE
    BEGIN
        SELECT *
        FROM ##DbStats;
    END

    
END;
        

		FETCH NEXT FROM THECURSOR INTO @LogicalName
		END
CLOSE THECURSOR
DEALLOCATE THECURSOR
DROP TABLE ##DbStats
DROP TABLE ##DbStatsPre
SELECT  Id_DbStats    ,LogicalName
FROM ##DbStatsPre WITH (NOLOCK)
ORDER BY Id_DbStats asc;

SELECT TOP 1 Id_DbStats
    ,LogicalName
    ,FreeMbRounded
    ,TargetFreeMbRounded
    ,DiffMb
    ,ShrinkIncrementInMb
    ,NumberOfShrinksLeft
FROM ##DbStats WITH (NOLOCK)
ORDER BY Id_DbStats DESC;

SELECT percent_complete AS PercentageComplete
    ,start_time AS StartTime
    ,STATUS AS CurrentStatus
    ,command AS Command
    ,estimated_completion_time AS EstimatedCompletionTime
    ,cpu_time AS CpuTime
    ,total_elapsed_time AS TotalElapsedTime
FROM sys.dm_exec_requests
WHERE Command = 'DbccFilesCompact'

SELECT percent_complete AS PercentageComplete
    ,start_time AS StartTime
    ,STATUS AS CurrentStatus
    ,command AS Command
    ,estimated_completion_time AS EstimatedCompletionTime
    ,cpu_time AS CpuTime
    ,total_elapsed_time AS TotalElapsedTime
FROM sys.dm_exec_requests
WHERE Command = 'DbccSpaceReclaim'
Share your love