In this article, I am going to show how we can automate a database daily health report using T-SQL scripts. For a senior database administrator, it is always important to review the health of the database server. When a DBA manages many database servers, it becomes difficult to visit every server and review the status of databases, SQL jobs, and backups. To ease the monitoring, I have developed a T-SQL script that populates the following information:
- Hostname, database edition, and version of the SQL Server
- List of disk drives, total space, used space, and available space
- List of the database hosted on the server
- List of SQL Jobs
- List of most recent backups of all databases
Once the T-SQL script is explained, I will demonstrate how we can schedule it using the SQL Server Agent.
In the script, I have declared a variable named @DatabaseServerInformation. The data type of the variable is varchar(max). In the variable, we will store the output of the query in the HTML formatted table.
Server and Instance status
In the first HTML table, we will display the following server information:
- Hostname
- SQL Server version
- SQL Server edition
- SQL Server is clustered or not
- SQL Server is in single-user mode
The T-SQL code to populate the server and instance information is the following:
1 2 3 4 5 6 7 8 9 10 |
The T-SQL code to populate the server and instance information is the following: /*Variables for server property*/ declare @DatabaseServerInformation nvarchar(max) declare @Hostname varchar(50) = (select convert(varchar(50),@@SERVERNAME)) declare @Version varchar(max) = (select convert(varchar(max),@@version)) declare @Edition varchar(50) = (select convert(varchar(50),SERVERPROPERTY('edition'))) declare @IsClusteredInstance varchar(50) = (SELECT CASE SERVERPROPERTY ('IsClustered') WHEN 1 THEN 'Clustered Instance' WHEN 0 THEN 'Non Clustered instance' ELSE 'null' END) declare @IsInstanceinSingleUserMode varchar(50) = (SELECT CASE SERVERPROPERTY ('IsSingleUser') WHEN 1 THEN 'Single user' WHEN 0 THEN 'Multi user' ELSE 'null' END) |
The HTML code to create the table is as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/*HTML Table with variables*/ set @DatabaseServerInformation= '<font face="Verdana" size="4">Server Info</font> <table border="1" cellpadding="0" cellspacing="0" style="border-collapse: collapse" bordercolor="#111111" width="90%" id="AutoNumber1" height="50"> <tr> <td width="27%" height="22" bgcolor="#D3D3D3"><b> <font face="Verdana" size="2" color="#FFFFFF">Host Name</font></b></td> <td width="39%" height="22" bgcolor="#D3D3D3"><b> <font face="Verdana" size="2" color="#FFFFFF">SQL Server version</font></b></td> <td width="90%" height="22" bgcolor="#D3D3D3"><b> <font face="Verdana" size="2" color="#FFFFFF">SQL Server edition</font></b></td> <td width="90%" height="22" bgcolor="#D3D3D3"><b> <font face="Verdana" size="2" color="#FFFFFF">Failover Clustered Instance</font></b></td> <td width="90%" height="22" bgcolor="#D3D3D3"><b> <font face="Verdana" size="2" color="#FFFFFF">Single User mode</font></b></td> </tr> <tr> <td width="27%" height="27"><font face="Verdana" size="2">'+@Hostname+'</font></td> <td width="39%" height="27"><font face="Verdana" size="2">' + @Version +'</font></td> <td width="90%" height="27"><font face="Verdana" size="2">'+@Edition+'</font></td> <td width="90%" height="27"><font face="Verdana" size="2">'+@IsClusteredInstance+'</font></td> <td width="90%" height="27"><font face="Verdana" size="2">'+@IsInstanceinSingleUserMode+'</font></td> </tr> </table>' |
The HTML table generated by above code looks like the following image:
Disk Status
The second HTML table contains the information of the disk is placed. The query to populate the disk volume information is the following:
1 2 3 4 5 6 7 |
SELECT DISTINCT volumes.logical_volume_name AS LogicalName, volumes.volume_mount_point AS Drive, CONVERT(INT,volumes.available_bytes/1024/1024/1024) AS FreeSpace, CONVERT(INT,volumes.total_bytes/1024/1024/1024) AS TotalSpace, CONVERT(INT,volumes.total_bytes/1024/1024/1024) - CONVERT(INT,volumes.available_bytes/1024/1024/1024) AS OccupiedSpace FROM sys.master_files mf CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.FILE_ID) volumes |
Output:
The following HTML code is used to generate a table:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<table id="AutoNumber1" style="BORDER-COLLAPSE: collapse" borderColor="#111111" height="40" cellSpacing="0" cellPadding="0" width="24%" border="1"> <tr> <td width="27%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Logical Name</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Volume </font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Free Space (GB)</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Occupied Space (GB)</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Total Space (GB)</font></b></td> </tr> <p style="margin-top: 1; margin-bottom: 0"> </p> <p><font face="Verdana" size="4">Disk Stats</font></p>' SELECT distinct @DatabaseServerInformation = @DatabaseServerInformation + '<tr><td><font face="Verdana" size="1">' + volumes.logical_volume_name +'</font></td>' + '<td><font face="Verdana" size="1">' + volumes.volume_mount_point +'</font></td>' + '<td><font face="Verdana" size="1">' + Convert(varchar,CONVERT(INT,volumes.available_bytes/1024/1024/1024)) +'</font></td>' + '<td><font face="Verdana" size="1">' + Convert(varchar,CONVERT(INT,volumes.total_bytes/1024/1024/1024) - CONVERT(INT,volumes.available_bytes/1024/1024/1024)) +'</font></td>' + '<td><font face="Verdana" size="1">' + Convert(varchar,CONVERT(INT,volumes.total_bytes/1024/1024/1024)) +'</font></td></tr>' FROM sys.master_files mf CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.FILE_ID) volumes |
The table generated by the code looks like the following image:
Database information
The third section of the code shows the list of the databases hosted on the server. The following information of the database is included.
- Database Name
- Database Created date and time
- Owner of the database
- State of the database
- Compatibility level
- Recovery model
- Size of the database
The query to populate the database is the following:
1 2 3 4 5 |
select a.database_id, a.name,a.create_date,b.name,a.user_access_desc,a.state_desc,compatibility_level, recovery_model_desc, Sum((c.size*8)/1024) as DBSizeInMB from sys.databases a inner join sys.server_principals b on a.owner_sid=b.sid inner join sys.master_files c on a.database_id=c.database_id Where a.database_id>5 Group by a.name,a.create_date,b.name,a.user_access_desc,compatibility_level,a.state_desc, recovery_model_desc,a.database_id |
Output:
Following is the code of the HTML table to display the query output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<table id="AutoNumber1" style="BORDER-COLLAPSE: collapse" borderColor="#111111" height="40" cellSpacing="0" cellPadding="0" width="24%" border="1"> <tr> <td width="27%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database Name</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Created Date and Time </font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database created by</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database Status</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database access status</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Compatibility Level</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Recovary Model</font></b></td> <td width="59%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database Size</font></b></td> </tr> <p style="margin-top: 1; margin-bottom: 0"> </p> <p><font face="Verdana" size="4">Disk Stats</font></p>' SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<tr><td><font face="Verdana" size="1">' + convert(varchar,a.name) +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(nvarchar,a.create_date) +'</font></td>' + '<td><font face="Verdana" size="1">' + b.name +'</font></td>' + '<td><font face="Verdana" size="1">' + a.state_desc +'</font></td>' + '<td><font face="Verdana" size="1">' + a.user_access_desc +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(nvarchar,a.compatibility_level) +'</font></td>' + '<td><font face="Verdana" size="1">' + a.recovery_model_desc +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(nvarchar,Sum((c.size*8)/1024)) +'</font></td></tr>' FROM sys.databases a inner join sys.server_principals b on a.owner_sid =b.sid inner join sys.master_files c on a.database_id =c.database_id Where a.database_id>5 Group by a.name,a.create_date,b.name,a.user_access_desc,compatibility_level, recovery_model_desc,a.database_id,a.state_desc |
The HTML table looks like following image:
Database backup information
The third section of the code shows the most recent full backup, differential backup, and Log backup of the databases hosted on the server. The following information of the backup is included.
- Database Name
- Backup Type
- Backup Start Date
- Backup end date
- Server name
- Backup size
- Backup is taken by
The query to populate the database is the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
create table #BackupInformation (DatabaseName varchar(200), backup_type varchar(50), backupstartdate datetime, backupfinishdate datetime, username varchar(200), backupsize numeric(10,2), BackupUser varchar(250)) ;with backup_information as ( select database_name, backup_type = case type when 'D' then 'Full backup' when 'I' then 'Differential backup' when 'L' then 'Log backup' else 'Other or copy only backup' end , backup_start_date , backup_finish_date , user_name , server_name , compressed_backup_size , rownum = row_number() over ( partition by database_name, type order by backup_finish_date desc ) from msdb.dbo.backupset ) insert into #BackupInformation select database_name [Database Name], backup_type [Backup Type], backup_start_date [Backup start date], backup_finish_date [Backup finish date], server_name [Server Name], Convert(varchar,convert(numeric(10,2),compressed_backup_size/ 1024/1024)) [Backup size in MB], user_name [Backup taken by] from backup_information where rownum = 1 order by database_name; |
Output:
Following is the code of the HTML table to display the query output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<table id="AutoNumber1" style="BORDER-COLLAPSE: collapse" borderColor="#111111" height="40" cellSpacing="0" cellPadding="0" width="54%" border="1"> <tr> <td width="27%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Database Name</font></b></td> <td width="19%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Backup Type </font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Backup Start Date</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Backup Finsh Date</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Server Name</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Backup Size in MB</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Backup generated by </font></b></td> </tr> <p style="margin-top: 1; margin-bottom: 0"> </p> <p><font face="Verdana" size="4">Backup Details</font></p>' SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<tr><td><font face="Verdana" size="1">' + convert(varchar,databasename) +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(nvarchar,backup_type) +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(varchar,backupstartdate,120) +'</font></td>' + '<td><font face="Verdana" size="1">' + convert(varchar,backupfinishdate,120) +'</font></td>' + '<td><font face="Verdana" size="1">' + servername +'</font></td>' + '<td><font face="Verdana" size="1">' + Convert(varchar,convert(numeric(10,2),backupsize)) +'</font></td>' + '<td><font face="Verdana" size="1">' + BackupUser +'</font></td></tr>' from #BackupInformation |
The table generated by HTML code looks like the following image:
Status of the SQL Jobs
The third section of the code shows the list of the SQL Server jobs created on the database instance. The T-SQL code includes the following set of information.
- Server Name
- SQL Job category
- SQL Job Name
- SQL job owner
- Is Job Enabled
- Date and time of next execution
- Date and time of last execution
- Last run status
The query to populate the status of the SQL Jobs is the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
create table #JobInformation (Servername varchar(100), categoryname varchar(100),JobName varchar(500), ownerID varchar(250),Enabled varchar(5),NextRunDate datetime, LastRunDate datetime, status varchar(50) ) Insert into #JobInformation (Servername,categoryname,JobName,ownerID,Enabled,NextRunDate,LastRunDate,status) SELECT convert (varchar, SERVERPROPERTY('Servername')) AS ServerName ,categories.NAME AS CategoryName ,sqljobs.name ,SUSER_SNAME(sqljobs.owner_sid) AS OwnerID ,CASE sqljobs.enabled WHEN 1 THEN 'Yes' ELSE 'No'END AS Enabled ,CASE job_schedule.next_run_date WHEN 0 THEN CONVERT(DATETIME, '1900/1/1') ELSE CONVERT(DATETIME, CONVERT(CHAR(8), job_schedule.next_run_date, 112) + ' ' + STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(8), job_schedule.next_run_time), 6), 5, 0, ':'), 3, 0, ':')) END NextScheduledRunDate ,lastrunjobhistory.LastRunDate ,ISNULL(lastrunjobhistory.run_status_desc,'Unknown') AS run_status_desc FROM msdb.dbo.sysjobs AS sqljobs LEFT JOIN msdb.dbo.sysjobschedules AS job_schedule ON sqljobs.job_id = job_schedule.job_id LEFT JOIN msdb.dbo.sysschedules AS schedule ON job_schedule.schedule_id = schedule.schedule_id INNER JOIN msdb.dbo.syscategories categories ON sqljobs.category_id = categories.category_id LEFT OUTER JOIN ( SELECT Jobhistory.job_id FROM msdb.dbo.sysjobhistory AS Jobhistory WHERE Jobhistory.step_id = 0 GROUP BY Jobhistory.job_id ) AS jobhistory ON jobhistory.job_id = sqljobs.job_id -- to get the average duration LEFT OUTER JOIN ( SELECT sysjobhist.job_id ,CASE sysjobhist.run_date WHEN 0 THEN CONVERT(DATETIME, '1900/1/1') ELSE CONVERT(DATETIME, CONVERT(CHAR(8), sysjobhist.run_date, 112) + ' ' + STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(8), sysjobhist.run_time), 6), 5, 0, ':'), 3, 0, ':')) END AS LastRunDate ,sysjobhist.run_status ,CASE sysjobhist.run_status WHEN 0 THEN 'Failed' WHEN 1 THEN 'Succeeded' WHEN 2 THEN 'Retry' WHEN 3 THEN 'Canceled' WHEN 4 THEN 'In Progress' ELSE 'Unknown' END AS run_status_desc ,sysjobhist.retries_attempted ,sysjobhist.step_id ,sysjobhist.step_name ,sysjobhist.run_duration AS RunTimeInSeconds ,sysjobhist.message ,ROW_NUMBER() OVER ( PARTITION BY sysjobhist.job_id ORDER BY CASE sysjobhist.run_date WHEN 0 THEN CONVERT(DATETIME, '1900/1/1') ELSE CONVERT(DATETIME, CONVERT(CHAR(8), sysjobhist.run_date, 112) + ' ' + STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(8), sysjobhist.run_time), 6), 5, 0, ':'), 3, 0, ':')) END DESC ) AS RowOrder FROM msdb.dbo.sysjobhistory AS sysjobhist WHERE sysjobhist.step_id = 0 --to get just the job outcome and not all steps )AS lastrunjobhistory ON lastrunjobhistory.job_id = sqljobs.job_id -- to get the last run details AND lastrunjobhistory.RowOrder=1 |
Query Output:
Following is the code of the HTML table to display the status of the SQL Jobs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<table id="AutoNumber1" style="BORDER-COLLAPSE: collapse" borderColor="#111111" height="40" cellSpacing="0" cellPadding="0" width="54%" border="1"> <tr> <td width="17%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Server Name</font></b></td> <td width="19%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Job Category </font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Job Name</font></b></td> <td width="15%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Job owner</font></b></td> <td width="5%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Enabled</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Next Run Date</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Last Run Date</font></b></td> <td width="29%" bgColor="#D3D3D3" height="15"><b> <font face="Verdana" size="1" color="#FFFFFF">Status</font></b></td> </tr> <p style="margin-top: 1; margin-bottom: 0"> </p> <p><font face="Verdana" size="4">Backup Details</font></p>' SELECT @DatabaseServerInformation = @DatabaseServerInformation + '<tr><td><font face="Verdana" size="1">' + ISNULL(convert(varchar,Servername),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(convert(nvarchar,categoryname),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(convert(varchar,JobName),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(convert(varchar,ownerID),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(Enabled,'') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(Convert(varchar,NextRunDate,120),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(Convert(varchar,LastRunDate,120),'-') +'</font></td>' + '<td><font face="Verdana" size="1">' + ISNULL(status,'-') +'</font></td></tr>' from #JobInformation |
The HTML table looks like the following image:
- Note: The code is too lengthy, so you can download the script for the article here
Schedule the job
I have created a stored procedure named getdailydummeryreport. The script contains the above code, and the output will be sent to the DBA Support team. To send the output, we are using SQL Server Database Mail. If you are using the SQL Server Express edition, then you can use the Windows task Scheduler to send the output of the procedure. To create the SQL job, Open SQL Server Management Studio Expand SQL Server Agent Right-click on Jobs Select New Job. On the New Job dialog box, provide the desired name of the Job and click ok Steps Click on New.
On the New job step dialog box, provide the desired name of the job step in Step Name text box, select Transact-SQL Script from the Type dialog box, and enter the following code in the command textbox. Click OK to save and close the job step.
To schedule the job, click on Schedule and click New.
On the New Schedule dialog box, configure the frequency and the date-time according to the business requirement. I want to run this report at 7 AM, so the schedule is configured accordingly.
Click OK to save and close the schedule. Click OK to save the SQL Job and close the dialog box. To test the SQL job, you can run the following T-SQL command.
1 |
exec msdb..sp_start_job [Database Summery Report - Daily] |
Once the job is executed successfully, you will receive the email, as shown below.
You can tweak the HTML code to change the format of the HTML tables.
Summary
In this article, I have shown a T-SQL Script that is used to generate a database health report and how it can be scheduled using the SQL Server agent job. In my future articles, I will show more scripts that can help perform daily DBA tasks to increase productivity.
- Different ways to identify and change compatibility levels in SQL Server - July 22, 2024
- Copy SQL Databases between Windows 10 and CentOS using SQL Server data tools - October 19, 2022
- Changing the location of FILESTREAM data files in SQL Database - October 14, 2022