Backup SQL Server Agent Jobs: PowerShell Automation Tips for SQL Server

Backup SQL Server Agent Jobs: PowerShell Automation Tips for SQL Server

SQL Server Agent jobs are an essential component of managing and automating various database tasks. Whether you’re scheduling backups, running maintenance plans, or executing custom scripts, these jobs are crucial for keeping your SQL Server environment running smoothly. But what happens when you need to back up and recover these jobs? Automating the process of scripting out SQL Server Agent jobs ensures you always have a backup of these critical assets. In this blog post, we’ll explore how to use the SqlServer PowerShell module to automate this process.

Now obviously, since SQL Server stores the Agent jobs in msdb, if you want to be able to include SQL Server Agent jobs in a full restore of a SQL Server then it is crucial that you are including msdb in your regular database backup routines. But let’s say that you also wanted to automate a process of scripting out all of the SQL Server Agent jobs on a SQL Server. There are a number of reasons that you may want to do this, some of which include:

  • Backup and recovery: Safeguard your jobs in case of accidental deletion or server failure
  • Migration: Easily transfer jobs to another SQL Server instance
  • Version Control: Keep track of changes made to your jobs over time

While SQL Server Management Studio (SSMS) allows you to manually script out jobs, automating the process with PowerShell is faster, repeatable, and less error-prone.

Prerequisites

To follow along, ensure the following:

  • PowerShell is installed on your machine.
  • The SqlServer PowerShell module is installed. If not, you can install it by running:
Install-Module -Name SqlServer -Scope CurrentUser
  • You have sufficient permissions to access SQL Server Agent and script out jobs.

Import the SqlServer module and create a SMO object:

Once you have validated that the SqlServer module is installed you can create a new SMO (SQL Management Object) connection to SQL Server by executing the following command in PowerShell:

Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server "YourSQLServerName"

Non-Standard port: If your SQL Server runs on a non-standard port then you can follow the server name by the port number separated by a comma. Like below:

Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server "YourSQLServerName,1234"

How let’s list out the jobs:

Once you have created the SMO object to your SQL Server you can list out the jobs that are on the server by running the following command:

$srv.JobServer.Jobs

And from here you can script each job by executing the following command:

$srv.JobServer.Jobs | foreach{
    $_.Script() | Out-File "C:\temp\AllMyJobs.sql" -Append  # put in the path here 
}

However…

It may not be very helpful to have everything all mashed into one big file like a SQL Server Agent stew (probably wouldn’t taste very good anyway!). So instead let’s put everything together and add some magic so that we can loop through each job and create a separate script file for each job.

Here is the script that puts it all together and scripts out each SQL Server Agent job to an individually named SQL script file:

clear-host
#------------------------------------------------------------------------------------------
# The following only needs to be run if the SqlServer module has not been installd yet:
# Install-Module -Name SqlServer -AllowClobber -Scope CurrentUser
#------------------------------------------------------------------------------------------

#------------------------------------------------------------------------------------------
# Define the SQL Server name that you want to script out Agent Jobs for.
# If the server is running on a non-standard port then include the port number separated by a comma.
# For example: "ServerName,1234"
#------------------------------------------------------------------------------------------
$serverName = "ServerName"


#------------------------------------------------------------------------------------------
# Define the base folder where the backup scripts will be stored
# If this path does not exist then the script will attempt to create it.
# ** The final output folder will include a subfolder of this path named for the date and time
# with an additional subfolder named "SqlServerAgentJobs"
# For example, something like this: C:\Temp\ScriptedSQLObjects\2025-01-07_135322\SqlServerAgentJobs\
#------------------------------------------------------------------------------------------
[string]$basePath = "C:\Temp\ScriptedSQLObjects\"    




#------------------------------------------------------------------------------------------
# Import the SQL Server module. 
# ** Remember: this line of code will fail if the SqlServer module has not been installed.
# See the line at the top of the script for syntax for installeing the module
#------------------------------------------------------------------------------------------
Import-Module SqlServer


#------------------------------------------------------------------------------------------
# Create a new SMO connection to the SQL Server
#------------------------------------------------------------------------------------------
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server $serverName

           

