powershelldba.de · Uwe Janke

Running SQL Server Backup Jobs with Ola Hallengren and Plain T-SQL: An Admin Guide

One procedure in master, called by a one-line Agent job step, backing up each database individually via Ola Hallengren's DatabaseBackup. This is the operations side: how to set it up, what every parameter does, how to exclude a database, and what to look at when something goes wrong.

This guide covers the setup produced by New-sqmBackupMaintenanceJob from sqmSQLTool, but the moving parts are plain SQL Server objects: an Agent job, a stored procedure in master, a small control table, and Ola Hallengren's Maintenance Solution. You can operate, inspect and change all of it with nothing but SSMS.

Why It Is Built This Way

Three design decisions shape everything below, and knowing them makes the rest predictable:

Everything that varies between jobs and instances - directory, backup type, retention, mail - is a parameter passed by the job step, so the configuration is visible where an operator actually looks, and the procedure itself contains no hard-coded values.

How the Pieces Fit Together

ObjectWhereRole
DatabaseBackupmasterOla Hallengren's procedure. Does the actual BACKUP, verification, naming and file cleanup.
sqm_BackupUserDatabasesmasterLoops over the databases and calls DatabaseBackup once per database. One generic procedure for every job and backup type.
sqm_BackupExcludemasterControl table: which databases to skip. Optional.
CommandLogmasterOla's own log table - every command it issued, with duration and outcome.
Agent jobmsdbOne T-SQL step containing one EXEC with all parameters spelled out.

Per-database calls are deliberate. Handing Ola a whole list in one call is fewer statements, but then one broken database can take the run down with it, and the exclusion list has to be assembled as one long -DatabaseName,... string that becomes unmanageable on instances with many exclusions. One call per database keeps failures isolated and the exclusion decision simple.

Prerequisites

Ola's Maintenance Solution must be present on the instance. Check:

SELECT OBJECT_ID(N'master.dbo.DatabaseBackup', N'P') AS DatabaseBackup,
       OBJECT_ID(N'master.dbo.CommandLog',     N'U') AS CommandLog;

If DatabaseBackup is NULL, install it from ola.hallengren.com (or with Install-sqmOlaMaintenanceSolution; Test-sqmOlaInstallation reports what is present). The backup target directory must exist and the SQL Server service account must be able to write to it - not your account. That distinction bites people with UNC paths: the backup is written by the engine, under its own service identity, no matter who started the job.

Creating the Jobs

Import-Module sqmSQLTool

New-sqmBackupMaintenanceJob -SqlInstance "SQL01" -BackupType FULL -UseExcludeTable
New-sqmBackupMaintenanceJob -SqlInstance "SQL01" -BackupType LOG  -UseExcludeTable

Each call creates (or with -Update, replaces) one job, and drops and recreates the shared procedure. So -Update refreshes the backup logic too, not just the job definition. Without -BackupPath, the instance's configured backup directory plus \Usr-db is resolved at creation time and written into the step, where it stays visible.

The module is only needed on the machine that creates the jobs. The jobs themselves are pure T-SQL, so nothing has to be installed on the SQL Server to run them.

On an Availability Group, one call covers every replica

If the instance belongs to an Availability Group, you do not run this once per replica. The command detects the other replicas from sys.availability_replicas and repeats the whole setup on each of them automatically: the Agent job and the stored procedure in master, with the same schedule, directory, retention and exclude settings.

That the procedure is created separately on every replica is not redundancy, it is necessary: master is a system database and is never part of an Availability Group, so nothing in it replicates. Each replica needs its own copy, and this is what puts it there.

Why this matters operationally: after a failover the new primary already has the job and the procedure in place, with identical settings. There is nothing to deploy, enable or fix up at the moment you can least afford it.

The Job Step

This is the entire step. Every setting is visible, and you can edit it here without going back to PowerShell:

EXEC master.dbo.[sqm_BackupUserDatabases]
     @BackupType               = N'FULL',
     @Directory                = N'D:\Backup\Usr-db',
     @CleanupTime              = 672,
     @UseExcludeTable          = 1,
     @SyncExcludeTable         = 1,
     @IncludeSystemDatabases   = 0,
     @Verify                   = 'Y',
     @Compress                 = 'Y',
     @Checksum                 = 'Y',
     @OverrideBackupPreference = 'Y',
     @LogToTable               = 'Y',
     @MailTo                   = NULL,
     @MailProfile              = N'Default',
     @MailOnSuccess            = 0;

Parameters

