05 September 2010   
 Search  
 
Register  Login  
* Data Related .Net Applications » Creating Windows Apps (.Net)  
   Article Details  
Compressing Your Database Backups


When I got a phone call from a vendor trying to sell me compression software for SQL Server backups, I thought '...that's a bloody good idea... but I ain't paying for it."

Side Notes 

 


Image of a note
SQL Server 2008 comes with back up compression. Both SQL Squeeze and compression in SQL 2008 can have an impact on CPU which may be an issue if you are compressing your tlog backups during the day. To see a comparison between the two using input/output performance counters, click here

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 


Image of a note
Initially, I didn't think there was a need to supply an argument for the path to the .ini file, as the file could just be placed in the current directory,  however this was changed for 2 reasons. The first is that you may want to use the same SQL Squeeze executable for several lots of backups, each requiring their own ini file with different parameters. The other reason is that if you choose to use cmdExec to execute SQL Squeeze, the application will think its current directory is the System32 folder. It appears that cmdExec runs SQL Squeeze from this location, even if you have placed the .exe elsewhere.
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Image of a note
Clearly with the transactional log being backed up regularly during the day, it's important that the process doesn't hog all the resources. Whilst the processor will still run high when compressing, by setting the priority in the ini file appropriately, we can ensure that other applications (such as SQL Server) will be given priority and given as much CPU as is needed before SQL Squeeze. Needless to say, compressing of large .bak files are best done out of hours immediately after the database is backed up.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

  

 


Image of a note
Initially, all files got zipped, however during testing, a 10gb file compressed to 1GB took (as with all compression tools) an hour to decompress. In the instance of a rapid restore this is no good, so now by using DurationBeforeZipped we can specify a period of time before files get zipped such a the first backup set during the first 24 hours. I figured this way we get the both of both worlds.
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Image of a note
To see the effect changing the Block Size has on performance counters when using SQL Squeeze,click here 

 


Image of a note
Don't worry if it doesn't all make sense, the whole sub-routine ZipFile can be copied and pasted into your code and then called for compression. Just remember there are four input parameters.
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Image of a note
If you would like the source code to this app, drop me an email and I'd be happy to send it

 

 

 


Image of a note
Please feel free to leave comments on your thoughts and suggestions.


SQL Squeeze - A free tool to compress your backups Image of SQL Squeeze icon

An introduction

I had another phone call from a well known SQL Server application provider trying to sell me SQL Server applications. They always ask how many SQL Servers I manage, followed by a short sucky whistle when I tell them 47. Then they make the hard-sell. Anyway, this time it was backup compression software that was on offer, and I thought actually that could be quite useful. Disk space is mostly cheap now-a-days, but not for high-end systems. Also, we have hundreds of drives for tens of applications and it does add up. Working for local government, it's a nightmare getting more space signed off too and is never instant. I could see the benefits for me as well as the organisation.

I looked at a few solutions available on the web such as that provided by Muthusamy Anantha Kumar. He uses a Winzip Command-Line add-in and calls it using xp_cmdshell. It's an interesting read, but I wasn't too keen on it, as it had no consideration for retaining backups for a specified period. Also it relied on another 3rd party tool that would need purchasing. Really, what I wanted was a neat little package that backed up and then deleted the backup after a set amount of days, just like a regular Maintenance Plan. I also wanted to use something that would work in conjunction with the existing plans. SQL Server 2005 plans use an extended procedure xp_delete_file to delete the backups. However after a little tinkering, it became evident that the proc checks against the master database to see if the backups were genuine, so I wouldn't be able to use it to delete .zip files.

Hmm. At that point, I wondered if there were any freebie .net compression assemblies I could use. There is. SharpZipLib. Together with the Dot Net File Class, creating the above is not difficult.

 

What the app does...

 A console application, it will accept an optional argument, look at a .ini file and take in a number of parameters: 

[DurationBeforeZip]
Integer

[Retention]
Integer 

[Compression]
0 >Integer < 10
[Paths]
String
Days=1
Hours=0
Minutes=0
Days=3
Hours=0
Minutes=0
CompLevel=9
BackupPath="D:\BackUps"