#------------------------------------------------------------------------------------------
# Define the final output folder path and attempt to create final output folder
#------------------------------------------------------------------------------------------
[string]$fileSuffix = (Get-Date -Format "yyyy-MM-dd_HHmmss").ToString().Trim()
[string]$outputFolder = [System.IO.Path]::Combine($basePath, $fileSuffix)

$objectFolderPath = [System.IO.Path]::Combine($outputFolder, "SqlServerAgentJobs")
if(!([System.IO.Directory]::Exists($objectFolderPath)))
{
    write-host "Creating directory path $objectFolderPath" -ForegroundColor Cyan
    [System.IO.Directory]::CreateDirectory($objectFolderPath) | out-null
}
write-host "Scripting SQL Server Agent Jobs to the following folder: $objectFolderPath" -ForegroundColor Cyan



#------------------------------------------------------------------------------------------
# Now cycle through the jobs and script out each job
#------------------------------------------------------------------------------------------
[int]$jobCnt = 0

$jobs = $srv.JobServer.Jobs
foreach($job in $jobs)
{
    [string]$objName = $job.Name.ToString().Trim()
    
    [string]$fileName = "Jobs_" + $objName + ".sql"
    $fileName = $fileName.Replace("\","").Replace("/", "").Replace(":", "").Replace(";", "").Replace("'", "").Replace("*", "").Replace("<", "").Replace(">", "").Replace("?", "").Replace("!", "").Replace("$", "").Replace("+", "").Replace("]", "").Replace("[", "")
    [string]$fullPath = join-path $objectFolderPath $fileName

    [string]$separator = "--******************************************************************************`r`n-- Job Name: " + $job.Name.ToString().Trim() + "`r`n-- Job Category: " + $job.Category.ToString().Trim() + "`r`n--******************************************************************************`r`n"
    $separator  | Out-File $fullPath -Append
    $job.Script() | Out-File $fullPath -Append
    "`r`n go `r`n"  | Out-File $fullPath -Append
    $jobCnt++
}

Write-Host "Total Agent Jobs scripted for $serverName is $jobCnt" -ForegroundColor Cyan

Monitoring SQL Server Drive Usage: Three Queries to Get the Job Done (mostly)

Monitoring SQL Server Drive Usage: Three Queries to Get the Job Done (mostly)

Understanding the drive space usage on your SQL Server is essential for maintaining optimal performance and preventing storage-related issues. SQL Server offers several ways to monitor drive usage directly from a query. In this post, we’ll explore three different approaches, each with its unique use case and level of detail.

1. Querying Drive Space Using sys.dm_os_volume_stats

The sys.dm_os_volume_stats dynamic management function is a reliable way to retrieve detailed drive usage statistics for volumes associated with SQL Server database files. This method is particularly useful for DBAs who want a comprehensive view of drive usage. One caveat though: sys.dm_os_volume_status is only going to return information for drives associated with SQL Server database files which, for most SQL Servers, isn’t going to include your operating system drive.

Here’s the query:

select distinct
  a.volume_mount_point
, convert(decimal(20,2), (((a.total_bytes / 1024.0) / 1024.0) / 1024.0)) as [DriveTotalSpace_GB]
, convert(decimal(20,2), (((a.available_bytes / 1024.0) / 1024.0) / 1024.0)) as [DriveAvailableSpace_GB]
, convert(decimal(20,2), ((((a.total_bytes - a.available_bytes) / 1024.0) / 1024.0) / 1024.0)) as [DriveUsedSpace_GB]
, convert(decimal(20,2), ((((a.available_bytes / 1024.0) / 1024.0) / 1024.0) / (((a.total_bytes / 1024.0) / 1024.0) / 1024.0)) * 100) as [DriveAvailablePercentage]
, convert(decimal(20,2), (((((a.total_bytes - a.available_bytes) / 1024.0) / 1024.0) / 1024.0) / (((a.total_bytes / 1024.0) / 1024.0) / 1024.0)) * 100) as [DriveUsedPercentage]
from sys.master_files as [b]
cross apply sys.dm_os_volume_stats(b.database_id,b.[file_id]) as [a];

Output: Provides detailed metrics, including total space, available space, used space, and percentages

Why Use This Query?

  • Provides detailed metrics, including total space, available space, used space, and percentages.
  • Helps monitor drive usage specifically for volumes where SQL Server database files are located.
  • Useful for regular storage audits and performance troubleshooting.

Why NOT Use This Query?

  • Does not display usage statistics for drives that are not associated with a SQL Server database

2. Using xp_fixeddrives to retrieve drive free space

The xp_fixeddrives extended stored procedure is a simpler option for quickly checking free space on all drives accessible to SQL Server. While it doesn’t provide as much detail as sys.dm_os_volume_stats, it’s fast and easy to use.

Here’s the query:

exec xp_fixeddrives;

Output:

The result shows each drive letter along with the free space (in MB) available on that drive.

Why Use This Query?

  • Quick and straightforward for a high-level view of available space.
  • Ideal for basic monitoring or initial troubleshooting.
  • No need to join multiple tables or write complex calculations.

Why NOT use this query?

  • Does not provide total drive size or percentages.
  • Limited to drives accessible to SQL Server.

3. Using sys.dm_os_enumerate_fixed_drives

sys.dm_os_enumerage_fixed_drives is DMV enumerates fixed mounted volumes and returns information about them.

SELECT *
    ,convert(decimal(20,2), (((free_space_in_bytes / 1024.0) / 1024.0) / 1024.0)) 'Free Space (GB)'
FROM sys.dm_os_enumerate_fixed_drives;

Why Use This Query?

  • Quick and straightforward for a high-level view of available space.
  • Ideal for basic monitoring or initial troubleshooting.
  • No need to join multiple tables or write complex calculations.

Why NOT use this query?

  • Although sys.dm_os_enumerate_fixed_drives returns more information than xp_fixeddrives, it still lacks in the sense that it is not going to return total drive size so that you can calculate used percentage.

Conclusion:

At the end of the day these are all quick and easy ways to get drive info from SQL Server… however they all lack in one form or another. There are a plethora of other utilities, including some simple PowerShell commands, that can give you a full picture of your drive usage on your SQL Servers.

Query to get SQL Server Agent job schedules

Retrieving SQL Agent Job Schedule Details in SQL Server

If you’re working with SQL Server Agent jobs, it’s often useful to have a quick way to retrieve schedule details for all jobs in your system. Whether you’re auditing schedules, troubleshooting overlapping jobs, or simply documenting your environment, having this query on hand can save you a lot of time.

Below is a SQL query that returns the schedule details for all SQL Agent jobs in your instance. The query pulls data from system tables in the msdb database, which is where SQL Server stores job and schedule information.

--**************************************************************************************************************************************
-- Get SQL Agent Job Schedules
--**************************************************************************************************************************************
declare @exlcude_disabled_schedules bit = 1, @exclude_disabled_jobs bit = 1

select a.[name] as [job_name], a.[enabled] as [IsJobEnabled], c.[enabled] as [IsScheduleEnabled], suser_sname(a.[owner_sid]) as [owner_name]
, c.[name] as [schedule_name]
, case when b.next_run_date > 0 then msdb.dbo.agent_datetime(b.next_run_date, b.next_run_time)  
    ELSE msdb.dbo.agent_datetime('19000101', b.next_run_time) END as [next_run_dttm]
, msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time) as [active_start_dttm]
, msdb.dbo.agent_datetime(c.active_end_date, c.active_end_time) as [active_end_dttm]

