en-UShe-IL
You are here:   Blog
Register   |  Login

Blog Archive:

Maximize
* Can be used in order to search for older blogs Entries

Search in blogs


Blog Categories:

Maximize
* Can be used in order to search for blogs Entries by Categories

Blog Tags:

Maximize
* Can be used in order to search for blogs by keywords

TNWikiSummit


Awared MVP

 


Microsoft® Community Contributor 


Microsoft® Community Contributor


 Read this before you use the blog! Maximize

Recent Entries

Minimize
אפר11

Written by: ronen ariely
11/04/2020 01:41 RssIcon

Good day guys,

I hope you are all safe locked at your home spending the time to learn and improve yourself.

I am using a lot of time in helping others in the forums, especially in Microsoft MSDN and TechNet forums, but not limited to these, and today question came from stackoverflow (an interface which I really hate but try to visit from time to time and help people).

The goal for this post is to Execute multiple jobs Synchronously from inside a main job, using only the SQL Server Agent pure solutions.

Before I start, I want to say that the reason I emphasize that I focus only on SQL Server Agent pure solutions, is that these are not the solutions which I will probably use in production if I have free privilege to design the solution, but these are the requirements for our solutions today.

The problem

Execute a job from inside another job is a simple task like executing any query. We simply need to start the external Job using the built-in stored procedure msdb.dbo.sp_start_job. Unfortunately if we have multiple jobs which needed to be executed in specific order Synchronously (meaning that only when one job end then the next one start), is a more complex task using the built-in SQL Server Agent. In this post I will mentioned several options to solve this task.

Demo problem

I will create a table to store the times which each job is executed.

USE [msdb]
GO
 
CREATE table T(
    job nvarchar(100),
    Step nvarchar(100),
    ExcTime DATETIME2
)
GO

Creating first Job

USE [msdb]
GO
 
DECLARE @jobId BINARY(16)
EXEC  msdb.dbo.sp_add_job @job_name=N'Job1',
        @enabled=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @category_name=N'[Uncategorized (Local)]',
        @job_id = @jobId OUTPUT
select @jobId
GO
 
EXEC msdb.dbo.sp_add_jobserver @job_name=N'Job1'
GO
 
EXEC msdb.dbo.sp_add_jobstep @job_name=N'Job1', @step_name=N'Job1Step1',
        @step_id=1,
        @cmdexec_success_code=0,
        @on_success_action=1,
        @on_fail_action=2,
        @retry_attempts=0,
        @retry_interval=0,
        @os_run_priority=0, @subsystem=N'TSQL',
        @command=N'
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''job1Start'', ''step1Start'', SYSDATETIME());
            WAITFOR DELAY ''00:00:10'';
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''job1End'', ''step1End'', SYSDATETIME());
            ',
        @database_name=N'master',
        @flags=0
GO
 
EXEC msdb.dbo.sp_update_job @job_name=N'Job1',
        @enabled=1,
        @start_step_id=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @description=N'',
        @category_name=N'[Uncategorized (Local)]',
        @notify_email_operator_name=N'',
        @notify_page_operator_name=N''
GO
 
 
-- test the job:
    -- I clean the data in the table ->
    -- execute the job which will insert data ->
    -- watch the data after 15 seconds so the job will finish
TRUNCATE TABLE msdb.dbo.T
GO
EXEC dbo.sp_start_job N'Job1'
GO
WAITFOR DELAY '00:00:15'
GO
select * from msdb.dbo.T order by ExcTime
GO
---------------------------------------------

Creating second Job

USE [msdb]
GO
 
DECLARE @jobId BINARY(16)
EXEC  msdb.dbo.sp_add_job @job_name=N'Job2',
        @enabled=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @category_name=N'[Uncategorized (Local)]',
        @job_id = @jobId OUTPUT
select @jobId
GO
 
EXEC msdb.dbo.sp_add_jobserver @job_name=N'Job2'
GO
 
EXEC msdb.dbo.sp_add_jobstep @job_name=N'Job2', @step_name=N'Job2Step1',
        @step_id=1,
        @cmdexec_success_code=0,
        @on_success_action=1,
        @on_fail_action=2,
        @retry_attempts=0,
        @retry_interval=0,
        @os_run_priority=0, @subsystem=N'TSQL',
        @command=N'
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''Job2Start'', ''step1Start'', SYSDATETIME());
            WAITFOR DELAY ''00:00:10'';
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''Job2End'', ''step1End'', SYSDATETIME());
            ',
        @database_name=N'master',
        @flags=0
