Script to determine free space to support shrinking SQL Server database files

By:   |   Updated: 2020-01-08   |   Comments (6)   |   Related: > Monitoring


Problem

As a DBA for several servers, I get a lot of calls about disk space. The first thing I do is look for data files I can shrink to free up some space on the disk. Some of the servers I support contain several databases and it can take a while to figure out which data file will give me the most immediate relief.

Solution

The first thing you need is the drive information. You can get this from the extended stored procedure xp_fixeddrives, which returns the drive letter and how much space is free on the drive. The next thing you need to know is how much space is available in each data file. When you pull up the information in SSMS, Microsoft uses two different DBCC calls; one for the data files and one for the log files. Instead of using separate DBCC calls, you can call the fileproperty function and pass the name of the data file and the SpaceUsed parameters. This has to be run from the context of the database in which the data files are located, so you can use the sp_msforeachdb stored procedure to return the results for all databases.

Now that you have the drive information and the space used, you can join it to the sys.databases and sys.master_files tables to return one view of all the data. It is now possible to quickly determine the files that have the most free space and where they are located.

USE master
GO 

CREATE TABLE #TMPFIXEDDRIVES ( DRIVE CHAR(1), MBFREE INT) 

INSERT INTO #TMPFIXEDDRIVES 
EXEC xp_FIXEDDRIVES 

CREATE TABLE #TMPSPACEUSED ( DBNAME VARCHAR(50), FILENME VARCHAR(50), SPACEUSED FLOAT) 

INSERT INTO #TMPSPACEUSED 
EXEC( 'sp_msforeachdb''use [?]; Select ''''?'''' DBName, Name FileNme, fileproperty(Name,''''SpaceUsed'''') SpaceUsed from sysfiles''') 

