SQL Agent: What ran last night after time …

------------------------------------------------------------------------------------------------------------------------
--
-- What ran last night after time ...
--
SELECT  [master_admin].[dbo].[fn_INTtoDateTime]([run_date], [run_time]) AS [RunDate],
        DATENAME(WEEKDAY, CONVERT(VARCHAR(10), [run_date], 112)),
        [j].[name],
        [master_admin].[dbo].[fn_RunDuration]([run_duration]),
        [h].[step_id],
        [s].[step_name],
        [command] AS [StepCommand],
        [instance_id]
FROM    [msdb].[dbo].[sysjobhistory] [h] WITH (NOLOCK)
JOIN    [msdb].[dbo].[sysjobs] [j] WITH (NOLOCK)
ON      [j].[job_id] = [h].[job_id]
JOIN    [msdb]..[sysjobsteps] [s] WITH (NOLOCK)
ON      [s].[job_id] = [j].[job_id]
        AND [s].[step_id] = [h].[step_id]
WHERE   [run_date] >= CONVERT(VARCHAR(8), DATEADD(DAY, -0, GETDATE()), 112)

AND [h].[run_time] >= 021439
ORDER BY [RunDate]

Required objects
[fn_INTtoDateTime]
[fn_RunDuration]

 

PowerShell Disk Space report permissions

After getting the following error:

The error information returned by PowerShell is: ‘Access is denied.

  • The account you are running as needs to be added to:
    • Distributed COM Users
    • Performance Monitor Users
    • WMI Control Properties\Security\
      • Click Security button
        • Add MYSRVNAM\Performance Monitor Users
          • Enable Account;
          • Remote Enable

           

This allows me to run the following as the user.

