DBA Scripts: Collect Blocking and Deadlock Events

🔧Part of the DBA-Tools Project, copy/paste SQL Server scripts and health checks.In: Maintenance & AutomationCollectors & Baselines

Standing Watch for Blocking and Deadlocks, Not Just Checking When Asked

Get Blocking Sessions and Get Deadlock Summary answer “what’s happening right now.” These two generators answer a different question: “what happened while nobody was looking.” Each creates a small recurring SQL Agent job that writes evidence into a DBAMonitor table, collector.Blocking every 2 minutes, collector.Deadlocks every 5, both idempotent, both cheap enough to leave running permanently.


Why a Standing Collector Matters Here

  • Blocking and deadlocks are transient by nature. By the time someone reports “the app was slow ten minutes ago,” the evidence in sys.dm_exec_requests is already gone. A collector is the only way to have it later.
  • Both write nothing when there’s nothing to write. The blocking collector filters on blocking_session_id > 0, the deadlock collector only inserts events newer than the last one seen, a quiet server produces an empty table, not noise.
  • Deadlock XML is preserved in full, not summarized, the actual deadlock_xml column is there for real post-incident investigation, not just a count.

Generate-CollectorJob-Blocking.sql

Creates DBAMonitor and collector.Blocking if absent, then a job that captures every active blocking chain, blocked and blocking SPID, wait type and resource, the blocked statement, and the blocker’s last statement, every 2 minutes. Writes nothing when the server is quiet.

