2611 字
13 分鐘
瀏覽次數
SQL Agent 多 Job 匯出怎麼做:一支腳本重建多個排程與 Step
2026-03-24
2026-04-14

結論#

用一支腳本批次匯出並重建 SQL Agent Job,連 step、schedule、通知一起帶走,省時間又避免漏設定。

適合用在哪裡#

  • DB 搬機 / 環境切換(DEV → UAT → PROD)
  • 多個 SQL Agent Job 要同步重建
  • 想避免手動建立 Job 遺漏 step 或 schedule
  • DBA / 後端工程師做維運自動化

流程步驟#

1. 定義參數與 Job 清單#

先集中管理會變動的設定(DB、Owner、是否覆蓋),再用表變數定義要匯出的 Job 名單,支援改名。這樣做可以一次處理多個 Job,不用重複操作。

DECLARE @OverrideDatabaseName SYSNAME = N'你的新DB'; -- 指定新 DB
DECLARE @OverrideOwnerLoginName SYSNAME = N'sa'; -- 指定 owner
INSERT INTO @Jobs (SourceJobName, TargetJobName)
VALUES
(N'Job_A', N'Job_A_New'),
(N'Job_B', NULL);

2. 先做檢查,避免匯出錯誤#

在產生腳本前先驗證來源 Job 是否存在,以及目標名稱是否重複。這能避免搬到一半才發現錯誤。

IF EXISTS (
SELECT 1
FROM @Jobs j
LEFT JOIN msdb.dbo.sysjobs sj
ON sj.name = j.SourceJobName
WHERE sj.job_id IS NULL
)
BEGIN
RAISERROR(N'Job 不存在', 16, 1);
RETURN;
END;

3. 動態組出 Job / Step / Schedule#

透過系統表 msdb.dbo.sysjobssysjobstepssysschedules,逐一組出完整腳本。這樣重建後的 Job 行為會與原本一致。

EXEC msdb.dbo.sp_add_job
@job_name = N'Job_A_New',
@enabled = 1;
EXEC msdb.dbo.sp_add_jobstep
@job_id = @jobId,
@step_name = N'Step1',
@command = N'SELECT 1;',
@database_name = N'你的新DB';

4. 輸出 Warning 與檢查資訊#

腳本會額外標示風險,例如非 TSQL step、Proxy、寫死 DB 名稱等,讓你在部署前能先檢查。

-- WARNING: 非 TSQL step
-- WARNING: 使用 Proxy
-- WARNING: command 可能寫死 DB

補充#

  • command 內若寫死 DB 名稱,腳本不會自動改,需手動調整
  • Proxy / SSIS / PowerShell step 要先在新環境建立對應設定
  • 正式環境建議不要直接覆蓋 Job,先確認再執行

指令 / 範例整理#