Get-WmiObject Win32_logicaldisk -ComputerName MYSRVNAM |? {$_.DeviceID -eq "E:" -or $_.DeviceID -eq "F:" -or $_.DeviceID -eq "G:" -or $_.DeviceID -eq "H:" }  `
| Format-Table SystemName, DeviceID, MediaType, `
@{Name="Size(GB)";Expression={[decimal]("{0:N0}" -f($_.size/1gb))}}, `
@{Name="Free Space(GB)";Expression={[decimal]("{0:N0}" -f($_.freespace/1gb))}}, `
@{Name="Free (%)";Expression={"{0,6:P0}" -f(($_.freespace/1gb) / ($_.size/1gb))}} `
-AutoSize | Out-String

Thanks to the original source!

Report on SQLAgent role members

Just remove the filter to see everyone.


;WITH    [RoleMembers]([member_principal_id], [role_principal_id])
          AS (
              SELECT    [rm1].[member_principal_id],
                        [rm1].[role_principal_id]
              FROM      [msdb].[sys].[database_role_members] [rm1] (NOLOCK)
              UNION ALL
              SELECT    [d].[member_principal_id],
                        [rm].[role_principal_id]
              FROM      [msdb].[sys].[database_role_members] [rm] (NOLOCK)
              INNER JOIN [RoleMembers] AS [d]
              ON        [rm].[member_principal_id] = [d].[role_principal_id]
             )
    SELECT DISTINCT
            [rp].[name] AS [database_role],
            [mp].[name] AS [database_user],
            [mp].[type]
    FROM    [RoleMembers] [drm]
    JOIN    [msdb].[sys].[database_principals] [rp]
    ON      ([drm].[role_principal_id] = [rp].[principal_id])
    JOIN    [msdb].[sys].[database_principals] [mp]
    ON      ([drm].[member_principal_id] = [mp].[principal_id])
    WHERE   [rp].[name] LIKE 'SQLAgent%'
            AND [mp].[name] NOT LIKE '##%'
    ORDER BY [rp].[name]


This should produce output similar to this:

 or

database_role	database_user	type
SQLAgentOperatorRole	PolicyAdministratorRole	R
SQLAgentReaderRole	PolicyAdministratorRole	R
SQLAgentReaderRole	SQLAgentOperatorRole	R
SQLAgentUserRole	dc_admin	R
SQLAgentUserRole	dc_operator	R
SQLAgentUserRole	MS_DataCollectorInternalUser	S
SQLAgentUserRole	PolicyAdministratorRole	R
SQLAgentUserRole	SQLAgentOperatorRole	R
SQLAgentUserRole	SQLAgentReaderRole	R

Msg 11520, Level 16, State 1

Msg 11520, Level 16, State 1, Procedure sp_describe_first_result_set, Line 1
The metadata could not be determined because statement ‘EXECUTE master.dbo.xp_sqlagent_is_starting @retval OUTPUT’ in procedure ‘sp_is_sqlagent_starting’ invokes an extended stored procedure.

In SQL 2012 sp_help_job has changed. See this solution:

Workaround: SQL Server 2012 – OPENROWSET on sp_help_job throws “The metadata could not be determined”

Issue:In SQL 2012, stored procedures that call any other stored procedures or extended stored procedures that return result sets should specify WITH RESULT SETSSQL Agent’s stored procedure sp_help_job  – returns single resultset if no parameters are passed to stored procedure.  – returns 3 resultsets if job_id or job_name is specified. MSDN linkWhen RESULTSET is not described,…

Source: blogs.msdn.microsoft.com/sqlagent/2012/07/12/workaround-sql-server-2012-openrowset-on-sp_help_job-throws-the-metadata-could-not-be-determined/

PowerShell: Check all servers for failed jobs.

This is a two part process.

First we use our [master_admin].[dbo].[rp_JobFailures] stored procedure to report any failures on the server. Here’s the guts of our procedure.

DECLARE @nDaysAgo INT
SELECT  @nDaysAgo = 1
SELECT  CAST([j].[name] AS VARCHAR(128)) [FAILED_job_name],
        LEFT(DATENAME(dw, CAST([run_date] AS CHAR(8))) + SPACE(1)
             + CAST([master_admin].[dbo].[fn_INTtoDateTime]([h].[run_date], [h].[run_time]) AS VARCHAR), 30) [RunDate],
        CASE [run_status]
          WHEN 0 THEN 'Failed'
          WHEN 1 THEN 'Succeeded'
          WHEN 2 THEN 'Retry'
          WHEN 3 THEN 'Canceled'
          WHEN 4 THEN 'In progress'
        END [job_status],
        (ISNULL(MAX([s].[step_id]), '')) [StepID],
        (ISNULL(MAX([s].[step_name]), '')) [StepName],
        (ISNULL(MAX([command]), MAX([h].[message]))) [StepCommand],
        MAX([operator_id_emailed]) [NotifiedID]
FROM    [msdb]..[sysjobhistory] [h]
JOIN    [msdb]..[sysjobs] [j]
ON      [j].[job_id] = [h].[job_id]
JOIN    [msdb]..[syscategories] [c]
ON      [j].[category_id] = [c].[category_id]
LEFT OUTER JOIN (
                 -- All jobsteps that have a final history job outcome of
	SELECT  [s].[step_name],
            [s].[command],
            [s].[step_id],
            [s].[job_id],
            [last_run_outcome]
    FROM    [msdb]..[sysjobsteps] [s] WITH (NOLOCK)
    LEFT OUTER JOIN (
                     SELECT [job_id]
                     FROM   [msdb]..[sysjobhistory] [h]
                     WHERE  [run_status] = 0
                            AND [h].[run_date] >= REPLACE(CONVERT(VARCHAR(10), DATEADD(DAY, -@nDaysAgo, GETDATE()), 121),
                                                          '-', '')
                    ) [okay]
    ON      [okay].[job_id] = [s].[job_id]
    WHERE   [okay].[job_id] IS NOT NULL
            AND [last_run_date] >= REPLACE(CONVERT(VARCHAR(10), DATEADD(DAY, -@nDaysAgo, GETDATE()), 121), '-', '')
                ) [s]
ON      [h].[job_id] = [s].[job_id]
        AND [h].[step_id] = [s].[step_id]
WHERE   [h].[run_date] >= REPLACE(CONVERT(VARCHAR(10), DATEADD(DAY, -@nDaysAgo, GETDATE()), 121), '-', '')
        AND [run_status] NOT IN (1, 4)
GROUP BY [j].[name],
        [run_date],
        [run_time],
        [run_status]
ORDER BY [h].[run_date],
        [h].[run_time],
        [j].[name] 

Then we use PowerShell to run that procedure on each of our SQL Servers, stored in $AllServersList.

# ------------------------------------------------------------------------------------------------------------------------
# What jobs failed since yesterday?

$TSQL = 'EXEC [master_admin].[dbo].[rp_JobFailures] @nDaysAgo=1, @vcSkipJob=NULL'

$VerbosePreference="Continue"
$bDEBUG = $true
#	$bDEBUG = $false ; $VerbosePreference="SilentlyContinue" ; $dt=@()
$AllServersList | %{
	$CurrentServer = $_
	Write-Host -ForegroundColor Blue "$(Get-Date -Format g)	[$CurrentServer]"
	Write-Verbose "	Invoke-sqlcmd2 -ConnectionTimeout 10 -ServerInstance $_ -Query ""$TSQL"""; 

	if (!($ExclusionList -contains $CurrentServer)) {
		if ($bDEBUG -eq $false) {
			$dr = Invoke-sqlcmd2 -ConnectionTimeout 10 -ServerInstance $CurrentServer -Query "$TSQL" -As DataRow
			if ($dr -ne $null) {
				$dr | add-member -name ServerName -type noteproperty -value "$CurrentServer"
				$dt += $dr
			}
		}
	} else {
		Write-Host -ForegroundColor Red "Skipping $CurrentServer $_"
	}
}

$dt |Select-Object ServerName, RunDate, Failed_Job_Name, StepID, StepName, StepCommand |ft -AutoSize

You can see I like to run Foreach without doing anything initially to make sure I got it right. Then highlighting from the commented $bDEBUG line to the end gets us what we want. I also use an exclusion list which is simply an array of server names to bypass.

The SQL purists will suggest we just run it within SSMS. Just create server groups for all your servers and right-click and run the query.

SQL: Test SQLAgentReaderRole role in msdb to allow viewing jobs

Sometimes a developer or analyst needs to see SQL Agent jobs or its history.

Create a login, add to role, open SSMS and log in as that login which has no other permissions, and view jobs, check history, and notice everything is grayed out.

CREATE LOGIN jperryx WITH PASSWORD = '1anGxxp22'

USE [msdb]
GO
CREATE USER [jperryx] FOR LOGIN [jperryx] 
ALTER ROLE [SQLAgentReaderRole] ADD MEMBER [jperryx]
GO

After testing yourself, and knowing what they can do with this access, you can decide if you should grant it or not.

You can find more about it here.

Now clean up.

DROP USER jperryx
GO
DROP LOGIN jperryx
GO

SQL: Audit to find identical schedules on multiple jobs

At one point it surprised me to see a SQL Agent job schedule in more than one job. A lot of things can cause this, especially if you script and clone jobs.

Here’s the audit you can use to find the jobs which share the exact same schedule. If you change it in one place, ooops, it changes everywhere!


/*
Audit to find identical schedules on multiple jobs
*/

SELECT  [j].[name] [JobName],
        [s].[name] [DupScheduleName],
		[sjs].[schedule_id]
FROM    [msdb].[dbo].[sysjobschedules] [sjs]
JOIN    [msdb].[dbo].[sysschedules] [s]
ON      [s].[schedule_id] = [sjs].[schedule_id]
JOIN    [msdb].[dbo].[sysjobs] [j]
ON      [j].[job_id] = [sjs].[job_id]
WHERE   [sjs].[schedule_id] IN (SELECT   [sjs].[schedule_id]
                               FROM     [msdb].[dbo].[sysjobschedules] [sjs]
                               JOIN     [msdb].[dbo].[sysschedules] [s]
                               ON       [s].[schedule_id] = [sjs].[schedule_id]
                               GROUP BY [sjs].[schedule_id]
                               HAVING   COUNT(*) > 1)
 

After you find them, you will have to drop the dups and recreate using a different name. Possibly just recreating and getting a new schedule_id would work as well.

SQL: Read from the registry

Now that you have your SQL Agent accounts set up to use DatabaseMail let’s check the registry.

--
-- We have to know the output values to prevent return nothing
--
DECLARE @nvcREG_SZ NVARCHAR(4000),
    @nREG_DWORD INT,
    @vbREG_BINARY VARBINARY

EXEC [master].[dbo].[xp_instance_regread]
    N'HKEY_LOCAL_MACHINE',
    N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent',
    N'UseDatabaseMail',
    @nREG_DWORD OUTPUT
SELECT  @nREG_DWORD AS [UseDatabaseMail]

EXEC [master].[dbo].[xp_instance_regread]
    N'HKEY_LOCAL_MACHINE',
    N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent',
    N'DatabaseMailProfile',
    @nvcREG_SZ OUTPUT
SELECT  @nvcREG_SZ AS [DatabaseMailProfile]

The nice thing about xp_instance_regread is it will figure out what instance of SQL you are running and so e.g.

 N'HKEY_LOCAL_MACHINE',
 N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent' 

becomes

 N'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\SQLServerAgent'

SQLAgent: Test email an operator

Create an operator

USE [msdb]
GO

EXEC [msdb].[dbo].[sp_add_operator]
    @name = N'DBA',
    @enabled = 1,
    @weekday_pager_start_time = 90000,
    @weekday_pager_end_time = 180000,
    @saturday_pager_start_time = 90000,
    @saturday_pager_end_time = 180000,
    @sunday_pager_start_time = 90000,
    @sunday_pager_end_time = 180000,
    @pager_days = 0,
    @email_address = N'TheDBAEmail@mycompany.org',
    @category_name = N'[Uncategorized]'
GO

Then test it.

--Send email to an operator called, "DBA', using the profile you created called, "SQLProfile".
USE [msdb];
GO

EXEC [dbo].[sp_notify_operator]
    @profile_name = N'SQLProfile',
    @name = N'DBA',
    @subject = N'Test Notification',
    @body = N'This is a test of notification via e-mail.';
GO

Also check to see if it was sent. Sometimes takes 10-15 seconds to appear…


SELECT  TOP 10 *
FROM    [msdb].[dbo].[sysmail_sentitems]
ORDER BY 1 DESC

And while we are here let’s make ourselves the failsafe operator.

EXEC [master].[dbo].[sp_MSsetalertinfo]
    @failsafeoperator = N'DBA'
GO 
EXEC [master].[dbo].[sp_MSsetalertinfo]
    @notificationmethod = 1
GO 

ERROR: Database Mail is not enabled for agent notifications.

I missed a step setting up SQL Agent on a new server. So here it is again.

/*
ENABLE SQL Server Agent to use the Database Mail
*/
-- Make sure SQL Server Agent is running.
EXEC [master].[dbo].[xp_servicecontrol]
 'QUERYSTATE',
 'SQLServerAgent'
 GO

USE [msdb]
--1)
EXEC [msdb].[dbo].[sp_set_sqlagent_properties]
 @email_save_in_sent_folder = 1
 
--2) We want to Use DatabaseMail
EXEC [master].[dbo].[xp_instance_regwrite]
 N'HKEY_LOCAL_MACHINE',
 N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent',
 N'UseDatabaseMail',
 N'REG_DWORD',
 1

--3) We want to have "SQLMail Profile" be the DatabaseMail profile SQL Agent uses.
EXEC [master].[dbo].[xp_instance_regwrite]
 N'HKEY_LOCAL_MACHINE',
 N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent',
 N'DatabaseMailProfile',
 N'REG_SZ',
 N'SQLMail Profile'

--
-- STOP/START SQL Server Agent
--
EXEC [master].[dbo].[xp_servicecontrol]
 'STOP',
 'SQLServerAgent'
 GO 
EXEC [master].[dbo].[xp_servicecontrol]
 'START',
 'SQLServerAgent'
 GO

Also see NOTE: Failed to notify ‘operator’ via email.