GO
 
EXEC msdb.dbo.sp_update_job @job_name=N'Job2',
        @enabled=1,
        @start_step_id=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @description=N'',
        @category_name=N'[Uncategorized (Local)]',
        @notify_email_operator_name=N'',
        @notify_page_operator_name=N''
GO
 
 
-- test the job:
    -- I clean the data in the table ->
    -- execute the job which will insert data ->
    -- watch the data after 15 seconds so the job will finish
EXEC dbo.sp_start_job N'Job2'
GO
WAITFOR DELAY '00:00:15'
GO
select * from msdb.dbo.T order by ExcTime
GO -- OK we should see two rows from previous test and two new rows
---------------------------------------------

OK, now we can clean the table and show the problem, by executing the two jobs one after the other

USE [msdb]
GO
 
TRUNCATE TABLE msdb.dbo.T
GO
 
EXEC dbo.sp_start_job N'Job1'
GO
EXEC dbo.sp_start_job N'Job2'
GO
 
-- wait for 25 secons and check the data
WAITFOR DELAY '00:00:25'
select * from msdb.dbo.T order by ExcTime
GO
/*
job          Step       ExcTime
Job2Start    step1Start  2020-04-11 03:17:41.9550418
job1Start    step1Start  2020-04-11 03:17:41.9550418
Job2End      step1End    2020-04-11 03:17:51.9579893
job1End      step1End    2020-04-11 03:17:51.9579893
*/
------- NOTICE! Jobs run asynchronously

The problem for our requirement is that SQL Server Agent is designed for best performance and for this it is designed to execute jobs in the background Asynchronously.

Solution one: Executing each job from the previous job

The basic approach is to Start the first job, and inside each Job at the end of the code add command (using sp_start_job) to execute the next job. Since the command comes at the end it will execute after the rest of the code assuming the rest of the code is simple query which run synchronously.

The disadvantage of this solution is that we need to change the code of the jobs in order to execute the next job. This means that if we have general jobs which we want to execute from time to time separately and sometimes together one after the other synchronously, then this solution does not fit.

Using this approach we must start by creating the last job which we need to execute since the job that need to run before it should call this job and therefore, we want it ready in the database. Therefore, we create the second job first

USE [msdb]
GO
 
DECLARE @jobId BINARY(16)
EXEC  msdb.dbo.sp_add_job @job_name=N'RonenJob2',
        @enabled=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @category_name=N'[Uncategorized (Local)]',
        @job_id = @jobId OUTPUT
select @jobId
GO
 
EXEC msdb.dbo.sp_add_jobserver @job_name=N'RonenJob2'
GO
 
EXEC msdb.dbo.sp_add_jobstep @job_name=N'RonenJob2', @step_name=N'RonenJob2Step1',
        @step_id=1,
        @cmdexec_success_code=0,
        @on_success_action=1,
        @on_fail_action=2,
        @retry_attempts=0,
        @retry_interval=0,
        @os_run_priority=0, @subsystem=N'TSQL',
        @command=N'
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''RonenJob2Start'', ''step1Start'', SYSDATETIME());
            WAITFOR DELAY ''00:00:10'';
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''RonenJob2End'', ''step1End'', SYSDATETIME());
            WAITFOR DELAY ''00:00:01'';
            ',
        @database_name=N'master',
        @flags=0
GO
 
EXEC msdb.dbo.sp_update_job @job_name=N'RonenJob2',
        @enabled=1,
        @start_step_id=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @description=N'',
        @category_name=N'[Uncategorized (Local)]',
        @notify_email_operator_name=N'',
        @notify_page_operator_name=N''
GO

 

Now let's create the first Job which will execute the second one in second step

USE [msdb]
GO
DECLARE @jobId BINARY(16)
EXEC  msdb.dbo.sp_add_job @job_name=N'RonenJob1',
        @enabled=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @category_name=N'[Uncategorized (Local)]',
        @owner_login_name=N'pituach-w10-vm\ronen', @job_id = @jobId OUTPUT