/*
Script Name : Generate-CollectorJob-Blocking
Category    : collectors
Purpose     : Generates DDL to create the DBA - Collect Blocking SQL Agent job.
              Creates the target database and collector.Blocking table if absent,
              then outputs T-SQL to install a recurring blocking-chain collection job.
              The job step inserts rows only when active blocking exists.
              Edit parameters, review output, then run on the target instance.
Author      : Peter Whyte (https://sqldba.blog)
Requires    : sysadmin (to run generated DDL); VIEW SERVER STATE at job runtime
Notes       : Default interval: every 2 minutes. On quiet servers the job runs but
              inserts nothing — blocking_session_id > 0 filter suppresses empty writes.
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;

-- ── Parameters ────────────────────────────────────────────────────────────────
DECLARE @TargetDatabase  sysname       = N'DBAMonitor';   -- created if absent
DECLARE @JobOwner        sysname       = N'sa';
DECLARE @CategoryName    nvarchar(128) = N'DBA Collectors';
DECLARE @IntervalMinutes int           = 2;
-- ─────────────────────────────────────────────────────────────────────────────

DECLARE @q       nchar(1)      = NCHAR(39);
DECLARE @crlf    nvarchar(2)   = CHAR(13) + CHAR(10);
DECLARE @ddl     nvarchar(max) = N'';
DECLARE @jobName sysname       = N'DBA - Collect Blocking';
DECLARE @stepCmd nvarchar(max);

-- ── Step command (| = single-quote placeholder) ────────────────────────────────
SET @stepCmd = REPLACE(
N'SET NOCOUNT ON;
WITH blocking_chain AS (
    SELECT
        r.session_id                AS blocked_spid,
        r.blocking_session_id       AS blocking_spid,
        r.wait_type,
        r.wait_time                 AS wait_time_ms,
        r.wait_resource,
        r.status,
        r.command,
        r.database_id,
        r.open_transaction_count,
        r.total_elapsed_time        AS elapsed_ms,
        s.login_name,
        s.host_name,
        s.program_name,
        r.sql_handle
    FROM sys.dm_exec_requests r
    JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id
    WHERE r.blocking_session_id > 0
)
INSERT INTO [<<DB>>].[collector].[Blocking]
    (server_name, collection_time, blocked_spid, blocking_spid, is_head_blocker,
     wait_type, wait_time_ms, wait_resource, status, command, database_name,
     open_transaction_count, elapsed_ms, login_name, host_name, program_name,
     blocked_statement, blocker_last_statement)
SELECT
    @@SERVERNAME                                            AS server_name,
    GETDATE()                                               AS collection_time,
    bc.blocked_spid,
    bc.blocking_spid,
    CASE WHEN NOT EXISTS (
             SELECT 1 FROM sys.dm_exec_requests r2
             WHERE r2.session_id = bc.blocking_spid
               AND r2.blocking_session_id > 0)
         THEN 1 ELSE 0 END                                  AS is_head_blocker,
    bc.wait_type,
    bc.wait_time_ms,
    bc.wait_resource,
    bc.status,
    bc.command,
    DB_NAME(bc.database_id)                                 AS database_name,
    bc.open_transaction_count,
    bc.elapsed_ms,
    bc.login_name,
    bc.host_name,
    bc.program_name,
    SUBSTRING(REPLACE(REPLACE(st_blocked.text, CHAR(13), | |), CHAR(10), | |),
              (bc.blocked_spid % 128) + 1, 1000)            AS blocked_statement,
    SUBSTRING(REPLACE(REPLACE(ISNULL(st_blocker.text, |(no active request)|),
              CHAR(13), | |), CHAR(10), | |), 1, 500)       AS blocker_last_statement
FROM blocking_chain bc
OUTER APPLY sys.dm_exec_sql_text(bc.sql_handle) AS st_blocked
OUTER APPLY (
    SELECT TOP 1 st2.text
    FROM sys.dm_exec_requests r2
    OUTER APPLY sys.dm_exec_sql_text(r2.sql_handle) AS st2
    WHERE r2.session_id = bc.blocking_spid
) AS st_blocker;'
, N'|', NCHAR(39));

SET @stepCmd = REPLACE(@stepCmd, N'<<DB>>', @TargetDatabase);

-- ═══════════════════════════════════════════════════════════════════════════════
-- DDL output
-- ═══════════════════════════════════════════════════════════════════════════════
SET @ddl =
    N'-- ================================================================' + @crlf +
    N'-- Generated by Generate-CollectorJob-Blocking.sql'                  + @crlf +
    N'-- Server    : ' + @@SERVERNAME                                      + @crlf +
    N'-- Target DB : ' + @TargetDatabase                                   + @crlf +
    N'-- Generated : ' + CONVERT(nvarchar(20), GETDATE(), 120)             + @crlf +
    N'-- ================================================================' + @crlf + @crlf;

-- ── 1. Target database ────────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = N' + @q + @TargetDatabase + @q + N')' + @crlf +
    N'    CREATE DATABASE [' + @TargetDatabase + N'];'                                               + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 2. Collector schema ───────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (SELECT 1 FROM [' + @TargetDatabase + N'].sys.schemas WHERE name = N' + @q + N'collector' + @q + N')' + @crlf +
    N'    EXEC [' + @TargetDatabase + N'].sys.sp_executesql N' + @q + N'CREATE SCHEMA collector' + @q + N';'              + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 3. Blocking table ─────────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (' + @crlf +
    N'    SELECT 1 FROM [' + @TargetDatabase + N'].sys.objects o'                                                                   + @crlf +
    N'    JOIN [' + @TargetDatabase + N'].sys.schemas s ON s.schema_id = o.schema_id'                                              + @crlf +
    N'    WHERE o.name = N' + @q + N'Blocking' + @q + N' AND s.name = N' + @q + N'collector' + @q + N')'                          + @crlf +
    N'CREATE TABLE [' + @TargetDatabase + N'].[collector].[Blocking] ('                                                             + @crlf +
    N'    id                      bigint IDENTITY(1,1) PRIMARY KEY,'                                                                + @crlf +
    N'    server_name             nvarchar(128) NOT NULL,'                                                                           + @crlf +
    N'    collection_time         datetime2     NOT NULL,'                                                                           + @crlf +
    N'    blocked_spid            smallint,'                                                                                         + @crlf +
    N'    blocking_spid           smallint,'                                                                                         + @crlf +
    N'    is_head_blocker         bit,'                                                                                              + @crlf +
    N'    wait_type               nvarchar(60),'                                                                                     + @crlf +
    N'    wait_time_ms            bigint,'                                                                                           + @crlf +
    N'    wait_resource           nvarchar(256),'                                                                                    + @crlf +
    N'    status                  nvarchar(30),'                                                                                     + @crlf +
    N'    command                 nvarchar(32),'                                                                                     + @crlf +
    N'    database_name           nvarchar(128),'                                                                                    + @crlf +
    N'    open_transaction_count  int,'                                                                                              + @crlf +
    N'    elapsed_ms              bigint,'                                                                                           + @crlf +
    N'    login_name              nvarchar(128),'                                                                                    + @crlf +
    N'    host_name               nvarchar(128),'                                                                                    + @crlf +
    N'    program_name            nvarchar(128),'                                                                                    + @crlf +
    N'    blocked_statement       nvarchar(1000),'                                                                                   + @crlf +
    N'    blocker_last_statement  nvarchar(500)'                                                                                     + @crlf +
    N');'                                                                                                                            + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 4. Agent category ─────────────────────────────────────────────────────────
SET @ddl +=
    N'USE msdb;' + @crlf +
    N'GO' + @crlf + @crlf +
    N'IF NOT EXISTS (SELECT 1 FROM msdb.dbo.syscategories WHERE name = N' + @q + @CategoryName + @q + N' AND category_class = 1)' + @crlf +
    N'    EXEC msdb.dbo.sp_add_category'                                                                                             + @crlf +
    N'        @class = N' + @q + N'JOB' + @q + N', @type = N' + @q + N'LOCAL' + @q + N', @name = N' + @q + @CategoryName + @q + N';' + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 5. Job + step + schedule ──────────────────────────────────────────────────
SET @ddl +=
    N'-- Job: ' + @jobName + @crlf +
    N'IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N' + @q + @jobName + @q + N')'  + @crlf +
    N'    EXEC msdb.dbo.sp_delete_job @job_name = N' + @q + @jobName + @q + N', @delete_unused_schedule = 1;' + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_job'                                                                  + @crlf +
    N'    @job_name         = N' + @q + @jobName + @q + N','                                     + @crlf +
    N'    @enabled          = 1,'                                                                 + @crlf +
    N'    @owner_login_name = N' + @q + @JobOwner + @q + N','                                    + @crlf +
    N'    @category_name    = N' + @q + @CategoryName + @q + N';'                                + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_jobstep'                                                              + @crlf +
    N'    @job_name          = N' + @q + @jobName + @q + N','                                    + @crlf +
    N'    @step_id           = 1,'                                                                + @crlf +
    N'    @step_name         = N' + @q + N'Capture blocking chains' + @q + N','                  + @crlf +
    N'    @subsystem         = N' + @q + N'TSQL' + @q + N','                                     + @crlf +
    N'    @database_name     = N' + @q + N'master' + @q + N','                                   + @crlf +
    N'    @command           = N' + @q + REPLACE(@stepCmd, @q, @q + @q) + @q + N','              + @crlf +
    N'    @retry_attempts    = 0,'                                                                + @crlf +
    N'    @on_success_action = 1,'                                                                + @crlf +
    N'    @on_fail_action    = 2;'                                                                + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_schedule'                                                             + @crlf +
    N'    @schedule_name        = N' + @q + @jobName + N' Every ' + CAST(@IntervalMinutes AS nvarchar(5)) + N'min' + @q + N',' + @crlf +
    N'    @freq_type            = 4,'                                                            + @crlf +   -- daily recurring
    N'    @freq_interval        = 1,'                                                            + @crlf +
    N'    @freq_subday_type     = 4,'                                                            + @crlf +   -- minutes
    N'    @freq_subday_interval = ' + CAST(@IntervalMinutes AS nvarchar(5)) + N';'              + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_attach_schedule'                                                          + @crlf +
    N'    @job_name      = N' + @q + @jobName + @q + N','                                        + @crlf +
    N'    @schedule_name = N' + @q + @jobName + N' Every ' + CAST(@IntervalMinutes AS nvarchar(5)) + N'min' + @q + N';' + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_jobserver @job_name = N' + @q + @jobName + @q + N';'                 + @crlf +
    N'GO' + @crlf;

SELECT @ddl AS ddl;

Generate-CollectorJob-Deadlocks.sql

Reads the system_health Extended Events ring buffer (already running by default, no session setup needed) and inserts only deadlock events newer than the last one already stored, every 5 minutes.

/*
Script Name : Generate-CollectorJob-Deadlocks
Category    : collectors
Purpose     : Generates DDL to create the DBA - Collect Deadlocks SQL Agent job.
              Creates the target database and collector.Deadlocks table if absent,
              then outputs T-SQL to install a recurring deadlock collection job.
              Reads the system_health XEvent ring buffer (~250 events, no session setup
              required) and inserts only events newer than the latest deadlock_time
              already stored for this server, preventing duplicate rows.
              Edit parameters, review output, then run on the target instance.
Author      : Peter Whyte (https://sqldba.blog)
Requires    : sysadmin (to run generated DDL); VIEW SERVER STATE at job runtime
Notes       : Default interval: every 5 minutes.
              Full deadlock XML preserved in deadlock_xml for post-incident investigation.
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;

-- ── Parameters ────────────────────────────────────────────────────────────────
DECLARE @TargetDatabase  sysname       = N'DBAMonitor';   -- created if absent
DECLARE @JobOwner        sysname       = N'sa';
DECLARE @CategoryName    nvarchar(128) = N'DBA Collectors';
DECLARE @IntervalMinutes int           = 5;
-- ─────────────────────────────────────────────────────────────────────────────

DECLARE @q       nchar(1)      = NCHAR(39);
DECLARE @crlf    nvarchar(2)   = CHAR(13) + CHAR(10);
DECLARE @ddl     nvarchar(max) = N'';
DECLARE @jobName sysname       = N'DBA - Collect Deadlocks';
DECLARE @stepCmd nvarchar(max);

-- ── Step command (| = single-quote placeholder) ────────────────────────────────
-- @since: latest deadlock_time already stored, or 24h ago as bootstrap.
-- Ring buffer holds ~250 events; on very busy servers events may be lost between runs.
-- SET QUOTED_IDENTIFIER ON is required: Agent T-SQL steps default it OFF, and the
-- XML .value()/.query() methods below fail with error 1934 without it.
SET @stepCmd = REPLACE(
N'SET QUOTED_IDENTIFIER ON;
SET NOCOUNT ON;
DECLARE @since datetime2;
SELECT @since = MAX(deadlock_time)
FROM [<<DB>>].[collector].[Deadlocks]
WHERE server_name = @@SERVERNAME;

IF @since IS NULL
    SET @since = DATEADD(HOUR, -24, GETDATE());

;WITH ring_buffer AS (
    SELECT
        -- XE timestamps are UTC; convert to server-local time using the current offset
        DATEADD(MINUTE, DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()), xdr.value(|@timestamp|, |datetime2|)) AS event_time,
        xdr.query(|.|) AS deadlock_xml
    FROM (
        SELECT CAST(target_data AS XML) AS target_xml
        FROM sys.dm_xe_session_targets t
        JOIN sys.dm_xe_sessions s ON s.address = t.event_session_address
        WHERE s.name = |system_health| AND t.target_name = |ring_buffer|
    ) AS rb
    CROSS APPLY target_xml.nodes(|//RingBufferTarget/event[@name="xml_deadlock_report"]|) AS xn(xdr)
)
INSERT INTO [<<DB>>].[collector].[Deadlocks]
    (server_name, collection_time, deadlock_time, victim_process_id,
     victim_spid, victim_login, victim_statement, process_count, deadlock_xml)
SELECT
    @@SERVERNAME                                                    AS server_name,
    GETDATE()                                                       AS collection_time,
    rb.event_time                                                   AS deadlock_time,
    rb.deadlock_xml.value(|(//deadlock/victim-list/victimProcess/@id)[1]|, |nvarchar(50)|)  AS victim_process_id,
    rb.deadlock_xml.value(|(//deadlock/process-list/process[@id = (//deadlock/victim-list/victimProcess/@id)[1]]/@spid)[1]|, |int|) AS victim_spid,
    rb.deadlock_xml.value(|(//deadlock/process-list/process[@id = (//deadlock/victim-list/victimProcess/@id)[1]]/@loginname)[1]|, |nvarchar(128)|) AS victim_login,
    REPLACE(REPLACE(
        rb.deadlock_xml.value(|(//deadlock/process-list/process[@id = (//deadlock/victim-list/victimProcess/@id)[1]]/inputbuf)[1]|, |nvarchar(1000)|),
        CHAR(13), | |), CHAR(10), | |)                             AS victim_statement,
    rb.deadlock_xml.value(|count(//deadlock/process-list/process)|, |int|) AS process_count,
    CAST(rb.deadlock_xml AS nvarchar(max))                         AS deadlock_xml
FROM ring_buffer rb
WHERE rb.event_time > @since;'
, N'|', NCHAR(39));

SET @stepCmd = REPLACE(@stepCmd, N'<<DB>>', @TargetDatabase);

-- ═══════════════════════════════════════════════════════════════════════════════
-- DDL output
-- ═══════════════════════════════════════════════════════════════════════════════
SET @ddl =
    N'-- ================================================================' + @crlf +
    N'-- Generated by Generate-CollectorJob-Deadlocks.sql'                 + @crlf +
    N'-- Server    : ' + @@SERVERNAME                                      + @crlf +
    N'-- Target DB : ' + @TargetDatabase                                   + @crlf +
    N'-- Generated : ' + CONVERT(nvarchar(20), GETDATE(), 120)             + @crlf +
    N'-- ================================================================' + @crlf + @crlf;

-- ── 1. Target database ────────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = N' + @q + @TargetDatabase + @q + N')' + @crlf +
    N'    CREATE DATABASE [' + @TargetDatabase + N'];'                                               + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 2. Collector schema ───────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (SELECT 1 FROM [' + @TargetDatabase + N'].sys.schemas WHERE name = N' + @q + N'collector' + @q + N')' + @crlf +
    N'    EXEC [' + @TargetDatabase + N'].sys.sp_executesql N' + @q + N'CREATE SCHEMA collector' + @q + N';'              + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 3. Deadlocks table ────────────────────────────────────────────────────────
SET @ddl +=
    N'IF NOT EXISTS (' + @crlf +
    N'    SELECT 1 FROM [' + @TargetDatabase + N'].sys.objects o'                                                        + @crlf +
    N'    JOIN [' + @TargetDatabase + N'].sys.schemas s ON s.schema_id = o.schema_id'                                    + @crlf +
    N'    WHERE o.name = N' + @q + N'Deadlocks' + @q + N' AND s.name = N' + @q + N'collector' + @q + N')'              + @crlf +
    N'CREATE TABLE [' + @TargetDatabase + N'].[collector].[Deadlocks] ('                                                 + @crlf +
    N'    id                bigint IDENTITY(1,1) PRIMARY KEY,'                                                            + @crlf +
    N'    server_name       nvarchar(128) NOT NULL,'                                                                       + @crlf +
    N'    collection_time   datetime2     NOT NULL,'                                                                       + @crlf +
    N'    deadlock_time     datetime2,'                                                                                     + @crlf +
    N'    victim_process_id nvarchar(50),'                                                                                 + @crlf +
    N'    victim_spid       int,'                                                                                           + @crlf +
    N'    victim_login      nvarchar(128),'                                                                                 + @crlf +
    N'    victim_statement  nvarchar(1000),'                                                                               + @crlf +
    N'    process_count     int,'                                                                                           + @crlf +
    N'    deadlock_xml      nvarchar(max)'                                                                                  + @crlf +
    N');'                                                                                                                  + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 4. Agent category ─────────────────────────────────────────────────────────
SET @ddl +=
    N'USE msdb;' + @crlf +
    N'GO' + @crlf + @crlf +
    N'IF NOT EXISTS (SELECT 1 FROM msdb.dbo.syscategories WHERE name = N' + @q + @CategoryName + @q + N' AND category_class = 1)' + @crlf +
    N'    EXEC msdb.dbo.sp_add_category'                                                                                             + @crlf +
    N'        @class = N' + @q + N'JOB' + @q + N', @type = N' + @q + N'LOCAL' + @q + N', @name = N' + @q + @CategoryName + @q + N';' + @crlf +
    N'GO' + @crlf + @crlf;

-- ── 5. Job + step + schedule ──────────────────────────────────────────────────
SET @ddl +=
    N'-- Job: ' + @jobName + @crlf +
    N'IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N' + @q + @jobName + @q + N')' + @crlf +
    N'    EXEC msdb.dbo.sp_delete_job @job_name = N' + @q + @jobName + @q + N', @delete_unused_schedule = 1;' + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_job'                                                    + @crlf +
    N'    @job_name         = N' + @q + @jobName + @q + N','                       + @crlf +
    N'    @enabled          = 1,'                                                   + @crlf +
    N'    @owner_login_name = N' + @q + @JobOwner + @q + N','                      + @crlf +
    N'    @category_name    = N' + @q + @CategoryName + @q + N';'                  + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_jobstep'                                                + @crlf +
    N'    @job_name          = N' + @q + @jobName + @q + N','                      + @crlf +
    N'    @step_id           = 1,'                                                  + @crlf +
    N'    @step_name         = N' + @q + N'Collect new deadlock events' + @q + N',' + @crlf +
    N'    @subsystem         = N' + @q + N'TSQL' + @q + N','                       + @crlf +
    N'    @database_name     = N' + @q + N'master' + @q + N','                     + @crlf +
    N'    @command           = N' + @q + REPLACE(@stepCmd, @q, @q + @q) + @q + N',' + @crlf +
    N'    @retry_attempts    = 0,'                                                  + @crlf +
    N'    @on_success_action = 1,'                                                  + @crlf +
    N'    @on_fail_action    = 2;'                                                  + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_schedule'                                               + @crlf +
    N'    @schedule_name        = N' + @q + @jobName + N' Every ' + CAST(@IntervalMinutes AS nvarchar(5)) + N'min' + @q + N',' + @crlf +
    N'    @freq_type            = 4,'                                               + @crlf +   -- daily recurring
    N'    @freq_interval        = 1,'                                               + @crlf +
    N'    @freq_subday_type     = 4,'                                               + @crlf +   -- minutes
    N'    @freq_subday_interval = ' + CAST(@IntervalMinutes AS nvarchar(5)) + N';' + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_attach_schedule'                                            + @crlf +
    N'    @job_name      = N' + @q + @jobName + @q + N','                          + @crlf +
    N'    @schedule_name = N' + @q + @jobName + N' Every ' + CAST(@IntervalMinutes AS nvarchar(5)) + N'min' + @q + N';' + @crlf + @crlf +

    N'EXEC msdb.dbo.sp_add_jobserver @job_name = N' + @q + @jobName + @q + N';'   + @crlf +
    N'GO' + @crlf;

SELECT @ddl AS ddl;

What You Will See First

Both tables will be empty after you install the jobs, and they will stay empty for as long as the server is healthy. That is the design, not a broken collector: these jobs write a row only when there is a real blocking chain or a real deadlock to record. The first rows you see will be your first genuine incident, which is exactly when you want them. If you want to see the same detection logic against the current moment rather than on a schedule, run Get Blocking Sessions or Get Deadlock Summary directly.


How To Run From The Repo

git clone https://github.com/peterwhyte-lgtm/dba-tools
cd dba-tools
.\Initialize-Environment.ps1

.\run.ps1 Generate-CollectorJob-Blocking
.\run.ps1 Generate-CollectorJob-Deadlocks
# Review the generated DDL, then run it on the target instance

These scripts live in the repo at:


Understanding the Results

  • An empty collector.Blocking or collector.Deadlocks table over a long period is a genuinely good sign, not evidence the collector isn’t working, confirm the job is enabled and has recent run history in Get Maintenance Job Status before assuming it’s broken.
  • is_head_blocker in the Blocking table matters more than raw row count. A single head blocker holding up ten sessions produces ten rows, all pointing back to one root cause, not ten separate problems.
  • deadlock_xml is the thing worth actually reading, the summary columns tell you a deadlock happened, the XML tells you which two statements actually collided.

Best Practices

  • Leave both collectors running permanently rather than standing them up only during an investigation, the value is having history from before you knew you needed it.
  • Pair with Generate Collector Alerts so a real blocking event or deadlock surfaces proactively instead of waiting to be queried.
  • Review the deadlock table periodically even without an active incident, a recurring deadlock pattern between the same two objects is worth fixing before it becomes a production outage.
  • Don’t shorten the blocking interval much below 2 minutes on a busy server without checking the collector job’s own overhead first, it’s cheap, but not free.

Related Scripts

You may also find these scripts useful:


Frequently Asked Questions

Does the blocking collector capture every blocking event, or just long ones?

Every one, there’s no minimum duration filter, any session with blocking_session_id > 0 at the moment the job runs gets captured. Very short blocking between polling intervals can still be missed, this is a snapshot collector, not a continuous trace.

Will the deadlock collector insert duplicate rows if I run the job manually while the schedule is also active?

No, it filters on deadlock_time newer than the latest one already stored per server, running it twice in quick succession with nothing new happening simply inserts zero rows the second time.


Summary

Two small, idempotent collectors that turn transient blocking and deadlock events into permanent history instead of evidence that’s gone by the time someone asks about it. Both generate clean DDL, confirmed by actually creating the database, schema, tables, and jobs on a real instance. Populating them with a genuine captured event reuses the same detection logic already proven in the standalone Blocking Sessions and Deadlock Summary posts, not repeated here.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *