SQLServerCentral Article

Managing Free Space

,

Managing Free Space can become a problem quickly, especially when you have to manage multiple database servers, in multiple environments, each server with multiple Hard Drives / LUNs. Some companies spend money on monitoring tools like SCOM, others try to manage it in-house by developing their own software/processes/reports to show free space per volume per.

You could quickly determine Free Space per HDD/LUN by executing this tsql command, but it doesn’t tell you the TOTAL space per HDD/LUN: xp_fixeddrives. It will give you the free space in MB per drive.

xp_fixeddrives

When I connect to a server to free up space, the first thing I do is open explorer, click on My Computer and arrange the columns in this order by dragging the columns around:

1) Name

2) Free Space

3) Total Size

This makes it easier to read. See the diagram below to get some idea. You can ask explorer to sort according to a column by clicking on the column, but sadly it doesn’t handle it well/correctly.

Warning: When checking space, be very careful not to overlook MB and think it is GB.

Drive listing

Space saving techniques:

- Find what is no longer used/needed and delete/archive it. E.g.

- Clean out C:\Temp and C:\Windows\Temp. Windows won’t let you delete the temp files that are currently in use. Delete the blue $...$ files in the Windows folder. Drawback is you won’t be able to rollback any Windows Updates that was applied. I would leave this to a last resort for the really desperate.

Windows patch folders

- Find out which sql backup files (.BAK/.UBAK) are lying around and delete/archive the ones that are no longer needed.

- Find out which detached database files are lying around and delete/archive them

- Shrink each of the files per database to reclaim the allocated free space. Jonathan Kehayias wrote a pretty good article called “Monitor free space in the database files” to achieve just that. You can find it at: http://code.msdn.microsoft.com/SQLExamples/Wiki/View.aspx?title=FileSpaceMon

- Determine the space allocation and usage per table and then dealing with that. E.g. Dropping/Archiving tables that are not needed any longer.

This article focuses on finding (a) detached db files and (b) old backup files.

To achieve this I mainly use the DOS DIR command that searches for the .MDF, .NDF, .LDF, .BAK and .uBAK files. Then I compare the list of database files (.MDF, .NDF, .LDF) that were found against sysaltfiles to see which of these files aren’t in use by the instance. If you’re awake you’ll realize that this is only suitable for servers that has only 1 sql instance, unless you run the code against each sql instance. This is because when you run the code, it is like saying "WHICH FILES HAS THE DISKS GOT THAT ISN'T IN A PARTICULAR SQL INSTANCE". Ideally we would like to say: "WHICH FILES HAS THE DISKS GOT THAT AREN'T USED BY ANY SQL INSTANCE ON THIS SERVER". Still it is very handy on single instance servers.

Enough talk - let’s get to the good stuff. This code is compatible with SS2000, 2005 and 2008.

There is 1 pre-requisite; your SQL Server must allow DOS commands to run. So if you have prevented this, you’ll have to re-enable this functionality for the script to run. I do a quick check at the beginning of the script to determine if the pre-requisite is enabled.

The script is broken into 3 Sections listed below:

  1. Gather data into temp tables
  2. Reports that pulls out what we are interested in. I created 4 reports for you. Look at the examples further on:
    • REPORT 1: POTENTIAL SPACE SAVING (SUMMARY) PER VOLUME
    • REPORT 2: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) PER DRIVE/VOLUME
    • REPORT 3: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) DESC BY WASTAGE
    • REPORT 4: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR .BAK/.UBAK FILES), FROM BIGGEST TO SMALLEST
  3. Discard temp tables

Procedure to get this working: Run the code in Section 1. Then run each report in turn in Section 2. Run Section 3 to do the cleanups. Should you want to discuss anything RE this, you can contact me on: paul_els@hotmail.com

Script Section 1 – Gathering data needed for our reports

-- OBTAIN A LIST OF ALL THE DRIVES ON THE SERVER
set nocount on
CREATE TABLE #Drives (DriveLetter char(1), MBFree int)
INSERT INTO #Drives
EXEC master..xp_fixeddrives
-- DECLARE VARIABLES
DECLARE @CurrentDriveLetter CHAR(1), @MaxDriveLetter CHAR(1), @EXECSTR varchar(1024)
-- FIND THE FIRST AND LAST DRIVES FOR THE LOOP
SELECT @CurrentDriveLetter = Min(DriveLetter), @MaxDriveLetter = Max(DriveLetter) from #Drives
-- CREATE THE TABLE TO HOST THE LIST OF FILES
CREATE TABLE #Files (
  autono_id int NOT NULL IDENTITY (1, 1), 
  RawData varchar(255), 
  FilePath varchar(255), 
  DriveLetter CHAR(1), 
  [FileName] varchar(255), 
  FileSize varchar(17), 
  FileSizeInMB decimal(18,2), 
  FileSizeInGB decimal(18,2)
  )
