In a previous post, I presented a solution for monitoring and logging long running queries in SQL Server. In this post I’ll show an extended version of that solution that also sends an email when a request has been blocked for more than 20 seconds.
Blocked Queries
Be sure to read the the original post about long running queries before implementing the solution outlined here.
To a DBA, notifications on blocked queries in a database can be a very useful tool in a proactive work trying to enhance the user experience. In the blog post mentioned above, I showed how to capture and store T-SQL queries that run for longer than 20 seconds.
That solution can be extended to also send an email whenever a long running query is caused by a lock. Most locks are resolved automatically after a while, but in some cases DBA intervention can be necessary. It is also very valuable troubleshooting information for the DBA. This extended solution will send the email when the total blocked_wait_time exceeds 20 seconds.
Note that the goal with this solution, and the one for long running queries, is to catch blocks that affect the user experience. Therefore, there is a rather long list of wait types that are not monitored, meaning not all blocks will be reported. Naturally, the list of excluded wait types can be changed as you see fit.
To use the solution you need to change the @profile_name and @recipients parameters for the sp_send_dbmail procedure:
EXEC msdb.dbo.sp_send_dbmail @profile_name = <YourEmailProfile>, @recipients = <Recipients>, @subject = 'Blocked queries detected!', @body = @MAIL_BODY, @body_format='HTML'
Follow the instructions in the long running queries post, but use this T-SQL instead (remember to change parameters on sp_send_dbmail):
SET QUOTED_IDENTIFIER ON
SET NOCOUNT ON
DECLARE @StartMonitoringAfterSeconds INT = 20, @MAIL_BODY NVARCHAR(4000)
IF (SELECT OBJECT_ID('#TLind_IGNORED_WAIT_TYPES')) IS NULL BEGIN
CREATE TABLE #TLind_IGNORED_WAIT_TYPES
(
WaitType NVARCHAR(60) NOT NULL
)
--http://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/
INSERT #TLind_IGNORED_WAIT_TYPES (WaitType)
VALUES
('BROKER_EVENTHANDLER'),
('BROKER_RECEIVE_WAITFOR'),
('BROKER_TASK_STOP'),
('BROKER_TO_FLUSH'),
('BROKER_TRANSMITTER'),
('CHECKPOINT_QUEUE'),
('CHKPT'),
('CLR_AUTO_EVENT'),
('CLR_MANUAL_EVENT'),
('CLR_SEMAPHORE'),
('DBMIRROR_DBM_EVENT'),
('DBMIRROR_EVENTS_QUEUE'),
('DBMIRROR_WORKER_QUEUE'),
('DBMIRRORING_CMD'),
('DIRTY_PAGE_POLL'),
('DISPATCHER_QUEUE_SEMAPHORE'),
('EXECSYNC'),
('FSAGENT'),
('FT_IFTS_SCHEDULER_IDLE_WAIT'),
('FT_IFTSHC_MUTEX'),
('HADR_CLUSAPI_CALL'),
('HADR_FILESTREAM_IOMGR_IOCOMPLETION'),
('HADR_LOGCAPTURE_WAIT'),
('HADR_NOTIFICATION_DEQUEUE'),
('HADR_TIMER_TASK'),
('HADR_WORK_QUEUE'),
('KSOURCE_WAKEUP'),
('LAZYWRITER_SLEEP'),
('LOGMGR_QUEUE'),
('ONDEMAND_TASK_QUEUE'),
('PWAIT_ALL_COMPONENTS_INITIALIZED'),
('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'),
('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'),
('REQUEST_FOR_DEADLOCK_SEARCH'),
('RESOURCE_QUEUE'),
('SERVER_IDLE_CHECK'),
('SLEEP_BPOOL_FLUSH'),
('SLEEP_DBSTARTUP'),
('SLEEP_DCOMSTARTUP'),
('SLEEP_MASTERDBREADY'),
('SLEEP_MASTERMDREADY'),
('SLEEP_MASTERUPGRADED'),
('SLEEP_MSDBSTARTUP'),
('SLEEP_SYSTEMTASK'),
('SLEEP_TASK'),
('SLEEP_TEMPDBSTARTUP'),
('SNI_HTTP_ACCEPT'),
('SP_SERVER_DIAGNOSTICS_SLEEP'),
('SQLTRACE_BUFFER_FLUSH'),
('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'),
('SQLTRACE_WAIT_ENTRIES'),
('WAIT_FOR_RESULTS'),
('WAITFOR'),
('WAITFOR_TASKSHUTDOWN'),
('WAIT_XTP_HOST_WAIT'),
('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'),
('WAIT_XTP_CKPT_CLOSE'),
('XE_DISPATCHER_JOIN'),
('XE_DISPATCHER_WAIT'),
('XE_TIMER_EVENT')
END
SELECT
R.session_id
,R.request_id
,R.start_time
,CAST('1900-01-01' AS DATETIME2) AS completed_time
,S.[host_name]
,S.[program_name]
,S.login_name
,R.[status]
,R.command
,R.database_id
,R.wait_type
,R.wait_time / 1000 AS blocked_wait_time_sec
,R.last_wait_type
,R.wait_resource
,R.total_elapsed_time / 1000 AS total_elapsed_time_sec
,ST.text
,R.blocking_session_id
,S_B.[host_name] AS blocking_host_name
,S_B.[program_name] AS blocking_program_name
,S_B.login_name AS blocking_login_name
,ST_B.text AS blocking_text
INTO
#TLind_LONG_RUNNING
FROM
sys.dm_exec_requests R
CROSS APPLY
sys.dm_exec_sql_text(R.sql_handle) AS ST
JOIN
sys.dm_exec_sessions S
ON
R.session_id = S.session_id
LEFT OUTER JOIN
sys.dm_exec_connections C
ON
R.blocking_session_id = C.session_id
OUTER APPLY
sys.dm_exec_sql_text(C.most_recent_sql_handle) AS ST_B
LEFT OUTER JOIN
sys.dm_exec_sessions S_B
ON
R.blocking_session_id = S_B.session_id
WHERE
R.[status] NOT IN ('background', 'sleeping')
AND
R.wait_type NOT IN (SELECT WaitType FROM #TLind_IGNORED_WAIT_TYPES)
AND
R.session_id > 50
AND
R.session_id <> @@spid
AND
(R.total_elapsed_time / 1000) > (@StartMonitoringAfterSeconds)
IF EXISTS (SELECT * FROM #TLind_LONG_RUNNING) BEGIN
CREATE UNIQUE CLUSTERED INDEX TLind_LONG_RUNNING_CLU ON #TLind_LONG_RUNNING(session_id, request_id, start_time) WITH IGNORE_DUP_KEY
CREATE TABLE #TLind_BLOCKED_QUERIES
(
session_id SMALLINT NOT NULL
,request_id INT NOT NULL
,start_time DATETIME NOT NULL
,mail_sent BIT DEFAULT(0) NOT NULL
)
CREATE UNIQUE CLUSTERED INDEX TLind_BLOCKED_QUERIES_CLU ON #TLind_BLOCKED_QUERIES(session_id, request_id, start_time) WITH IGNORE_DUP_KEY
WHILE EXISTS(SELECT * FROM #TLind_LONG_RUNNING WHERE completed_time = '1900-01-01') BEGIN
/*********************************
BLOCKED QUERY ACTION
*********************************/
INSERT #TLind_BLOCKED_QUERIES (session_id,request_id,start_time)
SELECT session_id, request_id, start_time
FROM #TLind_LONG_RUNNING
WHERE blocked_wait_time_sec >= 20
AND blocking_session_id > 50
AND blocking_session_id <> session_id
IF @@ROWCOUNT > 0 BEGIN
SET @MAIL_BODY = '<table border="1" align="center" cellpadding="2" cellspacing="0" style="color:black;font-family:consolas;text-align:center;font-size:8.0pt;">' +
'<tr>
<th>Blocked SPID</th>
<th>Blocked Login</th>
<th>Blocked Statement</th>
<th>Blocked Statement Time</th>
<th>blocked_wait_time_sec</th>
<th>Blocking SPID</th>
<th>Blocking Login</th>
<th>Blocking Statement</th>'
SELECT
@MAIL_BODY = @MAIL_BODY +
'<tr>' +
'<td>' + CAST(BQ.session_id AS NVARCHAR(5)) + '</td>' +
'<td>' + LR.login_name + '</td>' +
'<td>' + CAST(ISNULL(LR.text, '...') AS NVARCHAR(100)) + '</td>' +
'<td>' + CONVERT(NVARCHAR(23), LR.start_time, 121) + '</td>' +
'<td>' + CAST(LR.blocked_wait_time_sec AS NVARCHAR(10)) + '</td>' +
'<td>' + CAST(ISNULL(LR.blocking_session_id, 0) AS NVARCHAR(5)) + '</td>' +
'<td>' + ISNULL(LR.blocking_login_name, '...') + '</td>' +
'<td>' + CAST(ISNULL(LR.blocking_text, '...') AS NVARCHAR(100)) + '</td>'
FROM
#TLind_BLOCKED_QUERIES BQ
JOIN
#TLind_LONG_RUNNING LR
ON
BQ.session_id = LR.session_id
AND
BQ.request_id = LR.request_id
AND
BQ.start_time = LR.start_time
WHERE
BQ.mail_sent = 0
ORDER BY
LR.start_time DESC
UPDATE #TLind_BLOCKED_QUERIES SET mail_sent = 0
EXEC msdb.dbo.sp_send_dbmail
@profile_name = <YourEmailProfile>,
@recipients = <Recipients>,
@subject = 'Blocked queries detected!',
@body = @MAIL_BODY,
@body_format='HTML'
END
WAITFOR DELAY '00:00:01'
UPDATE
#TLind_LONG_RUNNING
SET
completed_time = CASE WHEN R.session_id IS NULL AND LR.completed_time = '1900-01-01' THEN GETDATE() ELSE LR.completed_time END
,[host_name] = COALESCE(S.[host_name], LR.[host_name])
,[program_name] = COALESCE(S.[program_name], LR.[program_name])
,login_name = COALESCE(S.[login_name], LR.[login_name])
,[status] = COALESCE(R.[status], LR.[status])
,command = COALESCE(R.command, LR.command)
,database_id = COALESCE(R.database_id, LR.database_id)
,wait_type = COALESCE(R.wait_type, LR.wait_type)
,blocked_wait_time_sec = COALESCE(R.wait_time / 1000, LR.blocked_wait_time_sec)
,last_wait_type = COALESCE(R.last_wait_type, LR.last_wait_type)
,wait_resource = COALESCE(R.wait_resource, LR.wait_resource)
,total_elapsed_time_sec = COALESCE(R.total_elapsed_time / 1000, LR.total_elapsed_time_sec)
,text = COALESCE(ST.text, LR.text)
,blocking_session_id = COALESCE(NULLIF(R.blocking_session_id, 0), LR.blocking_session_id)
,blocking_host_name = COALESCE(S_B.[host_name], LR.blocking_host_name)
,blocking_program_name = COALESCE(S_B.[program_name], LR.blocking_program_name)
,blocking_login_name = COALESCE(S_B.login_name, LR.blocking_login_name)
,blocking_text = COALESCE(ST_B.text, LR.blocking_text)
FROM
#TLind_LONG_RUNNING LR
LEFT OUTER JOIN
sys.dm_exec_requests R
ON
LR.session_id = R.session_id
AND
LR.request_id = R.request_id
AND
LR.start_time = R.start_time
OUTER APPLY
sys.dm_exec_sql_text(R.sql_handle) AS ST
LEFT OUTER JOIN
sys.dm_exec_sessions S
ON
R.session_id = S.session_id
LEFT OUTER JOIN
sys.dm_exec_connections C
ON
R.blocking_session_id = C.session_id
OUTER APPLY
sys.dm_exec_sql_text(C.most_recent_sql_handle) AS ST_B
LEFT OUTER JOIN
sys.dm_exec_sessions S_B
ON
R.blocking_session_id = S_B.session_id
INSERT
#TLind_LONG_RUNNING
SELECT
R.session_id
,R.request_id
,R.start_time
,CAST('1900-01-01' AS DATETIME2) AS completed_time
,S.[host_name]
,S.[program_name]
,S.login_name
,R.[status]
,R.command
,R.database_id
,R.wait_type
,R.wait_time / 1000 AS blocked_wait_time_sec
,R.last_wait_type
,R.wait_resource
,R.total_elapsed_time / 1000 AS total_elapsed_time_sec
,ST.text
,R.blocking_session_id
,S_B.[host_name] AS blocking_host_name
,S_B.[program_name] AS blocking_program_name
,S_B.login_name AS blocking_login_name
,ST_B.text AS blocking_text
FROM
sys.dm_exec_requests R
CROSS APPLY
sys.dm_exec_sql_text(R.sql_handle) AS ST
JOIN
sys.dm_exec_sessions S
ON
R.session_id = S.session_id
LEFT OUTER JOIN
sys.dm_exec_connections C
ON
R.blocking_session_id = C.session_id
OUTER APPLY
sys.dm_exec_sql_text(C.most_recent_sql_handle) AS ST_B
LEFT OUTER JOIN
sys.dm_exec_sessions S_B
ON
R.blocking_session_id = S_B.session_id
WHERE
R.[status] NOT IN ('background', 'sleeping')
AND
R.wait_type NOT IN (SELECT WaitType FROM #TLind_IGNORED_WAIT_TYPES)
AND
R.session_id > 50
AND
R.session_id <> @@spid
AND
(R.total_elapsed_time / 1000) > (@StartMonitoringAfterSeconds)
END
IF (SELECT OBJECT_ID('TLind_LONG_RUNNING')) IS NULL BEGIN
CREATE TABLE TLind_LONG_RUNNING
(
session_id SMALLINT NOT NULL
,request_id INT NOT NULL
,start_time DATETIME NOT NULL
,completed_time DATETIME2 NOT NULL
,[host_name] NVARCHAR(128) NULL
,[program_name] NVARCHAR(128) NULL
,login_name NVARCHAR(128) NOT NULL
,[status] NVARCHAR(30) NOT NULL
,command NVARCHAR(32) NOT NULL
,database_id SMALLINT NOT NULL
,wait_type NVARCHAR(60) NULL
,blocked_wait_time_sec INT NOT NULL
,last_wait_type NVARCHAR(60) NOT NULL
,wait_resource NVARCHAR(256) NOT NULL
,total_elapsed_time_sec INT NOT NULL
,text NVARCHAR(MAX) NULL
,blocking_session_id SMALLINT NULL
,blocking_host_name NVARCHAR(128) NULL
,blocking_program_name NVARCHAR(128) NULL
,blocking_login_name NVARCHAR(128) NULL
,blocking_text NVARCHAR(MAX) NULL
)
END
INSERT TLind_LONG_RUNNING
SELECT * FROM #TLind_LONG_RUNNING
END
DROP TABLE #TLind_LONG_RUNNING
IF (SELECT OBJECT_ID('#TLind_BLOCKED_QUERIES')) IS NOT NULL BEGIN
DROP TABLE #TLind_BLOCKED_QUERIES
END
If you want to do something else instead of sending an email when a blocked query is encountered, just change the highlighted code above to something else.
As always, make sure to test the solution before running it in a production environment.