ParameterDefaultMeaning
@BackupTyperequiredFULL, DIFF or LOG. Passed straight to Ola.
@DirectoryrequiredRoot backup directory. Ola creates per-database and per-type subfolders below it.
@CleanupTimeNULLRetention in hours. NULL = never delete anything.
@UseExcludeTable0Honour sqm_BackupExclude and skip databases marked inactive.
@SyncExcludeTable1Reconcile that table with sys.databases before backing up, so new databases appear automatically.
@IncludeSystemDatabases0Include master, model and msdb. Leave at 0 if you back those up separately.
@Verify'Y'RESTORE VERIFYONLY after each backup. Costs time, catches unreadable backups early.
@Compress / @Checksum'Y'Backup compression and page checksums.
@OverrideBackupPreference'Y''N' honours the Availability Group backup preference, so only the preferred replica backs up. See the AG section below.
@LogToTable'Y'Write every command to master.dbo.CommandLog. Keep this on.
@MailToNULLDatabase Mail recipient for the run report. NULL = no mail.
@MailOnSuccess0By default mail is only sent on failures; 1 sends it every run.

Retention: Always Hours

Ola's @CleanupTime is expressed in hours, so everything in this setup is:

Retention@CleanupTimeTypical use
2 days48LOG backups
1 week168DIFF on a short cycle
2 weeks336DIFF (default)
4 weeks672FULL (default)

