SQL agent job history

The SQL Server agent job history can be hard to use – the a picture of a job's history and current status is scattered across various msdb tables, and although information is gathered together in the job history GUI, it's still hard to see when a running step started or to look at several jobs simultaneously.

This script gives you a picture of recent and running jobs' steps, times and durations.

Note that the script can't read the currently executing job step from sysjobactivity, so it tries to infer it based on what happened in the previous step, and if there isn't one assumes the current step is 1. This produces some misleading results when a job is started at a later step which is still running.

;WITH history AS ( -- convenience view of sysjobhistory presenting start/end dates as DATETIME
  SELECT 
    job_id
  , step_name  -- take step_name directly rather than looking it up later - step_id is mutable...
  , step_id AS StepSeqNo  -- ...so let's call it what it is!
  , CAST(
      CAST(run_date AS VARCHAR(8)) 
        + ' ' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 1, 2)
        + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 3, 2)
        + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 5, 2)
    AS DATETIME) AS StartTime
  , DATEADD (
      SECOND
    , SUBSTRING(RIGHT('000000' + CAST(run_duration AS VARCHAR(6)), 6), 1, 2) * 3600
      + SUBSTRING(RIGHT('000000' + CAST(run_duration AS VARCHAR(6)), 6), 3, 2) * 60
      + SUBSTRING(RIGHT('000000' + CAST(run_duration AS VARCHAR(6)), 6), 5, 2)
    , CAST(
        CAST(run_date AS VARCHAR(8)) 
          + ' ' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 1, 2)
          + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 3, 2)
          + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time AS VARCHAR(6)), 6), 5, 2)
      AS DATETIME)
    ) AS EndTime
  , run_status
  FROM msdb.dbo.sysjobhistory
  WHERE run_date > CAST(CONVERT(VARCHAR(8), GETDATE() - 28, 112) AS INT)
), running AS ( -- information about running jobs  
  SELECT 
    ja.job_id
  , ja.start_execution_date AS JobStartTime
  -- Determine current step. This isn't reported directly, only the *previous* 
  -- step - so we infer the current step based on what happened in the previous.
  , CASE       
      -- no previous step? This is step 1
      WHEN ja.last_executed_step_id IS NULL THEN 1
      -- previous step succeeded? on_success_action 3 = "go to next step"; 4 = "go to step n"
      WHEN h.run_status = 1 AND js.on_success_action = 3 THEN ja.last_executed_step_id + 1
      WHEN h.run_status = 1 AND js.on_success_action = 4 THEN js.on_success_step_id
      -- previous step failed
      WHEN h.run_status = 0 AND js.on_fail_action = 3 THEN ja.last_executed_step_id + 1
      WHEN h.run_status = 0 AND js.on_fail_action = 4 THEN js.on_fail_step_id
      -- previous step was retried
      WHEN h.run_status = 2 THEN ja.last_executed_step_id
    END AS CurrentStep
  , COALESCE(h.EndTime, ja.start_execution_date) AS CurrentStepStartTime
  FROM msdb.dbo.sysjobactivity ja
    LEFT JOIN history h 
      ON ja.job_id = h.job_id
      AND ja.last_executed_step_id = h.StepSeqNo
      AND h.StartTime >= ja.start_execution_date
    LEFT JOIN msdb.dbo.sysjobsteps js 
      ON js.job_id = ja.job_id
      AND js.step_id = ja.last_executed_step_id  
        -- A step_id based join should be OK here because set of steps stored 
        -- for the job (hopefully!) match the set of steps in the running job.
  WHERE ja.start_execution_date IS NOT NULL
  AND ja.job_history_id IS NULL
  AND ja.session_id = (
    SELECT MAX(session_id)
    FROM msdb.dbo.syssessions
  )
), jobs AS (  -- all overarching job run periods with a unique RunId
  -- finished jobs
  SELECT 
    job_id
  , ROW_NUMBER() OVER (
      ORDER BY StartTime
    ) AS RunId
  , StartTime AS JobStartTime
  , EndTime AS JobEndTime
  , run_status AS JobStatus
  FROM history 
  WHERE StepSeqNo = 0
 
  UNION ALL
 
  -- running jobs
  SELECT 
    job_id
  , -ROW_NUMBER() OVER (
      ORDER BY JobStartTime
    ) AS RunId -- number down from zero to avoid collisions with completed RunId
  , JobStartTime
  , '29991231'
  , -1
  FROM running
), steps AS ( -- all individual step runs with start and end dates
  -- finished steps
  SELECT 
    job_id
  , step_name
  , StartTime AS StepStartTime
  , EndTime AS StepEndTime
  , run_status AS StepStatus
  FROM history 
  WHERE StepSeqNo > 0
 
  UNION ALL
 
  -- running steps  
  SELECT 
    r.job_id
  , js.step_name
  , r.CurrentStepStartTime
  , '29991231'
  , -1
  FROM running r
    INNER JOIN msdb.dbo.sysjobsteps js -- join to get step_name for running steps
      ON js.job_id = r.job_id
      AND js.step_id = r.CurrentStep
)
-- put it all together
SELECT 
  sj.name AS JobName
, j.RunId AS JobRunId
, ROW_NUMBER() OVER (
    PARTITION BY j.RunId
    ORDER BY s.StepStartTime
  ) AS StepSeqNo
, s.step_name AS StepName
, s.StepStartTime
, CASE WHEN s.StepEndTime <= GETDATE() THEN s.StepEndTime END StepEndTime
, DATEADD(
    SECOND
  , DATEDIFF(
      SECOND
    , s.StepStartTime
    , CASE 
        WHEN s.StepEndTime > GETDATE() THEN GETDATE() 
        ELSE s.StepEndTime 
      END
    )
  , CAST('00:00:00' AS TIME(0))
  ) AS StepDuration
, CASE s.StepStatus
    WHEN -1 THEN 'Running'
    WHEN 1 THEN 'Success'
    WHEN 0 THEN 'Failed'
    WHEN 3 THEN 'Cancelled'
    ELSE CAST(s.StepStatus AS VARCHAR)
  END AS StepStatus
FROM jobs j
  INNER JOIN steps s 
    ON s.job_id = j.job_id
    AND s.StepStartTime BETWEEN j.JobStartTime AND j.JobEndTime
  INNER JOIN msdb.dbo.sysjobs sj ON sj.job_id  = j.job_id