16

I've been working on code in T-SQL to add new schedules to a SQL Agent job using the sp_add_jobschedule proc in the msdb database. When I add a new schedule (typically a run-once at a specific date/time) and immediately look at the values in sysjobschedules and sysschedules, I can see that the new schedule has been added and is tied to the job_id for my SQL Agent job. However, the values for next_run_date and next_run_time have 0 in them. When I come back and look at them again in 2 or 3 minutes, they still show 0's in them. However when I come back another 5 or 10 minutes later, it now correctly shows the date and time values corresponding to the next scheduled run.

So my questions are:

  • How often do these values get updated?
  • What process is it that updates these values?
  • If I were to add a schedule that was, say, 1 minute in the future, does that mean that the job will not run since the next_run_date/time haven't been updated yet?

Example of the code I use to add a new schedule:

exec msdb.dbo.sp_add_jobschedule @job_id = @jobID
                    , @name = @JobName
                    , @enabled = 1
                    , @freq_type = 1
                    , @freq_interval = 0
                    , @freq_subday_type = 0
                    , @freq_subday_interval = 0
                    , @freq_relative_interval = 0
                    , @freq_recurrence_factor = 0
                    , @active_start_date = @ScheduleRunDate
                    , @active_end_date = 99991231
                    , @active_start_time = @ScheduleRunTime
                    , @active_end_time = 235959

where @jobID is binary(16) that holds the job_id of the job in question, @ScheduleRunDate and @ScheduleRunTime are INTs with the date and time respectively.

0

3 Answers 3

22

Short Answer

It looks like the data in msdb.dbo.sysjobschedules is updated by a background thread in SQL Agent, identified as SQLAgent - Schedule Saver, every 20 minutes (or less frequently, if xp_sqlagent_notify has not been called and no jobs have run in the meantime).

For more accurate information, look at next_scheduled_run_date in msdb.dbo.sysjobactivity. This is updated in real-time any time a job is changed or a job has run. As an added bonus, the sysjobactivity stores the data the right way (as a datetime column), making it a lot easier to work with than those stupid INTs.

That's the short answer:

It could be up to 20 minutes before sysjobschedules reflects the truth; however, sysjobactivity will always be up to date. If you want a lot more details about this, or how I figured it out...


Long Answer

If you care to follow the rabbit for a moment, when you call sp_add_jobschedule, this chain of events is set into motion:

msdb.dbo.sp_add_jobschedule == calls ==> msdb.dbo.sp_add_schedule
                                         msdb.dbo.sp_attach_schedule

msdb.dbo.sp_attach_schedule == calls ==> msdb.dbo.sp_sqlagent_notify

msdb.dbo.sp_sqlagent_notify == calls ==> msdb.dbo.xp_sqlagent_notify

Now, we can't chase the rabbit any further, because we can't really peek into what xp_sqlagent_notify does. But I think we can presume that this extended procedure interacts with the Agent service and tells it that there has been a change to this specific job and schedule. By running a server-side trace we can see that, immediately, the following dynamic SQL is called by SQL Agent:

exec sp_executesql N'DECLARE @nextScheduledRunDate DATETIME 
  SET @nextScheduledRunDate = msdb.dbo.agent_datetime(@P1, @P2) 
  UPDATE msdb.dbo.sysjobactivity 
    SET next_scheduled_run_date = @nextScheduledRunDate 
    WHERE session_id = @P3 AND job_id = @P4',
N'@P1 int,@P2 int,@P3 int,@P4 uniqueidentifier',
20120819,181600,5,'36924B24-9706-4FD7-8B3A-1F9F0BECB52C'

It seems that sysjobactivity is updated immediately, and sysjobschedules is only updated on a schedule. If we change the new schedule to be once a day, e.g.

@freq_type=4, 
@freq_interval=1, 
@freq_subday_type=1, 
@freq_subday_interval=0, 
@freq_relative_interval=0, 
@freq_recurrence_factor=1, 

We still see the immediate update to sysjobactivity as above, and then another update after the job is finished. Various updates come from background and other threads within SQL Agent, e.g.:

SQLAgent - Job Manager
SQLAgent - Update job activity
SQLAgent - Job invocation engine
SQLAgent - Schedule Saver

A background thread (the "Schedule Saver" thread) eventually comes around and updates sysjobschedules; from my initial investigation it appears this is every 20 minutes, and only happens if xp_sqlagent_notify has been called due to a change made to a job since the last time it ran (I did not perform any further testing to see what happens if one job has been changed and another has been run, if the "Schedule Saver" thread updates both - I suspect it must, but will leave that as an exercise to the reader).