Cleanup runs after a successful backup (Ola's AFTER_BACKUP mode), so a failed run never leaves you having deleted the old backups without having produced a new one.

Check retention against your restore requirement, not your disk. Log backups deleted after 48 hours mean point-in-time recovery only reaches back 48 hours, no matter how long the full backups are kept. If the two are set independently, say so explicitly in your recovery documentation.

Excluding a Database

master.dbo.sqm_BackupExclude holds one row per database. IsActive = 1 means "back this up" and is the default for anything newly discovered, so you never have to remember to add a new database - only to exclude one. You can maintain it with plain SQL, or in a dialog (see below):

-- stop backing up a database
UPDATE master.dbo.sqm_BackupExclude
SET    IsActive = 0, Reason = N'decommissioned 2026-09'
WHERE  DatabaseName = N'OldAppDB';

-- current state
SELECT DatabaseName, IsActive, IsOrphaned, Reason
FROM   master.dbo.sqm_BackupExclude
ORDER  BY IsActive, DatabaseName;

IsOrphaned = 1 marks a row whose database no longer exists on the instance. The row is kept rather than deleted, so that an exclusion decision survives a database being detached and reattached; if the database comes back, the flag clears itself on the next run.

Doing it in a dialog instead

If you would rather not write UPDATE statements against a control table - or want to hand the decision to someone who shouldn't - Show-sqmBackupExcludeForm opens a small dialog for exactly this table:

Show-sqmBackupExcludeForm -SqlInstance "SQL01"

Sync & Laden runs Sync-sqmBackupExcludeTable to reconcile the table with the current databases and fills the grid. From there you tick IsActive on or off per database and edit the Reason text; orphaned rows are highlighted so a database that has disappeared is obvious at a glance. Speichern writes back only the rows you actually changed.

The dialog and the UPDATE statement do the same thing to the same table - use whichever fits the situation. The backup job reads that table at the start of every run, so a change takes effect on the next run either way, with no job change and no restart.
A database with IsActive = 0 is not backed up at all - no full, no log. That is the intent, but it also means an accidental 0 silently removes a database from your backup strategy. The query above belongs in whatever you review periodically.

Schedules

Defaults when you don't specify a schedule:

TypeScheduleRetention
FULLdaily 20:15672 h (4 weeks)
DIFFMon-Sat 20:00336 h (2 weeks)
LOGevery 15 min from 00:0048 h (2 days)

The LOG start time matters more than it looks: an interval schedule only repeats between its start time and end of day, so starting at 00:00 is what makes "every 15 minutes" actually mean all day.

Verifying a Run

Three independent places to look, in increasing detail. What was backed up, according to SQL Server itself:

SELECT database_name, type, backup_start_date,
       CAST(compressed_backup_size/1024.0/1024 AS decimal(10,1)) AS SizeMB
FROM   msdb.dbo.backupset
WHERE  backup_start_date > DATEADD(DAY, -1, GETDATE())
ORDER  BY backup_start_date DESC;

type is D for full, I for differential and L for log - the quickest way to confirm a "LOG" job is really taking log backups.

What the procedure actually did, including failures and skips, is in the job history (View History, or the step output). A healthy run ends with a summary line:

Backing up SalesDB (FULL)...
Skipping OldAppDB (sqm_BackupExclude: IsActive = 0).
Done. OK: 18, skipped: 1, failed: 0.

And Ola's own log, with the exact command and duration per database:

SELECT TOP 50 DatabaseName, CommandType, StartTime, EndTime, ErrorNumber, Command
FROM   master.dbo.CommandLog
ORDER  BY ID DESC;
The one check worth automating: not "did the job succeed" but "is every database that should have a recent backup actually covered". A job that skipped a database reports success.

Confirming it with sqmSQLTool

The same three questions have ready-made answers in the module, which is usually quicker than writing the queries yourself:

QuestionCommand
Does every database have a current backup? Get-sqmDatabaseHealth reports the last FULL, DIFF and LOG backup per database - and reads sqm_BackupExclude while doing so, so a database you excluded on purpose is not reported as a gap. That is exactly the coverage check above, without the manual comparison.
Are the files themselves readable? Test-sqmBackupIntegrity runs RESTORE VERIFYONLY against one or more backup files (stripes included) and returns true or false.
Can the backup actually be restored? Invoke-sqmRestoreTest restores into a new, throwaway database - never over the original - and documents outcome, data volume, duration and throughput as a TXT and HTML report.
Get-sqmDatabaseHealth   -SqlInstance "SQL01"
Test-sqmBackupIntegrity -SqlInstance "SQL01" -BackupFile "D:\Backup\Usr-db\SalesDB\FULL\SalesDB_FULL_20260917.bak"
Invoke-sqmRestoreTest   -SqlInstance "SQL01" -Database "SalesDB"
The restore-test report is written to be handed over: it answers "was a restore test carried out, when, and did it work" in a form you can file. For a recurring obligation, New-sqmRestoreTestJob schedules it as a job rather than leaving it to be remembered.

Availability Groups

Two things work together here: the setup is deployed to every replica, and the backup preference decides which replica actually backs up.

What gets deployed where

Creating a job on an AG member automatically repeats the same setup on every other replica, unless you pass -SkipAlwaysOnPropagation. On each replica this creates:

Created on every replicaWhy
The Agent job and its scheduleJobs live in msdb, which does not replicate. Each replica needs its own.
master.dbo.sqm_BackupUserDatabasesmaster is a system database and is never part of an AG, so the procedure cannot arrive there by replication.
master.dbo.sqm_BackupExcludeSame reason. The exclusion decision therefore has to be maintained per replica - see the note below.

Propagation runs in update mode, so repeating the command is safe: existing jobs and procedures are refreshed rather than duplicated. If one replica cannot be reached, that is logged as a warning and the remaining replicas still get their copy, so a single unreachable node does not abort the rollout. Check the result for entries marked Secondary_* to see what happened where.

Which replica actually backs up

With @OverrideBackupPreference = 'Y' (the default here) every replica backs up what it has, regardless of the AG backup preference. That is the right default for a standalone instance and usually wrong for an AG, where it produces backups on several replicas at once.

On an AG, set it to 'N' and let the backup preference decide. Every replica then runs the same job on the same schedule, and only the preferred one actually takes backups. After a failover no job needs to be touched: the new primary was already running its copy, and the preference simply resolves differently. Set the preference on the AG itself, and remember that log backups taken on a secondary still form one continuous log chain with the primary's.

The exclude table is per replica. Because master does not replicate, marking a database IsActive = 0 on the primary does not exclude it anywhere else. Either repeat the change on each replica, or re-run the creating command, which propagates the current settings again.

When Something Goes Wrong

SymptomWhere to look
Job fails, some databases fineBy design: the step fails at the end if any database failed, after attempting all of them. The failing one is named in the step output and in CommandLog.ErrorNumber.
"The directory does not exist"Ola checks the target. Verify the path exists and that the SQL Server service account can write to it - test from the engine's perspective, not from Explorer under your own account.
LOG backup fails for one databaseAlmost always SIMPLE recovery model, or no full backup taken yet. Ola skips SIMPLE databases for log backups rather than failing.
A database is missing from the backupsCheck sqm_BackupExclude for IsActive = 0 first; then whether it is online in sys.databases.
Job reports success but no files appearConfirm the step is the EXEC you expect and that @Directory points where you think. Changing the procedure does not change existing jobs - recreate them with -Update.
Backup files pile up@CleanupTime = NULL means no cleanup. Note Ola only cleans the directory structure it manages, and only for the backup type being run.

Changing Things Safely

Because everything lives in the step parameters, most changes are an edit in SSMS - retention, mail recipient, target directory - and take effect on the next run. Two things worth knowing:

Related sqmSQLTool Commands

Full parameter reference for every function used or mentioned above:

CommandWhat it does here
New-sqmBackupMaintenanceJobCreates the backup job and the procedure described in this guide.
Install-sqmOlaMaintenanceSolutionInstalls Ola Hallengren's Maintenance Solution on the instance.
Test-sqmOlaInstallationReports whether the Maintenance Solution is present and the Agent is running.
Show-sqmBackupExcludeFormDialog for maintaining the exclude table.
Sync-sqmBackupExcludeTableReconciles the exclude table with the current databases.
Register-sqmBackupExcludeTriggerOptional DDL trigger that adds newly created databases to the exclude table immediately, instead of at the next run.
Invoke-sqmUserDatabaseBackupAd-hoc backup of user databases without going through a job.
Get-sqmDatabaseHealthReports the last FULL/DIFF/LOG backup per database, aware of the exclude table.
Test-sqmBackupIntegrityVerifies existing backup files with RESTORE VERIFYONLY.
Invoke-sqmRestoreTestRestore test into a throwaway database, with an auditable report - the only check that proves a backup is usable.
New-sqmRestoreTestJobSchedules that restore test as a recurring job.
New-sqmOlaUsrDbBackupJobAlternative job builder: separate FULL/DIFF/LOG jobs in Ola's own naming scheme.

The complete command reference is at sqmSQLTool commands.

A Short Checklist