select @jobId
GO
EXEC msdb.dbo.sp_add_jobserver @job_name=N'RonenJob1', @server_name = N'PITUACH-W10-VM\SQL2019'
GO
USE [msdb]
GO
EXEC msdb.dbo.sp_add_jobstep @job_name=N'RonenJob1', @step_name=N'job1step1',
        @step_id=1,
        @cmdexec_success_code=0,
        @on_success_action=3,
        @on_fail_action=2,
        @retry_attempts=0,
        @retry_interval=0,
        @os_run_priority=0, @subsystem=N'TSQL',
        @command=N'INSERT msdb.dbo.T(job,Step,ExcTime) values (''Ronenjob1Start'', ''step1Start'', SYSDATETIME());
            WAITFOR DELAY ''00:00:10'';
            INSERT msdb.dbo.T(job,Step,ExcTime) values (''Ronenjob1End'', ''step1End'', SYSDATETIME());
            WAITFOR DELAY ''00:00:01'';',
        @database_name=N'master',
        @flags=0
GO
USE [msdb]
GO
EXEC msdb.dbo.sp_add_jobstep @job_name=N'RonenJob1', @step_name=N'job1step2',
        @step_id=2,
        @cmdexec_success_code=0,
        @on_success_action=1,
        @on_fail_action=2,
        @retry_attempts=0,
        @retry_interval=0,
        @os_run_priority=0, @subsystem=N'TSQL',
        @command=N'EXEC msdb.dbo.sp_start_job N''RonenJob2'';',
        --@command=N'INSERT msdb.dbo.T(job,Step,ExcTime) values (''-----'', ''------'', SYSDATETIME());',
        @database_name=N'master',
        @flags=0
GO
USE [msdb]
GO
EXEC msdb.dbo.sp_update_job @job_name=N'RonenJob1',
        @enabled=1,
        @start_step_id=1,
        @notify_level_eventlog=0,
        @notify_level_email=2,
        @notify_level_page=2,
        @delete_level=0,
        @description=N'',
        @category_name=N'[Uncategorized (Local)]',
        @owner_login_name=N'pituach-w10-vm\ronen',
        @notify_email_operator_name=N'',
        @notify_page_operator_name=N''
GO

 

And here is the result as expected:

TRUNCATE TABLE msdb.dbo.T
GO
EXEC dbo.sp_start_job N'RonenJob1'
GO
WAITFOR DELAY '00:00:25'
select * from msdb.dbo.T order by ExcTime
GO

 

Solution two: Using loop to check if the previous job finished

In this solution we use two unrelated jobs. The solution will be bases on the execution procedure, which will include a loop to check if the first Job finished. Let's clean the table first

TRUNCATE TABLE msdb.dbo.T
GO

 

For the sake of this solution we will use the first two jobs which we created in order to show the problem. The difference will be in the execution.

declare @I INT
SET @I = 0;
 
EXEC dbo.sp_start_job N'Job1';
WAITFOR DELAY '00:00:02';
---------------------------------- Wait for job 1 to end
while exists (
    select * from msdb.dbo.sysjobs j
    inner join msdb.dbo.sysjobactivity a on j.job_id = a.job_id
    where name = 'Job1' and stop_execution_date is null and start_execution_date is not null
)
begin
    SET @I = @I + 1;
    PRINT CONVERT(NVARCHAR(10), @I ) + '. Waiting Job1...'
    WAITFOR DELAY '00:00:02';
end
---------------------------------- Execute Job2
SET @I = 0;
EXEC dbo.sp_start_job N'Job2';
WAITFOR DELAY '00:00:02';
---------------------------------- Wait for job 2 to end
while exists (
    select * from msdb.dbo.sysjobs j
    inner join msdb.dbo.sysjobactivity a on j.job_id = a.job_id
    where name = 'Job2' and stop_execution_date is null and start_execution_date is not null
)
begin
    SET @I = @I + 1;
    PRINT CONVERT(NVARCHAR(10), @I ) + '. Waiting Job2...'
    WAITFOR DELAY '00:00:02';
end
PRINT 'END'

 

And here we can check the table and confirm that the jobs were executed one after the other

-- wait for 25 secons and check the data
WAITFOR DELAY '00:00:25'
select * from msdb.dbo.T order by ExcTime
GO

 

Option three: Executing the jobs one after the other through one of the embedded languages

Executing the jobs one after the other through one of the embedded languages and technologies which SQL Server provides, like SQLCLR, VBS script using Distributed Management Objects (DMO), Python, and so on... This might be crazy and probably not recommended for most cases but it is a practical option for some scenarios.

 

Walla...
One more Corona Day pass and we can go to sleep 😀