I am not sure if the 20-minute cycle is offset from when SQL Agent starts, or from midnight, or from something machine-specific. On two different instances on the same physical server, the "Schedule Saver" thread updated sysjobschedules, on both instances, at almost the exact same time - 18:31:37 & 18:51:37 on one, and 18:31:39 & 18:51:39 on the other. I did not start SQL Server Agent at the same time on these servers, but there is a remote possibility that the start times happened to be 20 minutes offset. I doubt it, but I don't have time right now to confirm by restarting Agent on one of them and waiting for more updates to happen.

I know who did it, and when it happened, because I placed a trigger there and captured it, in case I couldn't find it in the trace, or I inadvertently filtered it out.

CREATE TABLE dbo.JobAudit
(
  [action] CHAR(1),
  [table] CHAR(1), 
  hostname SYSNAME NOT NULL DEFAULT HOST_NAME(), 
  appname SYSNAME  NOT NULL DEFAULT PROGRAM_NAME(),
  dt DATETIME2     NOT NULL DEFAULT SYSDATETIME()
);

CREATE TRIGGER dbo.schedule1 ON dbo.sysjobactivity FOR INSERT
AS
  INSERT dbo.JobAudit([action], [table] SELECT 'I', 'A';
GO
CREATE TRIGGER dbo.schedule2 ON dbo.sysjobactivity FOR UPDATE
AS
  INSERT dbo.JobAudit([action], [table] SELECT 'U', 'A';
GO
CREATE TRIGGER dbo.schedule3 ON dbo.sysjobschedules FOR INSERT
AS
  INSERT dbo.JobAudit([action], [table] SELECT 'I', 'S';
GO
CREATE TRIGGER dbo.schedule4 ON dbo.sysjobschedules FOR UPDATE
AS
  INSERT dbo.JobAudit([action], [table] SELECT 'U', 'S';
GO

That said, it is not hard to catch with a standard trace, this one even comes through as non-dynamic DML:

UPDATE msdb.dbo.sysjobschedules 
  SET next_run_date = 20120817, 
      next_run_time = 20000 
 WHERE (job_id = 0xB87B329BFBF7BA40B30D9B27E0B120DE 
 and schedule_id = 8)

If you want to run a more filtered trace to track this behavior over time (e.g. persisting through SQL Agent restarts instead of on-demand), you can run one that has appname = 'SQLAgent - Schedule Saver'...

So I think that if you want to know the next run time immediately, look at sysjobactivity, not sysjobschedules. This table is directly updated by Agent or its background threads ("Update job activity", "Job Manager" and "Job invocation engine") as activity happens or as it is notified by xp_sqlagent_notify.

Be aware, though, that it is very easy to muck up either table - since there are no protections against deleting data from these tables. (So if you decided to clean up, for example, you can easily remove all the rows for that job from the activity table.) In this case I'm not exactly sure how SQL Server Agent gets or saves the next run date. Perhaps worthy of more investigation at a later date when I have some free time...

1
  • If a job has not yet run for the first time, sysjobschedules will (eventually) show the correct values for next_run_date and next_run_time, whereas sysjobactivity.next_scheduled_run_date remains null until after the first execution. When getting the value from sysjobactivity you need to do so in a subquery that groups by job_id and gets the MAX(next_scheduled_run_date). Dec 31, 2014 at 17:49
1

msdb.dbo.sp_help_job always appears to return the correct actual next_run_date / next_run_time.

It uses sp_get_composite_job_info, which does the following call to actually retrieve the next_run_date/time.

      IF ((@@microsoftversion / 0x01000000) >= 8) -- SQL Server 8.0 or greater
        INSERT INTO @xp_results
        EXECUTE master.dbo.xp_sqlagent_enum_jobs @can_see_all_running_jobs, @job_owner, @job_id
      ELSE
        INSERT INTO @xp_results
        EXECUTE master.dbo.xp_sqlagent_enum_jobs @can_see_all_running_jobs, @job_owner

Since sysjobschedule appears to be unreliable, I would just use sp_help_job.

0

I was also wondering when next_run_date and next_run_time are being updated but was not able to find it. If you ever need to calculate these values yourself, I have created an UDF that will do just that.

Code updated May 6th 2022

CREATE FUNCTION [dbo].[fn_schedule_next_run]
(
    @schedule_id int,
    @from_msdb bit = 0
)
RETURNS DATETIME
AS
BEGIN
/*
This will calculate and return the next rundate/time for any schedule in [msdb].[dbo].[sysschedules] (@from_msdb = 1)
or from local table [schedule] (@from_msdb = 0). If there is no next rundate/time, the function returns NULL.

The fields in [msdb].[dbo].[sysschedules] (and the local table [schedule]) are defined as follows 
(as seen on https://learn.microsoft.com/en-us/sql/relational-databases/system-tables/dbo-sysschedules-transact-sql?view=sql-server-ver15) 

freq_type - int

    How frequently a job runs for this schedule.

    1 = One time only
    4 = Daily
    8 = Weekly
    16 = Monthly
    32 = Monthly, relative to freq_interval
    64 = Runs when the SQL Server Agent service starts
    128 = Runs when the computer is idle

freq_interval - int 

    Days that the job is executed. Depends on the value of freq_type. 
    The default value is 0, which indicates that freq_interval is unused. 
    See the table below for the possible values and their effects.

freq_subday_type - int

    Units for the freq_subday_interval. The following are the possible 
    values and their descriptions.

    1 : At the specified time
    2 : Seconds
    4 : Minutes
    8 : Hours

freq_subday_interval - int

    Number of freq_subday_type periods to occur between each execution of the job.

freq_relative_interval - int    

    When freq_interval occurs in each month, if freq_type is 32 (monthly relative). 
    Can be one of the following values:

    0 = freq_relative_interval is unused
    1 = First
    2 = Second
    4 = Third
    8 = Fourth
    16 = Last

freq_recurrence_factor - int

    Number of weeks or months between the scheduled execution of a job. 
    freq_recurrence_factor is used only if freq_type is 8, 16, or 32. 
    If this column contains 0, freq_recurrence_factor is unused.

active_start_date - int

    Date on which execution of a job can begin. The date is formatted as YYYYMMDD. NULL indicates today's date.

active_end_date - int

    Date on which execution of a job can stop. The date is formatted YYYYMMDD.

active_start_time - int

    Time on any day between active_start_date and active_end_date that job begins executing. 
    Time is formatted HHMMSS, using a 24-hour clock.

active_end_time - int

    Time on any day between active_start_date and active_end_date that job stops executing. 
    Time is formatted HHMMSS, using a 24-hour clock.

Value of freq_type              Effect on freq_interval
-------------------------------------------------------
    1 (once)                    freq_interval is unused (0)
    4 (daily)                   Every freq_interval days
    8 (weekly)                  freq_interval is one or more of the following:
                                    1 = Sunday
                                    2 = Monday
                                    4 = Tuesday
                                    8 = Wednesday
                                    16 = Thursday
                                    32 = Friday
                                    64 = Saturday
    16 (monthly)                On the freq_interval day of the month
    32 (monthly, relative)      freq_interval is one of the following:
                                    1 = Sunday
                                    2 = Monday
                                    3 = Tuesday
                                    4 = Wednesday
                                    5 = Thursday
                                    6 = Friday
                                    7 = Saturday
                                    8 = Day
                                    9 = Weekday
                                    10 = Weekend day
    64 (starts when SQL Server Agent service starts)    freq_interval is unused (0)
    128 (runs when computer is idle)                    freq_interval is unused (0)

-------------------------------------------------------------
This is more or less the algorithm used to find the next time
-------------------------------------------------------------
if freq_subday_type is not in (0, 1, 2, 4, 8)
    set time = null
if now < active_start_time
    set time = active_start_time, today = true
if now > active_end_time
    set time = active_start_time, today = false
if now >= active_start_time and now <= active_end_time
    if freq_subday_type = 1
        set time = active_start_time, today = false
    if freq_subday_type = 2
        set seconds = seconds since active_start_time until now
        if (seconds mod freq_subday_interval) = 0
            set time = active_start_time + seconds, today = true
        else
            set time = active_start_time + freq_subday_interval * ((seconds div freq_subday_interval) + 1), today = true
            if time > active_end_time
                set time = active_start_time, today = false
    if freq_sub_type = 4
        set minutes = minutes since active_start_time until now
        if (minutes mod freq_subday_interval) = 0 
            if (seconds(now) <= seconds(active_start_time))
                set time = active_start_time + minutes, today = true
            else
                set time = active_start_time + freq_subday_interval * ((minutes div freq_subday_interval) + 1), today = true
                if (time > active_end_time)
                    set time = active_start_time, today = false
        else
            set time = active_start_time + freq_subday_interval * ((minutes div freq_subday_interval) + 1), today = true
            if (time > active_end_time)
                set time = active_start_time, today = false
    if freq_sub_type = 8
        set hours = hours since active_start_time until now 
        if (hours mod freq_subday_interval) = 0
            if (minutes(now) < minutes(active_start_time)) or (minutes(now) = minutes(active_start_time) and seconds(now) <= seconds(active_start_time))
                set time = active_start_time + hours, today = true
            else
                set time = active_start_time + freq_subday_interval * ((hours div freq_subday_interval) + 1), today = true
                if (time > active_end_time)
                    set time = active_start_time, today = false
        else
            set time = active_start_time + freq_subday_interval * ((hours div freq_subday_interval) + 1), today = true
            if (time > active_end_time)
                set time = active_start_time, today = false
                
-------------------------------------------------------------
This is more or less the algorithm used to find the next date
-------------------------------------------------------------
if today > active_end_date then return null
set date = active_start_date
if freq_type = 1 (once) => 
    if date == today then return date else return null
if freq_type = 4 (daily) => 
    if date == today then return date
    if freq_interval = 0 then return null
    while (date < today && date < active_end_date) date += freq_interval
    if date < active_end_date then return date else return null
if freq_type = 8 (weekly) =>
    while (date < today && date < active_end_date) date += 7 * freq_recurrence_factor
    while (date < active_end_date)
        set next_week_date = date + 8
        while (date < next_week_date && weekday(date) not in freq_interval) date += 1
        if (date < next_week_date) ==> return date
        date += next_week_date - 8 + 7 * freq_recurrence_factor
    end while
    return null
if freq_type = 16 (monthly) =>
    while (day(date) < freq_interval && date < active_end_dateday) date += 1
    while (date < today && date < active_end_date) set month(date) = month(date) + freq_recurrence_factor
    if (date < active_end_date) then return date else return null
if freq_type = 32 (monthly relative) =>
    while (date < active_end_date)
        set year = year(date)
        set month = month(date)
        set day = first/second/third/fourth/last (freq_relative_interval) mo/tu/we/th/fr/sa/su/day/weekday/weekendday (freq_interval) of month/year
        if (date == today) return date
        set month(date) = month(date) + freq_recurrence_factor ; set year accordingly
    end
    return null;

*/

    DECLARE
        @freq_type int,
        @freq_interval int,
        @freq_subday_type int,
        @freq_subday_interval int,
        @freq_relative_interval int,
        @freq_recurrence_factor int,
        @active_start_date int,
        @active_end_date int,
        @active_start_time int,
        @active_end_time int;

    IF (@from_msdb = 1) BEGIN
        SELECT
            @freq_type = [freq_type],
            @freq_interval = [freq_interval],
            @freq_subday_type = [freq_subday_type],
            @freq_subday_interval = [freq_subday_interval],
            @freq_relative_interval = [freq_relative_interval],
            @freq_recurrence_factor = [freq_recurrence_factor],
            @active_start_date = [active_start_date],
            @active_end_date = [active_end_date],
            @active_start_time = [active_start_time],
            @active_end_time = [active_end_time]
        FROM 
            [msdb].[dbo].[sysschedules]
        WHERE
            [schedule_id] = @schedule_id
    END
    ELSE BEGIN
        SELECT
            @freq_type = [freq_type],
            @freq_interval = [freq_interval],
            @freq_subday_type = [freq_subday_type],
            @freq_subday_interval = [freq_subday_interval],
            @freq_relative_interval = [freq_relative_interval],
            @freq_recurrence_factor = [freq_recurrence_factor],
            @active_start_date = [active_start_date],
            @active_end_date = [active_end_date],
            @active_start_time = [active_start_time],
            @active_end_time = [active_end_time]
        FROM 
            [dbo].[schedule]
        WHERE
            [schedule_id] = @schedule_id
    END

    -- 'AT TIME ZONE' since SQL Server 2016
    DECLARE @CurrentDate DATE = GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE 'W. Europe Standard Time'
    DECLARE @CurrentTime TIME = GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE 'W. Europe Standard Time'
    DECLARE @CurrentDateAndTime DATETIME = CAST(@CurrentDate AS DATETIME) + CAST(@CurrentTime AS DATETIME)

    DECLARE @StartDateAndTime DATETIME = CAST(@CurrentDate AS datetime) + STUFF(STUFF(RIGHT('000000' + CAST(@active_start_time AS VARCHAR), 6), 5, 0, ':'), 3, 0, ':')
    DECLARE @EndDateAndTime DATETIME = CAST(@CurrentDate AS datetime) + STUFF(STUFF(RIGHT('000000' + CAST(@active_end_time AS VARCHAR), 6), 5, 0, ':'), 3, 0, ':')

    DECLARE @NextTime DATETIME = NULL
    DECLARE @ForToday BIT = 0
    DECLARE @Seconds INT
    DECLARE @Minutes INT
    DECLARE @Hours INT

    IF (@freq_subday_type NOT IN (0, 1, 2, 4, 8) OR @freq_subday_interval < 0) BEGIN
        SET @NextTime = NULL
        SET @ForToday = 1
    END
    ELSE IF @CurrentDateAndTime < @StartDateAndTime BEGIN
        SET @NextTime = @StartDateAndTime
        SET @ForToday = 1
    END
    ELSE IF (@CurrentDateAndTime > @EndDateAndTime) BEGIN
        SET @NextTime = @StartDateAndTime
        SET @ForToday = 0
    END
    ELSE IF (@freq_subday_type IN (0, 1)) BEGIN
        -- At the specified time
        SET @NextTime = @StartDateAndTime
        SET @ForToday = 0
    END
    ELSE IF (@freq_subday_type = 2) BEGIN
        -- Every @freq_subday_interval seconds
        SET @Seconds = DATEDIFF(SECOND, @StartDateAndTime, @CurrentDateAndTime)
        IF (@Seconds % @freq_subday_interval = 0) BEGIN
            SET @NextTime = @CurrentDateAndTime
            SET @ForToday = 1
        END
        ELSE BEGIN
            SET @Seconds = @freq_subday_interval * ((@Seconds / @freq_subday_interval) + 1)
            SET @NextTime = DATEADD(SECOND, @Seconds, @StartDateAndTime)
            IF (@NextTime <= @EndDateAndTime) BEGIN
                SET @ForToday = 1
            END
            ELSE BEGIN
                SET @NextTime = @StartDateAndTime
                SET @ForToday = 0
            END
        END
    END
    ELSE IF (@freq_subday_type = 4) BEGIN
        -- Every @freq_subday_interval minutes
        SET @Minutes = DATEDIFF(MINUTE, @StartDateAndTime, @CurrentDateAndTime)
        IF (@Minutes % @freq_subday_interval = 0) BEGIN
            IF (DATEPART(SECOND, @CurrentDateAndTime) <= DATEPART(SECOND, @StartDateAndTime)) BEGIN
                SET @NextTime = DATEADD(MINUTE, @Minutes, @StartDateAndTime)
                SET @ForToday = 1
            END
            ELSE BEGIN
                SET @Minutes = @freq_subday_interval * ((@Minutes / @freq_subday_interval) + 1)
                SET @NextTime = DATEADD(MINUTE, @Minutes, @StartDateAndTime)
                IF (@NextTime <= @EndDateAndTime) BEGIN
                    SET @ForToday = 1
                END
                ELSE BEGIN
                    SET @NextTime = @StartDateAndTime
                    SET @ForToday = 0
                END
            END
        END
        ELSE BEGIN
            SET @Minutes = @freq_subday_interval * ((@Minutes / @freq_subday_interval) + 1)
            SET @NextTime = DATEADD(MINUTE, @Minutes, @StartDateAndTime)
            IF (@NextTime <= @EndDateAndTime) BEGIN
                SET @ForToday = 1
            END
            ELSE BEGIN
                SET @NextTime = @StartDateAndTime
                SET @ForToday = 0
            END
        END
    END
    ELSE IF (@freq_subday_type = 8) BEGIN
        -- Every @freq_subday_interval hours
        SET @Hours = DATEDIFF(HOUR, @StartDateAndTime, @CurrentDateAndTime)
        IF (@Hours % @freq_subday_interval = 0) BEGIN
            IF (DATEPART(MINUTE, @CurrentDateAndTime) <= DATEPART(MINUTE, @StartDateAndTime) OR (DATEPART(MINUTE, @CurrentDateAndTime) = DATEPART(MINUTE, @StartDateAndTime) AND DATEPART(SECOND, @CurrentDateAndTime) <= DATEPART(SECOND, @StartDateAndTime))) BEGIN
                SET @NextTime = DATEADD(HOUR, @Hours, @StartDateAndTime)
                SET @ForToday = 1
            END
            ELSE BEGIN
                SET @Hours= @freq_subday_interval * ((@Hours / @freq_subday_interval) + 1)
                SET @NextTime = DATEADD(HOUR, @Hours, @StartDateAndTime)
                IF (@NextTime <= @EndDateAndTime) BEGIN
                    SET @ForToday = 1
                END
                ELSE BEGIN
                    SET @NextTime = @StartDateAndTime
                    SET @ForToday = 0
                END
            END
        END
        ELSE BEGIN
            SET @Hours= @freq_subday_interval * ((@Hours / @freq_subday_interval) + 1)
            SET @NextTime = DATEADD(HOUR, @Hours, @StartDateAndTime)
            IF (@NextTime <= @EndDateAndTime) BEGIN
                SET @ForToday = 1
            END
            ELSE BEGIN
                SET @NextTime = @StartDateAndTime
                SET @ForToday = 0
            END
        END
    END

    SET @NextTime = CASE WHEN @NextTime IS NOT NULL AND @ForToday <> 1 THEN DATEADD(DAY, 1, @NextTime) ELSE @NextTime END

    DECLARE @StartDate DATE = STUFF(STUFF(RIGHT('00000000' + CAST(@active_start_date AS VARCHAR), 8), 7, 0, '-'), 5, 0, '-')
    DECLARE @EndDate DATE = STUFF(STUFF(RIGHT('00000000' + CAST(@active_end_date AS VARCHAR), 8), 7, 0, '-'), 5, 0, '-')
    DECLARE @Today DATE = @NextTime
    DECLARE @RunningDate DATE = @NextTime
    DECLARE @NextDate DATE = NULL

    IF (@NextTime IS NULL) BEGIN
        GOTO DONE
    END

    IF (@freq_type = 1) BEGIN
        -- 1 = One time only
        SET @NextDate = CASE WHEN (@RunningDate = @Today) THEN @RunningDate ELSE NULL END
        GOTO DONE
    END

    IF (@RunningDate > @EndDate) BEGIN
        GOTO DONE
    END

    SET @RunningDate = @StartDate
    IF (@freq_type = 4) BEGIN
        -- 4 = Daily
        IF (@RunningDate = @Today) BEGIN
            SET @NextDate = @RunningDate
            GOTO DONE
        END
        IF (@freq_interval = 0) GOTO DONE
        WHILE (@RunningDate < @Today AND @RunningDate < @EndDate) BEGIN
            SET @RunningDate = DATEADD(DAY, @freq_interval, @RunningDate)
        END
        SET @NextDate = CASE WHEN (@RunningDate <= @EndDate) THEN @RunningDate ELSE NULL END
        GOTO DONE
    END

    IF (@freq_type = 8) BEGIN
        -- 8 = Weekly
        DECLARE @WhichWeekdays INT = @freq_interval
        DECLARE @EveryXWeeks INT = @freq_recurrence_factor
        DECLARE @NextWeekDate DATE

        IF ((@WhichWeekdays & 127) = 0) BEGIN
            -- if no day  of the week selected, allow any day of the week
            SET @WhichWeekdays = 127
        END
        IF (@EveryXWeeks <= 0) BEGIN
            -- if "occurs every x week" is not set, set "occurs every x week" to 1
            SET @EveryXWeeks = 1
        END
        IF (@StartDate > @Today) BEGIN
            SET @NextWeekDate = @StartDate
        END
        ELSE BEGIN
            -- set the starting week to the week of @Today
            SET @NextWeekDate = DATEADD(DAY, 7 * @EveryXWeeks * (DATEDIFF(DAY, @StartDate, @Today) / (7 * @EveryXWeeks)), @StartDate)
        END
        SET @RunningDate = @NextWeekDate
        IF (DATEDIFF(DAY, @RunningDate, @Today) >= 7) BEGIN
            SET @NextWeekDate = DATEADD(DAY, (7 * @EveryXWeeks), @NextWeekDate)
            SET @RunningDate = @NextWeekDate
        END
        ELSE BEGIN
            WHILE (@RunningDate < @Today) BEGIN
                SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
            END
        END
        IF (@RunningDate >= @Today AND @RunningDate < @EndDate AND (0 <> (POWER(2, DATEPART(WEEKDAY, @RunningDate) - 1) & @WhichWeekdays))) BEGIN
            SET @NextDate = @RunningDate
            GOTO DONE
        END
        WHILE (@RunningDate < @EndDate) BEGIN
            IF (DATEDIFF(DAY, @NextWeekDate, @RunningDate) >= 7) BEGIN
                SET @NextWeekDate = DATEADD(DAY, (7 * @EveryXWeeks), @NextWeekDate)
                SET @RunningDate = @NextWeekDate
            END
            ELSE BEGIN
                WHILE (DATEDIFF(DAY, @NextWeekDate, @RunningDate) < 7) BEGIN
                    IF (@RunningDate < @EndDate AND (0 <> (POWER(2, DATEPART(WEEKDAY, @RunningDate) - 1) & @WhichWeekdays))) BEGIN
                        SET @NextDate = @RunningDate
                        GOTO DONE
                    END
                    SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
                END
            END
        END
        GOTO DONE
    END

    IF (@freq_type = 16) BEGIN
        -- 16 = Monthly
        DECLARE @DayOfTheMonth INT = @freq_interval
        DECLARE @EveryXMonths INT = @freq_recurrence_factor
        DECLARE @NextMonthDate DATE

        IF (@DayOfTheMonth > 31) BEGIN
            GOTO DONE
        END
        IF (@EveryXMonths <= 0) BEGIN
            -- if "occurs every x months" is not set, set "occurs every month"
            SET @EveryXMonths = 1
        END

        IF (@StartDate > @Today) BEGIN
            SET @NextMonthDate = DATEADD(DAY, 1 - DATEPART(DAY, @StartDate), @StartDate)
        END
        ELSE BEGIN
            -- set the next month date (is nearest 1st to today of month
            SET @NextMonthDate = DATEADD(
                MONTH, 
                @EveryXMonths 
                    * (DATEDIFF(MONTH, 
                        DATEADD(DAY, 1 - DATEPART(DAY, @StartDate), @StartDate), 
                        DATEADD(DAY, 1 - DATEPART(DAY, @Today), @Today)) 
                        / @EveryXMonths), 
                DATEADD(DAY, 1 - DATEPART(DAY, @StartDate), @StartDate))
        END
        WHILE (DATEADD(MONTH, 1, @NextMonthDate) < @Today) BEGIN
            SET @NextMonthDate = DATEADD(MONTH, @EveryXMonths, @NextMonthDate)
        END
        SET @RunningDate = @NextMonthDate
        WHILE (@RunningDate < @Today AND @RunningDate < @EndDate) BEGIN
            SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
        END

        -- to avoid an endless loop for the below special cases of schedules, we need to count the maximum number of months
        --      - @DayOfTheMonth = 30 or 31 
        --      - @StartDate somewhere in february
        --      - @EveryXMonths = 12
        -- or also 
        --      - @DayOfTheMonth = 31 
        --      - @StartDate somewhere in a month with 30 days
        --      - @EveryXMonths = 12
        DECLARE @MaxMonths INT = 48

        IF (DATEPART(DAY, @RunningDate) <> @DayOfTheMonth) BEGIN
            WHILE (DATEPART(DAY, @RunningDate) <> @DayOfTheMonth AND @RunningDate < @EndDate AND @MaxMonths > 0) BEGIN
                IF (DATEPART(DAY, DATEADD(DAY, 1, @RunningDate)) > 1) BEGIN
                    -- we are stil in same month
                    SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
                END
                ELSE BEGIN
                    -- we passed to the next month
                    SET @NextMonthDate = DATEADD(MONTH, @EveryXMonths, @NextMonthDate)
                    SET @RunningDate = @NextMonthDate
                    SET @MaxMonths = @MaxMonths - @EveryXMonths
                END
            END
        END
        IF (DATEPART(DAY, @RunningDate) = @DayOfTheMonth ) BEGIN
            SET @NextDate = @RunningDate
        END
        GOTO DONE
    END

    IF (@freq_type = 32) BEGIN
        -- 32 = Monthly, relative to freq_interval
        DECLARE @FirstUpToLastType INT = @freq_relative_interval
        DECLARE @DayType INT = @freq_interval

        IF (@FirstUpToLastType = 0) BEGIN
            GOTO DONE
        END
        IF (@FirstUpToLastType = 5) BEGIN
            -- last su/mo/tu/we/th/fr/sa/day/weekday/weekendday
            SET @RunningDate = DATEADD(DAY, -1, DATEADD(MONTH, 1, DATEADD(DAY, 1 - DAY(@RunningDate), @RunningDate)))
            WHILE (@RunningDate < @EndDate) BEGIN
                IF (@DayType IN (1,2,3,4,5,6,7)) BEGIN
                    -- 1 = Sunday
                    -- 2 = Monday
                    -- 3 = Tuesday
                    -- 4 = Wednesday
                    -- 5 = Thursday
                    -- 6 = Friday
                    -- 7 = Saturday
                    WHILE (DATEPART(WEEKDAY, @RunningDate) <> @DayType) BEGIN
                        SET @RunningDate = DATEADD(DAY, -1, @RunningDate)
                    END
                END
                ELSE IF (@DayType = 9) BEGIN
                    -- 9 = Weekday
                    WHILE (DATEPART(WEEKDAY, @RunningDate) IN (1, 7)) BEGIN
                        SET @RunningDate = DATEADD(DAY, -1, @RunningDate)
                    END
                END
                ELSE IF (@DayType = 10) BEGIN
                    -- 10 = Weekend day
                    WHILE (DATEPART(WEEKDAY, @RunningDate) NOT IN (1, 7)) BEGIN
                        SET @RunningDate = DATEADD(DAY, -1, @RunningDate)
                    END
                END

                IF (@RunningDate >= @Today AND @RunningDate <= @EndDate) BEGIN
                    SET @NextDate = @RunningDate
                    GOTO DONE
                END
                IF (@RunningDate < @EndDate) BEGIN
                    GOTO DONE
                END
                -- try next month - should succeed if less than @EndDate
                SET @RunningDate = DATEADD(DAY, -1, DATEADD(MONTH, 1, DATEADD(DAY, 1 - DAY(@RunningDate), @RunningDate)))
            END
            GOTO DONE
        END
        ELSE BEGIN
            -- freq_relative_interval = 1,2,3,4
            -- first/second/third/fourth su/mo/tu/we/th/fr/sa/day/weekday/weekendday
            SET @RunningDate = DATEADD(DAY, 1 - DAY(@RunningDate), @RunningDate)
            WHILE (@RunningDate <= @EndDate) BEGIN
                IF (@DayType IN (1,2,3,4,5,6,7)) BEGIN
                    -- 1 = Sunday
                    -- 2 = Monday
                    -- 3 = Tuesday
                    -- 4 = Wednesday
                    -- 5 = Thursday
                    -- 6 = Friday
                    -- 7 = Saturday
                    WHILE (DATEPART(WEEKDAY, @RunningDate) <> @DayType) BEGIN
                        SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
                    END
                END
                ELSE IF (@DayType = 9) BEGIN
                    -- 9 = Weekday
                    WHILE (DATEPART(WEEKDAY, @RunningDate) NOT IN (2,3,4,5,6)) BEGIN
                        SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
                    END
                END
                ELSE IF (@DayType = 10) BEGIN
                    -- 10 = Weekend day
                    WHILE (DATEPART(WEEKDAY, @RunningDate) NOT IN (1, 7)) BEGIN
                        SET @RunningDate = DATEADD(DAY, 1, @RunningDate)
                    END
                END

                IF (@FirstUpToLastType > 1) BEGIN
                    -- for second, third, fourth and last
                    IF (@DayType IN (1,2,3,4,5,6,7)) BEGIN
                        -- 1-7 = Weekday Sunday to Saturday
                        SET @RunningDate = DATEADD(DAY, 7 * (@FirstUpToLastType - 1), @RunningDate)
                    END
                    ELSE IF (@DayType = 8) BEGIN
                        -- 8 = Day
                        SET @RunningDate = DATEADD(DAY, @FirstUpToLastType - 1, @RunningDate)
                    END
                    ELSE IF (@DayType = 9) BEGIN
                        -- 9 = Weekday
                        -- add 1, 2 or 3 days for second, third or fourth weekday unless we are 
                        -- Friday (second weekday), Thursday (third weekday) or Wednesday 
                        -- (fourth weekday) in which cases we add 2 more days to skip weekend
                        SET @RunningDate = DATEADD(DAY, @FirstUpToLastType - 1 + 
                            CASE @FirstUpToLastType 
                                WHEN 2 THEN CASE WHEN DATEPART(WEEKDAY, @RunningDate) <= 6 THEN 2 ELSE 0 END
                                WHEN 3 THEN CASE WHEN DATEPART(WEEKDAY, @RunningDate) <= 5 THEN 2 ELSE 0 END
                                WHEN 4 THEN CASE WHEN DATEPART(WEEKDAY, @RunningDate) <= 4 THEN 2 ELSE 0 END
                                ELSE 0
                            END, @RunningDate)
                    END
                    ELSE IF (@DayType = 10) BEGIN
                        -- 10 = Weekend day
                        -- this implementation will take the first day of the next weekend, not the next weekend day
                        -- add 7, 14 or 21 days for next weekend unless we are Sunday where we need to add 
                        -- 1 day less to have the Saturday of the next weekend 
                        SET @RunningDate = DATEADD(DAY, 7 * (@FirstUpToLastType - 1) - CASE WHEN DATEPART(WEEKDAY, @RunningDate) = 6 THEN 1 ELSE 0 END, @RunningDate)
                    END
                END
                IF (@RunningDate >= @Today AND @RunningDate <= @EndDate) BEGIN
                    SET @NextDate = @RunningDate
                    GOTO DONE
                END
                IF (@RunningDate < @EndDate) BEGIN
                    GOTO DONE
                END
                -- try next month - should succeed if less than @EndDate
                SET @RunningDate = DATEADD(MONTH, 1, DATEADD(DAY, 1 - DAY(@RunningDate), @RunningDate))
            END
        END
    END

DONE:
    IF (@NextDate IS NULL OR @NextTime IS NULL) BEGIN
        RETURN NULL
    END

    RETURN CAST(@NextDate AS DATETIME) + CAST(CAST(@NextTime AS TIME) AS DATETIME)
END

See https://github.com/paulpeeters/mssql_calculate_next_run_time

1
  • 1
    Welcome to Database Administrators! While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes or disappears.
    – Glorfindel
    Apr 1, 2022 at 17:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.