SQL Agent Job Script
This blog post is brought to you by the awesomeness that is the SQL Server Twitter community. Contributions were made by several awesome Tweeples, including Denny Cherry, Kendra Little, Ken Simmons, and Magnus Ahlkvist, among others.
What started this is something you've all probably run into at one time or another. We're currently having some resource contention on our server when a ton of processes kickoff and overlap in the morning. Now, I have a script that I've used in the past for monitoring SQL agent jobs, but this time I wanted to add some historical run-time information. I know the sysjobhistory table contains the information I need, but it has some... let's say, creative ways of storing the data. Opinions on the reasons why vary:

Regardless the reason, I needed to do some conversions. Denny shared with us the msdb.dbo.agent_datetime function to convert run_date and run_time into an actual datetime value. I have to say, this certainly cleans up the code quite a bit! Then Magnus shared a method to convert run_duration into seconds, which he modified from a script on SQLServerCentral. I was able to use these two tidbits to update my old script. You can now run this script to get back a list of all enabled procs, a script that will disable them, a breakdown of the schedule, and a historical run-time average.
/* How many days do you want to include in your run-time average? Recent values tend to be more useful. */ Declare @daysToAverage smallint = 30; Declare @avgRunTime Table ( job_id uniqueidentifier , avgRunTime int ); /* We need to parse the schedule into something we can understand */ Declare @weekDay Table ( mask int , maskValue varchar(32) ); Insert Into @weekDay Select 1, 'Sunday' Union All Select 2, 'Monday' Union All Select 4, 'Tuesday' Union All Select 8, 'Wednesday' Union All Select 16, 'Thursday' Union All Select 32, 'Friday' Union All Select 64, 'Saturday'; /* First, let's get our run-time average */ Insert Into @avgRunTime Select job_id , Avg((run_duration/10000) * 3600 + (run_duration/100%100)*60 + run_duration%100) As 'avgRunTime' /* convert HHMMSS to seconds */ From msdb.dbo.sysjobhistory Where step_id = 0 -- only grab our total run-time And run_status = 1 -- only grab successful executions And msdb.dbo.agent_datetime(run_date, run_time) >= DateAdd(day, -@daysToAverage, GetDate()) Group By job_id; /* Now let's get our schedule information */ With myCTE As( Select sched.name As 'scheduleName' , sched.schedule_id , jobsched.job_id , Case When sched.freq_type = 1 Then 'Once' When sched.freq_type = 4 And sched.freq_interval = 1 Then 'Daily' When sched.freq_type = 4 Then 'Every ' + Cast(sched.freq_interval As varchar(5)) + ' days' When sched.freq_type = 8 Then Replace( Replace( Replace(( Select maskValue From @weekDay As x Where sched.freq_interval & x.mask <> 0 Order By mask For XML Raw) , '"/><row maskValue="', ', '), '<row maskValue="', ''), '"/>', '') + Case When sched.freq_recurrence_factor <> 0 And sched.freq_recurrence_factor = 1 Then '; weekly' When sched.freq_recurrence_factor <> 0 Then '; every ' + Cast(sched.freq_recurrence_factor As varchar(10)) + ' weeks' End When sched.freq_type = 16 Then 'On day ' + Cast(sched.freq_interval As varchar(10)) + ' of every ' + Cast(sched.freq_recurrence_factor As varchar(10)) + ' months' When sched.freq_type = 32 Then Case When sched.freq_relative_interval = 1 Then 'First' When sched.freq_relative_interval = 2 Then 'Second' When sched.freq_relative_interval = 4 Then 'Third' When sched.freq_relative_interval = 8 Then 'Fourth' When sched.freq_relative_interval = 16 Then 'Last' End + Case When sched.freq_interval = 1 Then ' Sunday' When sched.freq_interval = 2 Then ' Monday' When sched.freq_interval = 3 Then ' Tuesday' When sched.freq_interval = 4 Then ' Wednesday' When sched.freq_interval = 5 Then ' Thursday' When sched.freq_interval = 6 Then ' Friday' When sched.freq_interval = 7 Then ' Saturday' When sched.freq_interval = 8 Then ' Day' When sched.freq_interval = 9 Then ' Weekday' When sched.freq_interval = 10 Then ' Weekend' End + Case When sched.freq_recurrence_factor <> 0 And sched.freq_recurrence_factor = 1 Then '; monthly' When sched.freq_recurrence_factor <> 0 Then '; every ' + Cast(sched.freq_recurrence_factor As varchar(10)) + ' months' End When sched.freq_type = 64 Then 'StartUp' When sched.freq_type = 128 Then 'Idle' End As 'frequency' , IsNull('Every ' + Cast(sched.freq_subday_interval As varchar(10)) + Case When sched.freq_subday_type = 2 Then ' seconds' When sched.freq_subday_type = 4 Then ' minutes' When sched.freq_subday_type = 8 Then ' hours' End, 'Once') As 'subFrequency' , Replicate('0', 6 - Len(sched.active_start_time)) + Cast(sched.active_start_time As varchar(6)) As 'startTime' , Replicate('0', 6 - Len(sched.active_end_time)) + Cast(sched.active_end_time As varchar(6)) As 'endTime' , Replicate('0', 6 - Len(jobsched.next_run_time)) + Cast(jobsched.next_run_time As varchar(6)) As 'nextRunTime' , Cast(jobsched.next_run_date As char(8)) As 'nextRunDate' From msdb.dbo.sysschedules As sched Join msdb.dbo.sysjobschedules As jobsched On sched.schedule_id = jobsched.schedule_id Where sched.enabled = 1 ) /* Finally, let's look at our actual jobs and tie it all together */ Select job.name As 'jobName' , sched.scheduleName , sched.frequency , sched.subFrequency , SubString(sched.startTime, 1, 2) + ':' + SubString(sched.startTime, 3, 2) + ' - ' + SubString(sched.endTime, 1, 2) + ':' + SubString(sched.endTime, 3, 2) As 'scheduleTime' -- HH:MM , SubString(sched.nextRunDate, 1, 4) + '/' + SubString(sched.nextRunDate, 5, 2) + '/' + SubString(sched.nextRunDate, 7, 2) + ' ' + SubString(sched.nextRunTime, 1, 2) + ':' + SubString(sched.nextRunTime, 3, 2) As 'nextRunDate' /* Note: the sysjobschedules table refreshes every 20 min, so nextRunDate may be out of date */ , 'Execute msdb.dbo.sp_update_job @job_id = ''' + Cast(job.job_id As char(36)) + ''', @enabled = 0;' As 'disableScript' , art.avgRunTime As 'avgRunTime_inSec' -- in seconds , (art.avgRunTime / 60) As 'avgRunTime_inMin' -- convert to minutes From msdb.dbo.sysjobs As job Join myCTE As sched On job.job_id = sched.job_id Left Join @avgRunTime As art On job.job_id = art.job_id Where job.enabled = 1 -- do not display disabled jobs Order By nextRunDate;
If this doesn't do exactly what you want, check out SQLJobVis, which Ken recommended. It's a free tool that helps visualize the job history of SQL jobs.
Find Missing Indexes in Stored Procs with T-SQL
This post should probably be called "The Power of Twitter." I've said it before, I'll say it again: I love Twitter. I mostly follow SQL Server people, so it's become a great source of new information and help when I feel like banging my head against the wall when I get stumped.
So last week, Jonathan Kehayias (@jmkehayias) posted a link to a missing index script on the MSDN forums. Jonathan's script is modified from something he put together for a question posed by Jeremiah Peschka (@peschkaj).
Jonathan's script intrigued me. I'd never tried to search a query plan's XML before, and it certainly presents some interesting possibilities. After dealing with a missing index in production a few weeks ago (caused by an index change), I thought it'd be a great idea to put a regular monitor in place.
Everything was going well until I ran into a problem where I couldn't get the proc name returned. The results spanned numerous databases; I had the object_id and database_id, but I wanted to store the proc name instead. I tried several different methods, including sp_msforeachdb and sp_executeSQL, and while I had a working solution, it was a little more clunky than I liked. So I asked my awesome followers on Twitter for any tips and within minutes I had half a dozen responses. In the end, @MladenPrajdic solved my problem with a pretty simple solution: put 'Use ?;' at the start of my sp_msforeachdb statement. Thanks again, Mladen!
I've now had this process running on my server for a few days now, with good success. The stored procedure below will return the database name, proc name, and query plan XML for any stored proc with a missing index. This is a centralized proc that will store the results in a table for later action. If you click on the XML, you should see the actual query plan with the missing index details. Because this looks at cached query plans, your best bet is to run it fairly regularly (maybe daily) to increase your chances of catching any problem procs.
/* Create a stored procedure skeleton */ If ObjectProperty(Object_ID('dbo.dba_missingIndexStoredProc_sp'), N'IsProcedure') Is Null Begin Execute ('Create Procedure dbo.dba_missingIndexStoredProc_sp As Print ''Hello World!''') RaisError('Procedure dba_missingIndexStoredProc_sp created.', 10, 1); End; Go /* Drop our table if it already exists */ If Exists(Select Object_ID From sys.tables Where [name] = N'dba_missingIndexStoredProc') Begin Drop Table dbo.dba_missingIndexStoredProc Print 'dba_missingIndexStoredProc table dropped!'; End /* Create our table */ Create Table dbo.dba_missingIndexStoredProc ( missingIndexSP_id int Identity(1,1) Not Null , databaseName varchar(128) Not Null , databaseID int Not Null , objectName varchar(128) Not Null , objectID int Not Null , query_plan xml Not Null , executionDate smalldatetime Not Null Constraint PK_missingIndexStoredProc Primary Key Clustered(missingIndexSP_id) ); Print 'dba_missingIndexStoredProc Table Created'; /* Configure our settings */ Set ANSI_Nulls On; Set Quoted_Identifier On; Go Alter Procedure dbo.dba_missingIndexStoredProc_sp /* Declare Parameters */ @lastExecuted_inDays int = 7 , @minExecutionCount int = 7 , @logResults bit = 1 , @displayResults bit = 0 As /********************************************************************************* Name: dba_missingIndexStoredProc_sp Author: Michelle Ufford, http://sqlfool.com Purpose: Retrieves stored procedures with missing indexes in their cached query plans. @lastExecuted_inDays = number of days old the cached query plan can be to still appear in the results; the HIGHER the number, the longer the execution time. @minExecutionCount = minimum number of executions the cached query plan can have to still appear in the results; the LOWER the number, the longer the execution time. @logResults = store results in dba_missingIndexStoredProc @displayResults = return results to the caller Notes: This is not 100% guaranteed to catch all missing indexes in a stored procedure. It will only catch it if the stored proc's query plan is still in cache. Run regularly to help minimize the chance of missing a proc. Called by: DBA and/or SQL Agent Job Date User Description ---------------------------------------------------------------------------- 2009-03-02 MFU Initial Release for public consumption ********************************************************************************* Exec dbo.dba_missingIndexStoredProc_sp @lastExecuted_inDays = 30 , @minExecutionCount = 5 , @logResults = 1 , @displayResults = 1; *********************************************************************************/ Set NoCount On; Set XACT_Abort On; Set Ansi_Padding On; Set Ansi_Warnings On; Set ArithAbort On; Set Concat_Null_Yields_Null On; Set Numeric_RoundAbort Off; Begin /* Declare Variables */ Declare @currentDateTime smalldatetime; Set @currentDateTime = GetDate(); Declare @plan_handles Table ( plan_handle varbinary(64) Not Null ); Create Table #missingIndexes ( databaseID int Not Null , objectID int Not Null , query_plan xml Not Null Constraint PK_temp_missingIndexes Primary Key Clustered ( databaseID, objectID ) ); Begin Try /* Perform some data validation */ If @logResults = 0 And @displayResults = 0 Begin /* Log the fact that there were open transactions */ Execute dbo.dba_logError_sp @errorType = 'app' , @app_errorProcedure = 'dba_missingIndexStoredProc_sp' , @app_errorMessage = '@logResults = 0 and @displayResults = 0; no action taken, exiting stored proc.' , @forceExit = 1 , @returnError = 1; End; Begin Transaction; /* Retrieve distinct plan handles to minimize dm_exec_query_plan lookups */ Insert Into @plan_handles Select Distinct plan_handle From sys.dm_exec_query_stats Where last_execution_time > DateAdd(day, -@lastExecuted_inDays, @currentDateTime) And execution_count > @minExecutionCount; With xmlNameSpaces ( Default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' ) /* Retrieve our query plan's XML if there's a missing index */ Insert Into #missingIndexes Select deqp.[dbid] , deqp.objectid , deqp.query_plan From @plan_handles As ph Cross Apply sys.dm_exec_query_plan(ph.plan_handle) As deqp Where deqp.query_plan.exist('//MissingIndex') = 1 And deqp.objectid Is Not Null; /* Do we want to store the results of our process? */ If @logResults = 1 Begin Insert Into dbo.dba_missingIndexStoredProc Execute sp_msForEachDB 'Use ?; Select ''?'' , mi.databaseID , Object_Name(o.object_id) , o.object_id , mi.query_plan , GetDate() From sys.objects As o Join #missingIndexes As mi On o.object_id = mi.objectID Where databaseID = DB_ID();'; End /* We're not logging it, so let's display it */ Else Begin Execute sp_msForEachDB 'Use ?; Select ''?'' , mi.databaseID , Object_Name(o.object_id) , o.object_id , mi.query_plan , GetDate() From sys.objects As o Join #missingIndexes As mi On o.object_id = mi.objectID Where databaseID = DB_ID();'; End; /* See above; this part will only work if we've logged our data. */ If @displayResults = 1 And @logResults = 1 Begin Select * From dbo.dba_missingIndexStoredProc Where executionDate >= @currentDateTime; End; /* If you have an open transaction, commit it */ If @@TranCount > 0 Commit Transaction; End Try Begin Catch /* Whoops, there was an error... rollback! */ If @@TranCount > 0 Rollback Transaction; /* Return an error message and log it */ Execute dbo.dba_logError_sp; End Catch; /* Clean-Up! */ Drop Table #missingIndexes; Set NoCount Off; Return 0; End Go
Not know what "EXECUTE dbo.dba_logError_sp;" is doing? Check out my blog post on Error Handling in T-SQL.
HTH!
Michelle
Source: http://sqlfool.com/2009/03/find-missing-indexes/
Categories
- Business Intelligence
- Internals
- Miscellaneous
- PASS
- Performance & Tuning
- Presentations
- SQL 2008
- SQL Tips
- Syndication
- T-SQL Scripts
Subscribe to my blog!
| Like what you see? Subscribe! |
![]() |
Around the Web
Recent Tweets
- RT @Phil_Factor: SQL Server table columns under the hood http://t.co/vp7gQ77B < what a great post
- @tradney haha that's awesome :)
- OH: @AdamMachanic's #sqlpass session was one of the best I've ever seen.
Archives
- November 2011
- October 2011
- September 2011
- July 2011
- June 2011
- May 2011
- April 2011
- January 2011
- December 2010
- November 2010
- October 2010
- September 2010
- July 2010
- June 2010
- May 2010
- April 2010
- February 2010
- January 2010
- November 2009
- October 2009
- September 2009
- August 2009
- July 2009
- June 2009
- May 2009
- April 2009
- March 2009
- February 2009
- January 2009
- December 2008
- November 2008
- October 2008