WHILE @CurrentDriveLetter <= @MaxDriveLetter
BEGIN
    -- STORE THE FILES WE ARE LOOKING FOR IN THE #FILES TABLE
    -- PRINT STR('dir ' + STR(@CurrentDriveLetter) + ':\*.mdf;*.ndf;*.ldf /s')
      SELECT @EXECSTR = 'dir ' + CONVERT(VARCHAR(1),@CurrentDriveLetter) + ':\*.mdf;' + CONVERT(VARCHAR(1),@CurrentDriveLetter) + ':\*.ndf;' + 
  CONVERT(VARCHAR(1),@CurrentDriveLetter) + ':\*.ldf;' + CONVERT(VARCHAR(1),@CurrentDriveLetter) + ':\*.ubak;' + 
  CONVERT(VARCHAR(1),@CurrentDriveLetter) + ':\*.BAK /s' -- string in the drive letter later
      INSERT INTO #Files (RawData)
      EXEC master..xp_cmdshell @EXECSTR
    -- PRINT @EXECSTR
      select @CurrentDriveLetter = MIN(DriveLetter) from #Drives where DriveLetter > @CurrentDriveLetter
END
-- CLEAN UP #FILES
update #Files
 set FilePath = REPLACE(RawData,'Directory of ','')
 where RawData like '%Directory of %:%'
update #Files
   set FilePath = SubString(FilePath, 2, 255)
 where FilePath is not null
delete from #Files
 where RawData is NULL
      or RawData = 'File Not Found'
      or RawData like '%Volume%'
      or RawData like '%File(s)%'
      or RawData like '%Dir(s)%'
      or RawData like '%Total Files Listed:%'
update #Files set [FileName] = substring (RawData, 40, 255) where FilePath is NULL
update #Files set FileSize = substring (RawData, 22, 17) where FilePath is NULL
update #Files set FileSize = replace(substring (RawData, 22, 17),',','') where FilePath is NULL
update #Files set DriveLetter = substring(FilePath, 1, 1) where FilePath is not NULL
update #Files
   set FileSizeInMB = CONVERT(decimal(18,2), FileSize) / 1024 / 1024,
       FileSizeInGB = CONVERT(decimal(18,2), FileSize) / 1024 / 1024 / 1024