[Extensions]
String
[LogSizeLimit]
Integer
[BlockSize]
Integer
[ProcessPriority]
String
Tran="trn"
Full="bak"
MB=1
BSLevel=9
PriorityLevel="Idle"

It will search the specified folder for any files that end with the specified extensions and if they are older than a specified amount of time, zip them up, and then delete the original files. It will then delete any other .zip files older than the specified retention period. The zipping and deleting is recursive. File names are retained after compression. A log file is written to and it's maximum size can be specified. Click below for a simplified flow chart. 

Click to view a simplified flow chart of the process

 Right. The Code 

There are six main steps during the whole process. Checking for and reading the optional argument, reading the .ini file, deleting the log file if it is bigger than the specified limit, zipping the files, then deleting the appropriate ones and finally printing when the process completed.

    Sub Main()
        'Read Console Argument
        ReadArgument()

        'Read Parameters
        ReadINI()

        'Delete Log
        DeleteLog()

        'Set the priority of the process
        SetPriority()

        'Zip the files recursively
        Zip()

        'Cleanup
        RecursiveDelete()

        'Sign off logfile
        ProcessCompleted()
    End Sub

 

 

The optional argument

The argument comes in the form of a path. This is the path to the ini file. If no argument is specified, then the application will look in the current directory of the executable for the ini file.

 

     Private Sub ReadArgument()
       
'If a path argument is specified then hold on to it
        If My.Application.CommandLineArgs.Count > 0 Then
            PathArg = My.Application.CommandLineArgs(0).ToString
        End If

        If PathArg <> Nothing Then
            currentDir = PathArg
        Else
            'Otherwise hold on to the current directory
            currentDir = Directory.GetCurrentDirectory
        End If

        'Now we know the path, lets construct paths to the log and ini file
        currentLog = currentDir + "\" + "SQLSqueeze_Log.txt"
        currentConfig = currentDir + "\" + "SQLSqueeze_config.ini"
    End Sub

 

 We then read the ini file. Within the actual file, parameters are created in blocks...

 

[Retention]
Days=3
Hours=0
Minutes=0

[DurationBeforeZip]
Days=1
Hours=0
Minutes=0

[Compression]
CompLevel=9

[Paths]
BackupPath="D:\BackUps"

[Extensions]
Tran="trn"
Full="bak"

[LogSizeLimit]
MB=1

[BlockSize]
BSLevel=9

[ProcessPriority]
ProcessLevel="Idle"


and each chunk is read into variables used later.

    Private Sub ReadINI()
        INIsource = New IniConfigSource(currentConfig)
        RetentionDays = INIsource.Configs("Retention").Get("Days")
        RetentionHours = INIsource.Configs("Retention").Get("Hours")
        RetentionMinutes = INIsource.Configs("Retention").Get("Minutes")
        ZipDays = INIsource.Configs("DurationBeforeZip").Get("Days")
        ZipHours = INIsource.Configs("DurationBeforeZip").Get("Hours")
        ZipMinutes = INIsource.Configs("DurationBeforeZip").Get("Minutes")
        Path = INIsource.Configs("Paths").Get("BackupPath")
        CompressionLevel = INIsource.Configs("Compression").Get("Level")
        tranext = INIsource.Configs("Extensions").Get("Tran")
        fullext = INIsource.Configs("Extensions").Get("Full")
        LogSizeLimit = INIsource.Configs("LogSizeLimit").Get("MB")
        BlockSizeLevel = INIsource.Configs("BlockSize").Get("Level")
        ProcessPriority = INIsource.Configs("ProcessPriority").Get("ProcessLevel").ToUpper
    End Sub

 

Checking the size of the log file

The log file will be written to and time-stamped every time a file is compressed or deleted. I thought this was important as if you are dealing with hundreds of files and something goes wrong, you want to know what has been processed successfully. The file will also be written to if an exception occurs. So, potentially, the log can grow quite a bit in size, and will need to be deleted if its size exceeds that specified by us in the .ini file.

     Private Sub DeleteLog()
        'If the log file already exists, check the size of it
        If System.IO.File.Exists(currentLog) Then
            Dim LogFile As New FileInfo(currentLog)
            Dim ActualLogSize As Integer = LogFile.Length

            'If the size is bigger than the limit specified, delete it
            If (ActualLogSize / 1000000) > LogSizeLimit Then
                System.IO.File.Delete(currentLog)
            End If
        End If
    End Sub

 

 