Click to expand
USE msdb;
GO
SET NOCOUNT ON;
-------------------------------------------------------------------------------
-- 參數區
-------------------------------------------------------------------------------
DECLARE @OverrideDatabaseName SYSNAME = N'你的新DB'; -- NULL = 不覆蓋 step database_name
DECLARE @OverrideOwnerLoginName SYSNAME = N'sa'; -- NULL = 沿用原 owner
DECLARE @OverrideOperatorName SYSNAME = NULL; -- NULL = 沿用原 operator
DECLARE @DropIfExists BIT = 1; -- 1=若同名 Job 已存在,先刪除再重建
DECLARE @AttachToLocalServer BIT = 1; -- 1=最後執行 sp_add_jobserver 綁到 (LOCAL)
DECLARE @CRLF NVARCHAR(10) = CHAR(13) + CHAR(10) + '/**/';
DECLARE @FinalSQL NVARCHAR(MAX) = N'';
DECLARE @Header NVARCHAR(MAX) = N'';
-------------------------------------------------------------------------------
-- 指定要匯出的 Jobs
-------------------------------------------------------------------------------
DECLARE @Jobs TABLE
(
RowNum INT IDENTITY(1,1) PRIMARY KEY,
SourceJobName SYSNAME NOT NULL,
TargetJobName SYSNAME NULL
);
-- ===== 在這裡填你要匯出的 Jobs =====
INSERT INTO @Jobs (SourceJobName, TargetJobName)
VALUES
(N'Job_A', N'Job_A_New'),
(N'Job_B', NULL),
(N'Job_C', N'Job_C_New');
-- ==================================
-------------------------------------------------------------------------------
-- 基本檢查:來源 Job 是否存在
-------------------------------------------------------------------------------
IF EXISTS
(
SELECT 1
FROM @Jobs j
LEFT JOIN msdb.dbo.sysjobs sj
ON sj.name = j.SourceJobName
WHERE sj.job_id IS NULL
)
BEGIN
SELECT
j.SourceJobName
FROM @Jobs j
LEFT JOIN msdb.dbo.sysjobs sj
ON sj.name = j.SourceJobName
WHERE sj.job_id IS NULL;
RAISERROR(N'上面列出的 SourceJobName 在 msdb.dbo.sysjobs 中找不到,請先修正。', 16, 1);
RETURN;
END;
-------------------------------------------------------------------------------
-- 檢查 TargetJobName 是否重複
-------------------------------------------------------------------------------
IF EXISTS
(
SELECT 1
FROM
(
SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName
FROM @Jobs
) x
GROUP BY x.FinalJobName
HAVING COUNT(*) > 1
)
BEGIN
SELECT
x.FinalJobName,
COUNT(*) AS Cnt
FROM
(
SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName
FROM @Jobs
) x
GROUP BY x.FinalJobName
HAVING COUNT(*) > 1;
RAISERROR(N'上面列出的 TargetJobName / FinalJobName 有重複,會互相覆蓋,請先修正。', 16, 1);
RETURN;
END;
-------------------------------------------------------------------------------
-- Header
-------------------------------------------------------------------------------
SET @Header =
N'/* =====================================================================' + @CRLF +
N' SQL Agent Multi-Job Export Script' + @CRLF +
N' Generated : ' + CONVERT(NVARCHAR(19), GETDATE(), 120) + @CRLF +
N' Notes:' + @CRLF +
N' 1. 請先確認新環境是否存在對應 Login / Operator / Proxy / Database。' + @CRLF +
N' 2. 若 step command 內文有寫死舊 DB,需人工調整。' + @CRLF +
N' 3. 建議先建立後停用 Job,手動測試 step 無誤再啟用 schedule。' + @CRLF +
N'===================================================================== */' + @CRLF + @CRLF;
SET @FinalSQL = @Header;
-------------------------------------------------------------------------------
-- 先準備待處理 Job 清單
-------------------------------------------------------------------------------
DECLARE @JobList TABLE
(
RowNum INT PRIMARY KEY,
SourceJobName SYSNAME NOT NULL,
TargetJobName SYSNAME NOT NULL,
JobId UNIQUEIDENTIFIER NOT NULL
);
INSERT INTO @JobList (RowNum, SourceJobName, TargetJobName, JobId)
SELECT
j.RowNum,
j.SourceJobName,
ISNULL(j.TargetJobName, j.SourceJobName) AS TargetJobName,
sj.job_id
FROM @Jobs j
JOIN msdb.dbo.sysjobs sj
ON sj.name = j.SourceJobName;
-------------------------------------------------------------------------------
-- 逐一組 Script
-------------------------------------------------------------------------------
DECLARE
@RowNum INT,
@SourceJobName SYSNAME,
@TargetJobName SYSNAME,
@JobId UNIQUEIDENTIFIER,
@JobSQL NVARCHAR(MAX),
@Warnings NVARCHAR(MAX),
@Tmp NVARCHAR(MAX);
DECLARE JobCursor CURSOR FAST_FORWARD FOR
SELECT RowNum, SourceJobName, TargetJobName, JobId
FROM @JobList
ORDER BY RowNum;
OPEN JobCursor;
FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @JobSQL = N'';
SET @Warnings = N'';
SET @Tmp = N'';
---------------------------------------------------------------------------
-- Job 區塊標頭
---------------------------------------------------------------------------
SET @JobSQL = @JobSQL +
N'/* =====================================================================' + @CRLF +
N' Job #' + CAST(@RowNum AS NVARCHAR(10)) + N'' + @CRLF +
N' Source Job : ' + REPLACE(@SourceJobName, '*/', '* /') + @CRLF +
N' Target Job : ' + REPLACE(@TargetJobName, '*/', '* /') + @CRLF +
N'===================================================================== */' + @CRLF + @CRLF;
---------------------------------------------------------------------------
-- Warning 區
---------------------------------------------------------------------------
IF EXISTS (
SELECT 1
FROM msdb.dbo.sysjobsteps
WHERE job_id = @JobId
GROUP BY job_id
HAVING COUNT(*) > 1
)
BEGIN
SET @Warnings = @Warnings + N'-- WARNING: 此 Job 含多個 steps,請逐一確認 step 間跳轉。' + @CRLF;
END;
IF EXISTS (
SELECT 1
FROM msdb.dbo.sysjobsteps
WHERE job_id = @JobId
AND subsystem <> N'TSQL'
)
BEGIN
SET @Warnings = @Warnings + N'-- WARNING: 含非 TSQL steps(如 CmdExec / PowerShell / SSIS),新環境需確認元件與權限。' + @CRLF;
END;
IF EXISTS (
SELECT 1
FROM msdb.dbo.sysjobsteps
WHERE job_id = @JobId
AND proxy_id IS NOT NULL
AND proxy_id <> 0
)
BEGIN
SET @Warnings = @Warnings + N'-- WARNING: 此 Job 使用 Proxy,新環境必須先建立對應 Proxy/Credential。' + @CRLF;
END;
IF EXISTS (
SELECT 1
FROM msdb.dbo.sysjobsteps
WHERE job_id = @JobId
AND (
command LIKE N'%USE [%]%'
OR command LIKE N'%].dbo.%'
OR command LIKE N'%.dbo.%'
)
)
BEGIN
SET @Warnings = @Warnings + N'-- WARNING: 某些 step command 可能寫死資料庫名稱,請人工檢查 command 內容。' + @CRLF;
END;
IF @Warnings <> N''
BEGIN
SET @JobSQL = @JobSQL +
N'/* Pre-check warnings */' + @CRLF +
@Warnings + @CRLF;
END;
---------------------------------------------------------------------------
-- 刪除同名 Job(可選)
---------------------------------------------------------------------------
SET @JobSQL = @JobSQL +
N'USE msdb;' + @CRLF +
N'GO' + @CRLF;
IF @DropIfExists = 1
BEGIN
SET @JobSQL = @JobSQL +
N'IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''')' + @CRLF +
N'BEGIN' + @CRLF +
N' EXEC msdb.dbo.sp_delete_job @job_name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''';' + @CRLF +
N'END' + @CRLF +
N'GO' + @CRLF;
END;
SET @JobSQL = @JobSQL +
N'DECLARE @jobId BINARY(16);' + @CRLF + @CRLF;
---------------------------------------------------------------------------
-- sp_add_job
---------------------------------------------------------------------------
SELECT @Tmp =
N'EXEC msdb.dbo.sp_add_job' + @CRLF +
N' @job_name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''',' + @CRLF +
N' @enabled = ' + CAST(j.enabled AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_eventlog = ' + CAST(j.notify_level_eventlog AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_email = ' + CAST(j.notify_level_email AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_netsend = ' + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_page = ' + CAST(j.notify_level_page AS NVARCHAR(10)) + N',' + @CRLF +
N' @delete_level = ' + CAST(j.delete_level AS NVARCHAR(10)) + N',' + @CRLF +
N' @description = N''' + REPLACE(ISNULL(j.description, N''), '''', '''''') + N''',' + @CRLF +
N' @category_name = N''' + REPLACE(ISNULL(c.name, N'[Uncategorized (Local)]'), '''', '''''') + N''',' + @CRLF +
N' @owner_login_name = N''' + REPLACE(ISNULL(@OverrideOwnerLoginName, SUSER_SNAME(j.owner_sid)), '''', '''''') + N''',' + @CRLF +
N' @job_id = @jobId OUTPUT;' + @CRLF + @CRLF
FROM msdb.dbo.sysjobs j
LEFT JOIN msdb.dbo.syscategories c
ON j.category_id = c.category_id
WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
---------------------------------------------------------------------------
-- Steps
---------------------------------------------------------------------------
SET @Tmp = N'';
SELECT @Tmp = @Tmp +
N'EXEC msdb.dbo.sp_add_jobstep' + @CRLF +
N' @job_id = @jobId,' + @CRLF +
N' @step_id = ' + CAST(s.step_id AS NVARCHAR(10)) + N',' + @CRLF +
N' @step_name = N''' + REPLACE(s.step_name, '''', '''''') + N''',' + @CRLF +
N' @subsystem = N''' + REPLACE(s.subsystem, '''', '''''') + N''',' + @CRLF +
N' @command = N''' + REPLACE(ISNULL(s.command, N''), '''', '''''') + N''',' + @CRLF +
CASE
WHEN s.additional_parameters IS NULL THEN N''
ELSE N' @additional_parameters = N''' + REPLACE(s.additional_parameters, '''', '''''') + N''',' + @CRLF
END +
N' @cmdexec_success_code = ' + CAST(ISNULL(s.cmdexec_success_code, 0) AS NVARCHAR(10)) + N',' + @CRLF +
N' @on_success_action = ' + CAST(s.on_success_action AS NVARCHAR(10)) + N',' + @CRLF +
N' @on_success_step_id = ' + CAST(s.on_success_step_id AS NVARCHAR(10)) + N',' + @CRLF +
N' @on_fail_action = ' + CAST(s.on_fail_action AS NVARCHAR(10)) + N',' + @CRLF +
N' @on_fail_step_id = ' + CAST(s.on_fail_step_id AS NVARCHAR(10)) + N',' + @CRLF +
CASE
WHEN s.server IS NULL THEN N''
ELSE N' @server = N''' + REPLACE(s.server, '''', '''''') + N''',' + @CRLF
END +
N' @database_name = ' +
CASE
WHEN ISNULL(@OverrideDatabaseName, s.database_name) IS NULL THEN N'NULL'
ELSE N'N''' + REPLACE(ISNULL(@OverrideDatabaseName, s.database_name), '''', '''''') + N''''
END + N',' + @CRLF +
N' @database_user_name = ' +
CASE
WHEN s.database_user_name IS NULL THEN N'NULL'
ELSE N'N''' + REPLACE(s.database_user_name, '''', '''''') + N''''
END + N',' + @CRLF +
N' @retry_attempts = ' + CAST(ISNULL(s.retry_attempts, 0) AS NVARCHAR(10)) + N',' + @CRLF +
N' @retry_interval = ' + CAST(ISNULL(s.retry_interval, 0) AS NVARCHAR(10)) + N',' + @CRLF +
N' @os_run_priority = ' + CAST(ISNULL(s.os_run_priority, 0) AS NVARCHAR(10)) + N',' + @CRLF +
N' @output_file_name = ' +
CASE
WHEN s.output_file_name IS NULL THEN N'NULL'
ELSE N'N''' + REPLACE(s.output_file_name, '''', '''''') + N''''
END + N',' + @CRLF +
CASE
WHEN p.name IS NULL OR p.name = N'' THEN N''
ELSE N' @proxy_name = N''' + REPLACE(p.name, '''', '''''') + N''',' + @CRLF
END +
N' @flags = ' + CAST(ISNULL(s.flags, 0) AS NVARCHAR(10)) + N';' + @CRLF + @CRLF
FROM msdb.dbo.sysjobsteps s
LEFT JOIN msdb.dbo.sysproxies p
ON s.proxy_id = p.proxy_id
WHERE s.job_id = @JobId
ORDER BY s.step_id;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
---------------------------------------------------------------------------
-- start_step_id
---------------------------------------------------------------------------
SELECT @Tmp =
N'EXEC msdb.dbo.sp_update_job' + @CRLF +
N' @job_id = @jobId,' + @CRLF +
N' @start_step_id = ' + CAST(j.start_step_id AS NVARCHAR(10)) + N';' + @CRLF + @CRLF
FROM msdb.dbo.sysjobs j
WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
---------------------------------------------------------------------------
-- schedules
---------------------------------------------------------------------------
SET @Tmp = N'';
SELECT @Tmp = @Tmp +
N'EXEC msdb.dbo.sp_add_jobschedule' + @CRLF +
N' @job_id = @jobId,' + @CRLF +
N' @name = N''' + REPLACE(sch.name, '''', '''''') + N''',' + @CRLF +
N' @enabled = ' + CAST(sch.enabled AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_type = ' + CAST(sch.freq_type AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_interval = ' + CAST(sch.freq_interval AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_subday_type = ' + CAST(sch.freq_subday_type AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_subday_interval = ' + CAST(sch.freq_subday_interval AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_relative_interval = ' + CAST(sch.freq_relative_interval AS NVARCHAR(10)) + N',' + @CRLF +
N' @freq_recurrence_factor = ' + CAST(sch.freq_recurrence_factor AS NVARCHAR(10)) + N',' + @CRLF +
N' @active_start_date = ' + CAST(sch.active_start_date AS NVARCHAR(20)) + N',' + @CRLF +
N' @active_end_date = ' + CAST(sch.active_end_date AS NVARCHAR(20)) + N',' + @CRLF +
N' @active_start_time = ' + CAST(sch.active_start_time AS NVARCHAR(20)) + N',' + @CRLF +
N' @active_end_time = ' + CAST(sch.active_end_time AS NVARCHAR(20)) + N';' + @CRLF + @CRLF
FROM
(
SELECT DISTINCT
sch.name,
sch.enabled,
sch.freq_type,
sch.freq_interval,
sch.freq_subday_type,
sch.freq_subday_interval,
sch.freq_relative_interval,
sch.freq_recurrence_factor,
sch.active_start_date,
sch.active_end_date,
sch.active_start_time,
sch.active_end_time
FROM msdb.dbo.sysjobschedules js
JOIN msdb.dbo.sysschedules sch
ON js.schedule_id = sch.schedule_id
WHERE js.job_id = @JobId
) sch
ORDER BY sch.name;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
---------------------------------------------------------------------------
-- operator / notify
---------------------------------------------------------------------------
SELECT @Tmp =
N'EXEC msdb.dbo.sp_update_job' + @CRLF +
N' @job_id = @jobId,' + @CRLF +
N' @notify_level_email = ' + CAST(j.notify_level_email AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_netsend = ' + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N',' + @CRLF +
N' @notify_level_page = ' + CAST(j.notify_level_page AS NVARCHAR(10)) +
CASE
WHEN ISNULL(@OverrideOperatorName, op_email.name) IS NOT NULL
THEN N',' + @CRLF + N' @notify_email_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_email.name), '''', '''''') + N''''
ELSE N''
END +
CASE
WHEN ISNULL(@OverrideOperatorName, op_netsend.name) IS NOT NULL
THEN N',' + @CRLF + N' @notify_netsend_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_netsend.name), '''', '''''') + N''''
ELSE N''
END +
CASE
WHEN ISNULL(@OverrideOperatorName, op_page.name) IS NOT NULL
THEN N',' + @CRLF + N' @notify_page_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_page.name), '''', '''''') + N''''
ELSE N''
END +
N';' + @CRLF + @CRLF
FROM msdb.dbo.sysjobs j
LEFT JOIN msdb.dbo.sysoperators op_email
ON j.notify_email_operator_id = op_email.id
LEFT JOIN msdb.dbo.sysoperators op_netsend
ON j.notify_netsend_operator_id = op_netsend.id
LEFT JOIN msdb.dbo.sysoperators op_page
ON j.notify_page_operator_id = op_page.id
WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
---------------------------------------------------------------------------
-- job server
---------------------------------------------------------------------------
IF @AttachToLocalServer = 1
BEGIN
SET @JobSQL = @JobSQL +
N'EXEC msdb.dbo.sp_add_jobserver' + @CRLF +
N' @job_id = @jobId,' + @CRLF +
N' @server_name = N''(LOCAL)'';' + @CRLF + @CRLF;
END;
---------------------------------------------------------------------------
-- step review
---------------------------------------------------------------------------
SET @JobSQL = @JobSQL +
N'/* Step Review' + @CRLF;
SET @Tmp = N'';
SELECT @Tmp = @Tmp +
N'-- Step ' + CAST(s.step_id AS NVARCHAR(10)) +
N' | Name: ' + REPLACE(REPLACE(s.step_name, CHAR(13), N' '), CHAR(10), N' ') +
N' | Subsystem: ' + s.subsystem +
N' | DB: ' + ISNULL(s.database_name, N'(NULL)') +
N' | Proxy: ' + ISNULL(p.name, N'(NULL)') +
CASE
WHEN s.command LIKE N'%USE [%]%'
OR s.command LIKE N'%].dbo.%'
OR s.command LIKE N'%.dbo.%'
THEN N' | CHECK_COMMAND_FOR_DB_NAME=YES'
ELSE N''
END +
@CRLF
FROM msdb.dbo.sysjobsteps s
LEFT JOIN msdb.dbo.sysproxies p
ON s.proxy_id = p.proxy_id
WHERE s.job_id = @JobId
ORDER BY s.step_id;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'') +
N'*/' + @CRLF +
N'GO' + @CRLF + @CRLF;
---------------------------------------------------------------------------
-- 加到總腳本
---------------------------------------------------------------------------
SET @FinalSQL = @FinalSQL + @JobSQL;
FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;
END
CLOSE JobCursor;
DEALLOCATE JobCursor;
-------------------------------------------------------------------------------
-- 最終輸出
-------------------------------------------------------------------------------
SELECT @FinalSQL AS DeployScript;
GO

收尾#

這種腳本重點不是匯出,而是讓 Job 搬遷變成「可重複、可驗證」的流程。

SQL Agent 多 Job 匯出怎麼做:一支腳本重建多個排程與 Step
https://joyceowo.github.io/posts/32da78ea09fa8006894dd4c158a631fb/
作者
JoyceOwO
發佈於
2026-03-24
許可協議
CC BY-NC-SA 4.0