If your like me you have been frustrated by finding stray mdf, ldf, or ndf files on your SQL server from databases deleted long ago that should have been cleaned up but someone forgot.
To permanently solve this problem I have written a couple of PowerShell scripts and configured a scheduled task to send an email to our DBA group if we ever have SQL database files (mdf, ldf, ndf) sitting on any local drive attached to an SQL server that are not currently being used by a database on that SQL server.
First up we a simple helper function that allows us to run SQL statements from anywhere .net is installed without SQL specific utilities installed:
function Invoke-SQL { param( [string] $dataSource = ".\SQLEXPRESS", [string] $database = "MasterData", [string] $sqlCommand = $(throw "Please specify a query.") ) $connectionString = "Data Source=$dataSource; " + "Integrated Security=SSPI; " + "Initial Catalog=$database" $connection = new-object system.data.SqlClient.SQLConnection($connectionString) $command = new-object system.data.sqlclient.sqlcommand($sqlCommand,$connection) $connection.Open() $adapter = New-Object System.Data.sqlclient.sqlDataAdapter $command $dataset = New-Object System.Data.DataSet $adapter.Fill($dataSet) | Out-Null $connection.Close() $dataSet.Tables }
Next up we have have Get-SQLDatabaseFilesNotUsed:
function Get-SQLDatabaseFilesNotUsed { param ( [parameter(Mandatory = $true)][string]$SQLServerName, [switch]$IncludeEqual ) $SQLCommand = @" set nocount on SELECT DB_NAME([database_id])AS [Database Name], [file_id], name, physical_name, type_desc, state_desc, CONVERT( bigint, size/128.0) AS [Total Size in MB] FROM sys.master_files WITH (NOLOCK) ORDER BY DB_NAME([database_id]) OPTION (RECOMPILE); "@ $Results = Invoke-Sql -datasource $SQLServerName -database master -SQLCommand $SQLCommand $SQLDatabaseFilesInUse = $Results | select @{Name="fullname";Expression={$_.physical_Name -replace "\\\\","\" <#SQL server allows \\ in paths and just uses it like \#> }} $SQLDatabaseAndLogFilesOnDisk = invoke-command -ComputerName $SQLServerName -ScriptBlock { $Filesystems = get-psdrive -PSProvider FileSystem Foreach ($FileSystem in $Filesystems) { Get-ChildItem $FileSystem.Root -Recurse -include *.mdf,*.ldf,*.ndf -File -ErrorAction SilentlyContinue | select fullname } } $SQLDatabaseAndLogFilesOnDisk = $SQLDatabaseAndLogFilesOnDisk | Where fullname -notlike "C:\windows\winsxs*" | Where fullname -notlike "C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\Binn\*" | select fullname Compare-Object -ReferenceObject $SQLDatabaseFilesInUse -DifferenceObject $SQLDatabaseAndLogFilesOnDisk -Property FullName -IncludeEqual:$IncludeEqual | FT -AutoSize }
One thing to note, if you have additional database files that you want to keep on disk and exclude from this check just add another “Where fullname –notlike “<Path where you have files you don’t want checked>” |” to the others you see that filter out some files that share the same extensions as SQL database files even though they are not databases.
Lastly we have a script file Test-SQLDatabaseFilesNotUsed.ps1 that will want to call from your scheduled task:
. c:\scripts\Invoke-SQL.ps1 . c:\scripts\Get-SQLDatabaseFilesNotUsed.ps1 $OutputMessage = "" $FromAddress = "scheduledtasks@tervis.com" $ToAddress = "WindowsServerApplicationsAdministrator@tervis.com" $Subject = "SQL Server has data or log files that are not being used" $SMTPServer = "cudaspam.tervis.com" $OutputMessage = Get-SQLDatabaseFilesNotUsed -SQLServerName sql if ($OutputMessage) { Send-MailMessage -From $FromAddress -to $ToAddress -subject $Subject -SmtpServer $SMTPServer -Body ($OutputMessage | FT -autosize | out-string -Width 200) }
Now save all of these in a C:\scripts directory and create a scheduled task with whatever schedule you like (I have ours run once per week) that executes:
Powershell.exe -noprofile -file c:\scripts\Test-SQLDatabaseFilesNotUsed.ps1
This has saved me a +100GB already and from this point forward you won’t have to worry if you have space being used up but the database files that time forgot.