Setting the priority of the SQL Squeeze process

    Private Sub SetPriority()
        Select Case ProcessPriority
            Case "IDLE"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.Idle
            Case "BELOWNORMAL"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.BelowNormal
            Case "NORMAL"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.Normal
            Case "ABOVENORMAL"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.AboveNormal
            Case "HIGH"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.High
            Case "REALTIME"
                System.Diagnostics.Process.GetCurrentProcess.PriorityClass = ProcessPriorityClass.RealTime
        End Select
    End Sub

 

Compressing the files 

The compression is split up in to sub-stages, as commented below in the code.

    Public Sub Zip()
        'Search and hold on to all the directories in the backup folder
        astrDirNames = Directory.GetDirectories(Path, "*.*", SearchOption.AllDirectories)

        'Make sure the structure of the directories are correct (see below)...
        ValidateFolders()

        'Adjust the path so that it points to each directory recursively.
        For Each strDir In astrDirNames
            Path = strDir + "\"

            'Go here for further instructions (see below)...
            RecursiveZip()
        Next
    End Sub

 

So as not to miss any files, the directories must be structured as a folder within another folder for example:

 

Image of the correct directory structure

 

The following checks for this and exits with a message written to the log if this is not the case... 

 Private Sub ValidateFolders()

        'The actual files must be in a folder within a folder, otherwise exit
        If astrDirNames.Length = 0 Then
            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
            + "Error: Backup files cannot be placed in a 1st level folder but must be " + _
            "placed in a folder within a root folder such as 'Backup_Folder'..." _
            + Environment.NewLine + "No files have been processed", True)
            End
        End If

        'If there are tlog files incorrectly placed in the first level folder, exit
        If Directory.GetFiles(Path, "*." + tranext).Length > 0 Then
            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
            + "Error: Backup files cannot be placed in a 1st level folder but must be " + _
            "placed in a folder within a root folder such as 'Backup_Folder'..." _
            + Environment.NewLine + "No files have been processed", True)

            For Each strFile In Directory.GetFiles(Path, "*." + tranext)
                My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
                + "Offending file: " + strFile, True)
            Next
            End
        End If

        'As above but for the full back up files
        If Directory.GetFiles(Path, "*." + fullext).Length > 0 Then
            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
            + "Error: Backup files cannot be placed in a 1st level folder but must be " + _
            "placed in a folder within a root folder such as 'Backup_Folder'..." _
            + Environment.NewLine + "No files have been processed", True)

            For Each strFile In Directory.GetFiles(Path, "*." + fullext)
                My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
                + "Offending file: " + strFile, True)
            Next
            End
        End If
    End Sub

 