, case c.freq_type when 1 then 'One time only'
                 when 4 then 'Daily'
                 when 8 then 'Weekly'
                 when 16 then 'Monthly'
                 when 32 then 'Monthly, relative to freq_interval'
                 when 64 then 'Runs when SQL Server Agent service starts'
                 when 128 then 'Runs when computer is idle'
                 else 'Unknown'
                 end as [freq_type_descr]
, case when c.freq_type = 1 then 'Unused'
       when c.freq_type = 4 then 'Every ' + convert(varchar(10), c.freq_interval) + ' days'
       when c.freq_type = 8 then 'Weekly: every'
                   + case when c.freq_interval & 1 = 1 then ' Sunday' else '' end 
                   + case when c.freq_interval & 2 = 2 then ' Monday' else '' end 
                   + case when c.freq_interval & 4 = 4 then ' Tuesday' else '' end 
                   + case when c.freq_interval & 8 = 8 then ' Wednesday' else '' end 
                   + case when c.freq_interval & 16 = 16 then ' Thursday' else '' end 
                   + case when c.freq_interval & 32 = 32 then ' Friday' else '' end 
                   + case when c.freq_interval & 64 = 64 then ' Saturday' else '' end 

       when c.freq_type = 16 then 'Monthly: on the ' + convert(varchar(10), c.freq_interval) + ' day of every ' + convert(varchar(10), c.freq_recurrence_factor) + ' month(s)'
       when c.freq_type = 32 then 'Monthly: on the ' + case when c.freq_relative_interval = 0 then 'Unused'
                 when c.freq_relative_interval = 1 then 'First'
                 when c.freq_relative_interval = 2 then 'Second'
                 when c.freq_relative_interval = 4 then 'Third'
                 when c.freq_relative_interval = 8 then 'Fourth'
                 when c.freq_relative_interval = 16 then 'Last'
                 else 'Unknown' end + ' ' + case when c.freq_interval = 1 then  'Sunday'
                                                                when c.freq_interval = 2 then  'Moday'
                                                                when c.freq_interval = 3 then  'Tusday'
                                                                when c.freq_interval = 4 then  'Wednesday'
                                                                when c.freq_interval = 5 then  'Thursday'
                                                                when c.freq_interval = 6 then  'Friday'
                                                                when c.freq_interval = 7 then  'Saturday'
                                                                when c.freq_interval = 8 then  'Day'
                                                                when c.freq_interval = 9 then  'Weekday'
                                                                when c.freq_interval = 10 then 'Weekend day'
                                                                end + ' of every ' + convert(varchar(10), c.freq_recurrence_factor) + ' month(s)'
       else 'Unused'
       end as [freq_interval_descr]
, case when c.freq_type = 1 then 'At the specified time'
       when c.freq_subday_type = 1 then 'At the specified time'
       when c.freq_subday_type = 2 then 'Seconds'
       when c.freq_subday_type = 4 then 'Minutes'
       when c.freq_subday_type = 8 then 'Hours'
                 end as [freq_subday_type_descr]
, case 
when c.freq_type = 1 then 'At ' + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time), 121),12, 12)
when c.freq_subday_type = 1 then 'At ' + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time), 121),12, 12)
when c.freq_subday_type in (2,4,8) then 'Every ' + convert(varchar(10), c.freq_subday_interval) + ' ' + case c.freq_subday_type when 1 then 'At the specified time'
                 when 2 then 'Seconds' + ' between ' + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time), 121),12, 12) + ' and '  + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_end_date, c.active_end_time), 121),12, 12)
                 when 4 then 'Minutes' + ' between ' + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time), 121),12, 12) + ' and '  + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_end_date, c.active_end_time), 121),12, 12)
                 when 8 then 'Hours' + ' between ' + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_start_date, c.active_start_time), 121),12, 12) + ' and '  + substring(convert(varchar(23), msdb.dbo.agent_datetime(c.active_end_date, c.active_end_time), 121),12, 12)
                 end 
                 else 'Unused' end as [freq_subday_interval_descr]
from msdb.dbo.sysjobs as [a]
join msdb.dbo.sysjobschedules as [b] on b.job_id = a.job_id
join msdb.dbo.sysschedules as [c] on c.schedule_id = b.schedule_id and (@exlcude_disabled_schedules = 0 or (@exlcude_disabled_schedules = 1 and c.[enabled] = 1))
where (@exclude_disabled_jobs = 0 or (@exclude_disabled_jobs = 1 and a.[enabled] = 1))
order by a.[name] asc, c.[name] asc