A Shrink That Stops Blocking Others: SHRINKFILE WAIT_AT_LOW_PRIORITY

SHRINKFILE WAIT_AT_LOW_PRIORITY lets a shrink step aside when it would otherwise get in the way of your users. The shrink queues politely behind other sessions, and you choose what happens if the wait runs long. Shrinking is still a rare tool, not a habit.

A pumice stone is set aside so another hand can use the wooden handle

The Friday cleanup that blocks the app

Here is a familiar story. You archived a big table last week, and the data file is now mostly empty. Someone says, “Let’s shrink it and get the disk back.” You run DBCC SHRINKFILE at 4 PM on Friday.

The shrink needs locks. Sessions that were fine a minute ago start to queue behind it. Your phone rings. A normal shrink does not care that it is in the way.

The WAIT_AT_LOW_PRIORITY option fixes that politeness problem. It does not fix the other problem, which is that shrinking is often a bad idea. We will look at both.

Build a database worth shrinking

The demo creates a database called SqlAuthorityDemo and drops it at the end. The data file starts at 64 MB. Two tables are filled together, one row each in turn, so their pages are mixed through the file. One row fills about one page.

USE master;
DROP DATABASE IF EXISTS SqlAuthorityDemo;
CREATE DATABASE SqlAuthorityDemo;
GO
ALTER DATABASE SqlAuthorityDemo SET RECOVERY SIMPLE;
ALTER DATABASE SqlAuthorityDemo MODIFY FILE (NAME = SqlAuthorityDemo, SIZE = 64MB);
USE SqlAuthorityDemo;
SET NOCOUNT ON;

CREATE TABLE dbo.KeepRows (Id int IDENTITY PRIMARY KEY, Pad char(8000) NOT NULL DEFAULT 'k');
CREATE TABLE dbo.JunkRows (Id int IDENTITY PRIMARY KEY, Pad char(8000) NOT NULL DEFAULT 'j');

DECLARE @i int = 0;
WHILE @i < 1500
BEGIN
    INSERT dbo.KeepRows DEFAULT VALUES;
    INSERT dbo.JunkRows DEFAULT VALUES;
    SET @i += 1;
END;

Look before you shrink

Check the file size, the used space, and the fragmentation of the table you plan to keep. Then drop the junk table, which leaves holes between the pages of the table you kept. In my run the file is 64 MB and, with both tables in place, about 27 MB is used. Fragmentation of the kept table is under one percent.

SELECT name,
       CONVERT(decimal(10,1), size / 128.0) AS SizeMiB,
       CONVERT(decimal(10,1), FILEPROPERTY(name, 'SpaceUsed') / 128.0) AS UsedMiB
FROM sys.database_files
WHERE type = 0;

SELECT avg_fragmentation_in_percent, page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.KeepRows'), 1, NULL, 'LIMITED');

DROP TABLE dbo.JunkRows;

Shrink with the polite option

Now the shrink itself. The target is 20 MB. The WAIT_AT_LOW_PRIORITY clause tells the shrink to queue behind other sessions instead of ahead of them. ABORT_AFTER_WAIT says what to do when the wait ends. NONE keeps waiting as a normal request. SELF gives up the shrink. BLOCKERS ends the sessions in the way.

I use SELF every time. BLOCKERS means a user loses their work so a file can get smaller. That is a poor trade.

DBCC SHRINKFILE (N'SqlAuthorityDemo', 20)
    WITH WAIT_AT_LOW_PRIORITY (ABORT_AFTER_WAIT = SELF);
GO
SELECT name,
       CONVERT(decimal(10,1), size / 128.0) AS SizeMiB,
       CONVERT(decimal(10,1), FILEPROPERTY(name, 'SpaceUsed') / 128.0) AS UsedMiB
FROM sys.database_files
WHERE type = 0;

SELECT avg_fragmentation_in_percent, page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.KeepRows'), 1, NULL, 'LIMITED');

The file is now 20 MB, my target. But look at the second result. Fragmentation of the kept table jumped from under one percent to about 30 percent in my run. The shrink moved pages from the end of the file into the holes at the front, and their order got scrambled. Your exact number will differ.

I did not create a second session to hold a lock here. So this demo shows that the command works. It does not show the waiting behavior itself. Test that on a quiet test server with two query windows before you trust it.

What happens when the low-priority wait ends

Repairing the damage makes the file grow back

The usual fix for fragmentation is a rebuild. A rebuild needs working room to build the new copy. The file has only a few free megabytes now, so it grows again. The result shows fragmentation of zero, and in my run the file ended at 84 MB, bigger than the 64 MB I started with. Your growth step depends on your autogrowth setting.

ALTER INDEX ALL ON dbo.KeepRows REBUILD;

SELECT name,
       CONVERT(decimal(10,1), size / 128.0) AS SizeMiB,
       CONVERT(decimal(10,1), FILEPROPERTY(name, 'SpaceUsed') / 128.0) AS UsedMiB
FROM sys.database_files
WHERE type = 0;

SELECT avg_fragmentation_in_percent, page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.KeepRows'), 1, NULL, 'LIMITED');
GO
USE master;
DROP DATABASE IF EXISTS SqlAuthorityDemo;

That is the whole point. You shrank the file, fragmented the table, rebuilt it, and the file grew back. The disk space you wanted never really came back.

When a shrink is actually worth it

Shrink after a one-time drop in data that will not return, such as a purge or a retired archive. Make sure the files will not need that space again. Do not put it in a nightly job. And if you shrink, use SELF so it steps aside for your users.

Next time someone asks for a shrink, ask first whether the space will be needed again.

A polite shrink is not a good shrink, it is a rare cleanup done with manners.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Shrinking Database, SQL Lock, SQL Server DBCC
Previous Post
MySQL – Generate Script for a Table Using SQL
Next Post
JSON_OBJECT and JSON_ARRAY: Building JSON in SQL Server 2022

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.