All files are compressed by calling "ZipFile" (as provided with the SharpZipLib libraries mentioned at the beginning of this article) recursively.

     Public Sub RecursiveZip()
        Try
            'Retreive the list of tran files at the path specified in the ini file
            astrFileNames = Directory.GetFiles(Path, "*." + tranext)

            'Extract the filename from the path
            For Each strFile In astrFileNames

                Dim dFile As New FileInfo(strFile)

                'If the file was created after the DurationBeforeZip parameter then zip it
                If dFile.CreationTime < DateTime.UtcNow.AddDays(-Zipdays).AddHours _
                                    (-ZipHours).AddMinutes(-ZipMinutes) Then

                    strFileName = strFile.Substring(strFile.LastIndexOf("\") + 1)

                    'Compress the file and give it an extention of zip
                    'Parameters are drawn from the ini file
                    ZipFile(Path + strFileName, Path + strFileName + ".zip", CompressionLevel, BlockSizeLevel)

                    'Then write the accomplished to the log file
                    My.Computer.FileSystem.WriteAllText(currentLog, _
                    Environment.NewLine + Path + strFileName + " compressed successfully on " _
                    + DateTime.UtcNow, True)
                End If
            Next

        Catch ex As Exception
            My.Application.Log.WriteException(ex, TraceEventType.Error, _
                Environment.NewLine + "An exception has occured when compressing file ")

            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
           + ex.ToString + ": " + strFileName + Environment.NewLine, True)
        End Try
        '----------

        Try
            'As above but for full backup files eg. bak
            astrFileNames = Directory.GetFiles(Path, "*." + fullext)

            'Extract the filename from the path
            For Each strFile In astrFileNames

                Dim dFile As New FileInfo(strFile)

                'If the file was created after the DurationBeforeZip parameter then zip it
                If dFile.CreationTime < DateTime.UtcNow.AddDays(-Zipdays).AddHours _
                                    (-ZipHours).AddMinutes(-ZipMinutes) Then

                    strFileName = strFile.Substring(strFile.LastIndexOf("\") + 1)

                    ZipFile(Path + strFileName, Path + strFileName + ".zip", CompressionLevel, BlockSizeLevel)

                    My.Computer.FileSystem.WriteAllText(currentLog, _
                    Environment.NewLine + Path + strFileName + _
                    " compressed successfully on " + DateTime.UtcNow, True)
                End If
            Next

        Catch ex As Exception
            My.Application.Log.WriteException(ex, _
                TraceEventType.Error, _
                 Environment.NewLine + "An exception has occured when compressing file ")

            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
           + ex.ToString + ": " + strFileName + Environment.NewLine, True)
        End Try
    End Sub

 

The following is the call (ZipFile) to the code that talks to the compression libraries in the above routine and does the actual zipping up. There are four input parameters, the path to the file, what the file will be called after it is zipped, the compression level from 0 to 9 with the higher end being a higher compression rate, and finally the Blocksize, the number of bytes compressed at a time (the block size affects both the compression ratio achieved, and the amount of memory needed for compression and decompression). BlockSize 1 through BlockSize 9 specifies the block size to be 100,000 bytes through 900,000 bytes respectively.

 

ZipFile(Path + strFileName, Path + strFileName + ".zip", CompressionLevel, BlockSize)

 

    'Zips the file
    'Check to see if the file exists. Remember we are still usig our variables that hold
    'paths to the files
    
    Public Sub ZipFile(ByVal strFileToZip As String, ByVal strZippedFile As String, _
    ByVal nCompressionLevel As Integer, ByVal nBlockSize As Integer)

        If (Not System.IO.File.Exists(strFileToZip)) Then
            Throw New System.IO.FileNotFoundException("The specified file " + _
            strFileToZip + "could not be found. Zipping aborted.")
        End If

        'Prep the file
        Dim strmStreamToZip As System.IO.FileStream
        strmStreamToZip = New System.IO.FileStream(strFileToZip, System.IO.FileMode.Open,         System.IO.FileAccess.Read)

        Dim strmZipFile As System.IO.FileStream
        strmZipFile = System.IO.File.Create(strZippedFile)

        Dim strmZipStream As ZipOutputStream
        strmZipStream = New ZipOutputStream(strmZipFile)

        Dim myZipEntry As ZipEntry

        'The name of the file (strFileName)...
        myZipEntry = New ZipEntry(strFileName)
        strmZipStream.PutNextEntry(myZipEntry)
        strmZipStream.SetLevel(nCompressionLevel)

        Dim abyBuffer(nBlockSize) As Byte
        Dim nSize As System.Int32
        nSize = strmStreamToZip.Read(abyBuffer, 0, abyBuffer.Length)
        strmZipStream.Write(abyBuffer, 0, nSize)

        Try
            While (nSize < strmStreamToZip.Length)
                Dim nSizeRead As Integer
                nSizeRead = strmStreamToZip.Read(abyBuffer, 0, abyBuffer.Length)
                strmZipStream.Write(abyBuffer, 0, nSizeRead)
                nSize = nSize + nSizeRead
            End While

        Catch Ex As System.Exception
            Throw Ex

        End Try

        strmZipStream.Finish()
        strmZipStream.Close()
        strmStreamToZip.Close()
    End Sub 

 

So now the files in each directory are compressed. The directory now holds newly created zip files, the original back up and tlog files, and possibly zip files from before. We now need to delete files that were zipped beyond the retention period specified in the .ini file.Then we delete all files ending in the backup and tlog extensions specified in the .ini file.

 

     Private Sub RecursiveDelete()
        
    'For each directory in the top directory
        For Each strDir2 In astrDirNames
            Path = strDir2 + "\"

            'Then go here to delete the files
            DeleteFiles()
        Next
    End Sub

 

     Private Function DeleteFiles() As Integer
        Try
            'For each zip file in the backup directory
            For Each strFile2 In Directory.GetFiles(Path, "*.zip")

                'We will want an attribute of that file
                Dim dDir As New FileInfo(strFile2)

                'If the creation time is greater than that specified in the ini file...
                If dDir.CreationTime < DateTime.UtcNow.AddDays(-RetentionDays).AddHours _
                (-RetentionHours).AddMinutes(-RetentionMinutes) Then

                    Delete(strFile2)

                    My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
                    + strFile2 + " deleted successfully on " + DateTime.UtcNow, True)
                End If
            Next

        Catch ex As Exception
            My.Application.Log.WriteException(ex, _
                TraceEventType.Error, _
                 Environment.NewLine + "An exception has occured when deleting file ")

            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
         + ex.ToString + ": " + strFile2 + Environment.NewLine, True)
        End Try

        '-----------------------------------------

        Try
            'Delete uncompressed backup files older than the DurationBeforeZip parameter
            For Each strFile In Directory.GetFiles(Path, "*." + fullext)

                Dim dFile As New FileInfo(strFile)

                If dFile.CreationTime < DateTime.UtcNow.AddDays(-Zipdays).AddHours _
                                    (-ZipHours).AddMinutes(-ZipMinutes) Then

                    Delete(strFile)

                    My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine + _
                    strFile + " deleted successfully on " + DateTime.UtcNow, True)

                End If
            Next

        Catch ex As Exception
            My.Application.Log.WriteException(ex, _
                TraceEventType.Error, _
                  Environment.NewLine + "An exception has occured when deletng file ")

            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
         + ex.ToString + ": " + strFile + Environment.NewLine, True)
        End Try


        '--------------------------------------

        Try
            'Regardless of creation time, delete all tran files
            For Each strFile In Directory.GetFiles(Path, "*." + tranext)

                Dim dFile As New FileInfo(strFile)

                'Delete uncompressed backup files older than the DurationBeforeZip parameter
                If dFile.CreationTime < DateTime.UtcNow.AddDays(-Zipdays).AddHours _
                                    (-ZipHours).AddMinutes(-ZipMinutes) Then

                    Delete(strFile)

                    My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine + _
                    strFile + " deleted successfully on " + DateTime.UtcNow, True)
                End If
            Next

        Catch ex As Exception
            My.Application.Log.WriteException(ex, _
                TraceEventType.Error, _
                  Environment.NewLine + "An exception has occured when deleting file ")

            My.Computer.FileSystem.WriteAllText(currentLog, Environment.NewLine _
         + ex.ToString + ": " + strFile + Environment.NewLine, True)
        End Try
    End Function

 

Finally, we sign off the log file.

     Private Sub ProcessCompleted()
        My.Computer.FileSystem.WriteAllText(currentLog, _
          Environment.NewLine + "------------------------------------------------------------" _
        + Environment.NewLine + "Process Completed at: " + DateTime.UtcNow _
        + Environment.NewLine + "SQL Squeeze. Developed by Drew Salem - www.thebuddingdba.com" _
        + Environment.NewLine + "------------------------------------------------------------" _
        + Environment.NewLine, True)
   
End Sub

 

And there you have it. A tool that will compress your backup files, and delete old ones as specified by you.

If you would like to download SQL Squeeeeeze and read suggestions on how to use it, you can do by clicking here.

 

Click to go to the SQL Squeeze download page

 


 

 

Disclaimer:

The information in this website including this weblog is provided "AS IS" with no warranties, and confers no rights. This weblog does not represent the thoughts, intentions, plans or strategies of my employer. It is solely my own personal opinion. All code samples (if any, ever) and downloads are provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.  

You must be a registered subscriber in order to view this Article.
To learn more about becoming a subscriber, please visit our Subscription Services page.

Written By: Drew Salem
Date Posted: 03/03/2008
Number of Views: 593

Return
An error has occurred.
Error: Unable to load the Article Details page.