DECLARE @autono_id int, @fp varchar(255), @drive char(1)
select top 1 @autono_id = autono_id, @fp = [FilePath], @drive = DriveLetter
 from #files F1
 where FilePath is not null
 and autono_id < (select max(autono_id) from #Files where FilePath is NULL)
 order by autono_id desc
WHILE @autono_id IS NOT NULL
BEGIN
      update #Files
         set [FilePath] = @fp, DriveLetter = @Drive
       where autono_id > @autono_id and [FilePath] is NULL
      DELETE from #Files where [FileName] is null AND DriveLetter = @Drive AND autono_id > @autono_id
      SELECT @autono_id = NULL, @fp = NULL, @drive = NULL -- RESET FLAGS
      select top 1 @autono_id = autono_id, @fp = [FilePath], @drive = DriveLetter
        from #files F1
       where FilePath is not null
         and autono_id < (select max(autono_id) from #Files where FilePath is NULL)
       order by autono_id desc
END
delete from #Files where FileName is NULL or FilePath like '%i386%' or FilePath like '%ia64%'

Script Section 2 – Run each report respectively

-- REPORT 1: POTENTIAL SPACE SAVING (SUMMARY) PER VOLUME
select 'DriveLetter' = Ltrim(rtrim(LEFT(DriveLetter,1))),
       'Potential Saving (in MB)' = LTRIM(STR(SUM(FileSizeInMB))),
       'Potential Saving (in GB)' = LTRIM(STR(SUM(FileSizeInGB)))
  from #Files
 where REPLACE(FilePath + '\' + [FileName], ':\\', ':\') not in (select Ltrim(rtrim(filename)) from master.dbo.sysaltfiles)
 group by Ltrim(rtrim(LEFT(DriveLetter,1)))
 order by Ltrim(rtrim(LEFT(DriveLetter,1)))
-- REPORT 2: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) PER DRIVE/VOLUME
select 'Drive' = Ltrim(rtrim(LEFT(DriveLetter,1))),
       'FileName' = REPLACE(FilePath + '\' + [FileName], ':\\', ':\'), FileSizeInMB, FileSizeInGB
  from #Files
 where REPLACE(FilePath + '\' + [FileName], ':\\', ':\') not in (select Ltrim(rtrim(filename)) from master.dbo.sysaltfiles)
   and right(FileName,3) <> 'bak' -- EXCLUDE .BAK/.UBAK FILES
   and FileSizeInMB > 0 -- YOU CAN STIPULATE YOU ARE LOOKING FOR FILES LARGER THAN X MB
 order by Ltrim(rtrim(LEFT(DriveLetter,1))) asc, 3 desc
-- REPORT 3: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) DESC BY WASTAGE
select 'Drive' = Ltrim(rtrim(LEFT(DriveLetter,1))),
       'FileName' = REPLACE(FilePath + '\' + [FileName], ':\\', ':\'), FileSizeInMB, FileSizeInGB
  from #Files
 where REPLACE(FilePath + '\' + [FileName], ':\\', ':\') not in (select Ltrim(rtrim(filename)) from master.dbo.sysaltfiles)
   and right(FileName,3) <> 'bak' -- EXCLUDE .BAK/.UBAK FILES
   and FileSizeInMB > 0 -- YOU CAN STIPULATE YOU ARE LOOKING FOR FILES LARGER THAN X MB
 order by 3 desc
-- REPORT 4: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR .BAK/.UBAK FILES), FROM BIGGEST TO SMALLEST
select DriveLetter, 'FileName' = REPLACE(FilePath + '\' + [FileName], ':\\', ':\'), FileSizeInMB, FileSizeInGB
  from #Files
 where FilePath + '\' + FileName not in (select filename from master.dbo.sysaltfiles)
   and right(FileName,3) = 'bak'
   and FileSizeInMB > 0 -- YOU CAN STIPULATE YOU ARE LOOKING FOR FILES LARGER THAN X MB
 order by FileSizeInMB desc, REPLACE(FilePath + '\' + [FileName], ':\\', ':\') asc

Script Section 3 – Cleanup/Discard the temp tables

drop table #Files
drop table #Drives

Examples of the output of each report:

REPORT 1: POTENTIAL SPACE SAVING (SUMMARY) PER VOLUME

drive summary report

· REPORT 2: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) PER DRIVE/VOLUME

potential free space

Notice:

o It is sorted by Drive, by FileSizeInMB desc.

o Not everything that is listed can be deleted - use your own discretion.

· REPORT 3: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR DATABASE FILES) DESC BY WASTAGE

potential saving report by wastage

Notice: It is virtually the same report as Report 2, except that the sort order is from most to least.

REPORT 4: POTENTIAL SPACE SAVING (DETAILED) REPORT (FOR .BAK/.UBAK FILES), FROM BIGGEST TO SMALLEST

potential saving by size

Summary

I built this script to help me to quickly identify where old sql backup files and detached db files are lying around in order to save space by getting rid of them. It is very effective at achieving this goal. There is no excuse for not setting up maintenance jobs, but in a multi DBA environment someone else may have left files lying around. The code can be extended to search for other file extensions quite easily. Further you can even precede the code with code to determine if the pre-requisite is met, e.g. are DOS commands are allowed by SQL? The code below is a step in that direction:

DECLARE @sqlversion sql_variant
SELECT @sqlversion = SERVERPROPERTY('productversion')
IF LEFT(CONVERT(VARCHAR(255),@SQLVERSION),2) <> '8.'
  BEGIN
    -- START CHECKING SP_CONFIGURE FOR XP_CMDSHELL OPTION  --
                CREATE TABLE #xp_cmdshell (OptionName varchar(255), minval int, maxval int, configval int, runval int)
                INSERT INTO #xp_cmdshell
                EXEC master..sp_configure
                declare @runval int
                select @runval = runval from #xp_cmdshell where OptionName = 'xp_cmdshell'
                drop table #xp_cmdshell
                if @runval is null
                 begin
                                RAISERROR ('enable "show advanced options" before you run this code', -- Message text.
                                                  10, -- Severity,
                                                  16 -- State,
                                                  )
                 end     
                if @runval = 1
                 begin
                                print '' -- The pre-requisites are enabled, so we can continue
                 end
                else -- IF xp_cmdshell is NOT enabled
                 begin
                                RAISERROR ('enable xp_cmdshell before you run this code', -- Message text.
                                                  10, -- Severity,
                                                  16 -- State,
                                                  )
                 end
    -- FINISH CHECKING SP_CONFIGURE FOR XP_CMDSHELL OPTION --
  END

Rate

3.41 (17)

You rated this post out of 5. Change rating

Share

Share

Rate

3.41 (17)

You rated this post out of 5. Change rating