SELECT   C.DRIVE, 
         CASE  
           WHEN (C.MBFREE) > 1000 THEN CAST(CAST(((C.MBFREE) / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' GB' 
           ELSE CAST(CAST((C.MBFREE) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' MB' 
           END AS DISKSPACEFREE, 
         A.NAME AS DATABASENAME, 
         B.NAME AS FILENAME, 
         CASE B.TYPE  
           WHEN 0 THEN 'DATA' 
           ELSE TYPE_DESC 
           END AS FILETYPE, 
         CASE  
           WHEN (B.SIZE * 8 / 1024.0) > 1000 
           THEN CAST(CAST(((B.SIZE * 8 / 1024) / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' GB' 
           ELSE CAST(CAST((B.SIZE * 8 / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' MB' 
           END AS FILESIZE, 
         CAST((B.SIZE * 8 / 1024.0) - (D.SPACEUSED / 128.0) AS DECIMAL(15,2)) SPACEFREE, 
         B.PHYSICAL_NAME 
FROM     SYS.DATABASES A 
         JOIN SYS.MASTER_FILES B ON A.DATABASE_ID = B.DATABASE_ID 
         JOIN #TMPFIXEDDRIVES C  ON LEFT(B.PHYSICAL_NAME,1) = C.DRIVE 
         JOIN #TMPSPACEUSED D    ON A.NAME = D.DBNAME AND B.NAME = D.FILENME 
ORDER BY DISKSPACEFREE, 
         SPACEFREE DESC 
          
DROP TABLE #TMPFIXEDDRIVES 

DROP TABLE #TMPSPACEUSED 

After copying and pasting the above into a SQL Server query window and running all of the code the result set will look something like below. Here we see the FileSize and the SpaceFree (returned in MB) for each file. In the first row below, the FileSize is 81.94MB and 70.56MB of this file is unused spaced.

data file space free
data file space free
Next Steps
  • With a little modification, you can schedule the script to shrink data files containing over X amount of free space on a drive with less than X amount of disk space.
  • In a perfect world, DBA's would just be able to ask for more drive space and not have to worry about shrinking files. It is more efficient to give the database the room it needs instead of having to shrink them all the time. Until then, I guess I will keep using this script.


sql server categories

sql server webinars

subscribe to mssqltips

sql server tutorials

sql server white papers

next tip



About the author
MSSQLTips author Ken Simmons Ken Simmons is a database administrator, developer, SQL Server book author and Microsoft SQL Server MVP.

This author pledges the content of this article is based on professional experience and not AI generated.

View all my tips


Article Last Updated: 2020-01-08

Comments For This Article




Sunday, October 18, 2020 - 6:03:09 PM - Derek Back To Top (86656)
I wrote this script and it gives me a heads up. Hope of use. Derek ([email protected])

USE [DBATools]
GO

/****** Object: StoredProcedure [dbo].[usp_Check_Disk_Space] Script Date: 18/10/2020 23:02:08 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[usp_Check_Disk_Space]
AS

-- ===============================================================================
-- This script uses the extended stored procedure xp_fixeddrives to get the
-- available disk space on all local drives and compares it to the acceptable
-- available space threshold for that drive.
--
-- If the available disk space is less than the pre-defined threshold, an email
-- alert is sent.
--
-- Before using this script the table dbo.tbl_Local_Drives must be created to hold
-- the local drive letters and the acceptable available disk space threshold
-- for each local drive.
--
-- Set the threshold in MB's.
--
-- Written By : Derek Wallace ( DBA Services )
-- Date : 17/10/2020
--
-- Version : 1.0
--
-- Modification :
-- Modified By :
-- Modification Date : dd/mm/yyyy
--
-- ===============================================================================

BEGIN

DECLARE @DriveLetter CHAR(1)
DECLARE @DriveThreshold INT
DECLARE @DriveFreeSpace INT
DECLARE @Email_In_Box VARCHAR(25) -- Email box of the support team
DECLARE @EmailSubject VARCHAR(100) -- email subject for disk alert
DECLARE @EmailMsg VARCHAR(256) -- email message for disk alert

SET @Email_In_Box = '[email protected]' -- Set the email address for the support team

-- Create a temporary table to store the drive letter and available disk space for each local drive
CREATE TABLE #Local_Disks
(
DriveLetter CHAR(1) NOT NULL,
FreeMB INTEGER NOT NULL
)

-- Update the temporary table with the drive letter and the available disk space for each local drive
INSERT INTO #Local_Disks
EXEC MASTER..xp_fixeddrives
SELECT * from #Local_Disks
ORDER BY DriveLetter

-- Now have a look at the local drives on the temporary table
DECLARE DriveCursor CURSOR FAST_FORWARD FOR
SELECT DriveLetter, FreeMB from #Local_Disks

OPEN DriveCursor
FETCH NEXT FROM DriveCursor INTO
@DriveLetter, @DriveFreeSpace

WHILE (@@FETCH_STATUS = 0)
BEGIN

-- Get the threshold for the drive
SET @DriveThreshold = (SELECT Threshold FROM [dbo].[tbl_Local_Drives] (NOLOCK)
WHERE Drive_Letter = @DriveLetter)
Print ''
Print @Driveletter
Print @DriveThreshold
Print ''

-- If the available space on the drive is less than the pre-defined threshold send and email alert
IF @DriveFreeSpace < @DriveThreshold BEGIN
SET @EmailSubject = ''
SET @EmailSubject = @@SERVERNAME
SET @EmailSubject = @EmailSubject + ' Available disk space on local drive '
SET @EmailSubject = @EmailSubject + @DriveLetter
SET @EmailSubject = @EmailSubject + ': is less than threshold.'

SET @EmailMsg = ''
SET @EmailMsg = @@SERVERNAME
SET @EmailMsg = @EmailMsg + ' To avoid a potential production issue check the disk usage on '
SET @EmailMsg = @EmailMsg + @DriveLetter + ':'


EXEC msdb.dbo.sp_send_dbmail
@recipients = @Email_In_Box,
@subject = @EmailSubject,
@body = @EmailMsg
END

-- Now get the next drive
FETCH NEXT FROM DriveCursor INTO
@DriveLetter, @DriveFreeSpace
End

Close DriveCursor
DEALLOCATE DriveCursor

-- Drop the temporary table
DROP TABLE #Local_Disks

END
GO



Monday, February 17, 2020 - 9:03:00 PM - sony Back To Top (84588)

Hi Ken,

Thanks for the link.

I wanna ask your opinion, when we do an insert.. the insert thing the DML didnt use/have to use index right?

and i have a database with 2 million rows and one database with empty table if i do an insert will the insert/execution time will be different?

i mean if those 2 million rows is causing more time than the empty one?

as per my understanding it doesnt matter.

let me hear from you

Regards


Monday, February 17, 2020 - 1:31:58 PM - Greg Robidoux Back To Top (84579)

Hi Sony, you can take a look at this article: https://www.mssqltips.com/sqlservertip/4368/execute-sql-server-dbcc-shrinkfile-without-causing-index-fragmentation/


Monday, February 17, 2020 - 10:53:10 AM - sony Back To Top (84575)

Hi, 

Can you share the script that use to shrink data files?

Regards


Saturday, July 18, 2009 - 9:03:36 PM - carlos.araque Back To Top (3755)

 Hello there!

If you have spaces in the database name i.e Adventure Works, the script doesn´t work.

I've modified the script to solve the problem.

-- **************************************************

USE MASTER

GO

CREATE TABLE #TMPFIXEDDRIVES (
  DRIVE  CHAR(1),
  MBFREE INT)

INSERT INTO #TMPFIXEDDRIVES
EXEC xp_FIXEDDRIVES

CREATE TABLE #TMPSPACEUSED (
  DBNAME    VARCHAR(50),
  FILENME   VARCHAR(50),
  SPACEUSED FLOAT)

INSERT INTO #TMPSPACEUSED
EXEC( 'sp_msforeachdb''use [?]; Select ''''[?]'''' [DBName], Name FileNme, fileproperty(Name,''''SpaceUsed'''') SpaceUsed from sysfiles''')

SELECT   C.DRIVE,
         CASE
           WHEN (C.MBFREE) > 1000 THEN CAST(CAST(((C.MBFREE) / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' GB'
           ELSE CAST(CAST((C.MBFREE) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' MB'
         END AS DISKSPACEFREE,
         A.NAME AS DATABASENAME,
         B.NAME AS FILENAME,
         CASE B.TYPE
           WHEN 0 THEN 'DATA'
           ELSE TYPE_DESC
         END AS FILETYPE,
         CASE
           WHEN (B.SIZE * 8 / 1024.0) > 1000 THEN CAST(CAST(((B.SIZE * 8 / 1024) / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' GB'
           ELSE CAST(CAST((B.SIZE * 8 / 1024.0) AS DECIMAL(18,2)) AS VARCHAR(20)) + ' MB'
         END AS FILESIZE,
         CAST((B.SIZE * 8 / 1024.0) - (D.SPACEUSED / 128.0) AS DECIMAL(15,2)) SPACEFREE,
         B.PHYSICAL_NAME
FROM     SYS.DATABASES A
         JOIN SYS.MASTER_FILES B
           ON A.DATABASE_ID = B.DATABASE_ID
         JOIN #TMPFIXEDDRIVES C
           ON LEFT(B.PHYSICAL_NAME,1) = C.DRIVE
         JOIN #TMPSPACEUSED D
           ON '[' + A.NAME + ']' = D.DBNAME
              AND B.NAME = D.FILENME
ORDER BY DISKSPACEFREE,
         SPACEFREE DESC
        
DROP TABLE #TMPFIXEDDRIVES

DROP TABLE #TMPSPACEUSED

-- **************************************************


Monday, June 2, 2008 - 11:15:03 AM - tosscrosby Back To Top (1065)

Glad you had this little disclaimer in there: "In a perfect world, DBA’s would just be able to ask for more drive space and not have to worry about shrinking files. It is more efficient to give the database the room it needs instead of having to shrink them all the time." I am not a big fan of shrinking files UNLESS you have performed a rare event, such as archiving stale data. Why risk fragmentation and performance? The price to be paid is greater than the rewards of doing so (UNLESS disk space has become critical!!). Proper planning/monitoring can prolong and/or eliminate disk contraint concerns. I snapshot all production databases every week and do comparisons monthy or quarterly, depending on the environment. Allows me to proactively assess my needs and address them before I HAVE to. - But that's just me. ;-)















get free sql tips
agree to terms