Looking into a performance issue, such as when someone reports “blocking”, an important tool every DBA (in my opinion) should have on their toolbelt is the sp_WhoIsActive stored procedure.

The stored procedure can be run without parameters, however the input parameters available (see documentation for details) provide increased flexibility and/or results.

One of these handy features is storing the results in a table; this combined with other features in SSMS will allow for quick basic monitoring.

Store the data

The following will create a table in the TEMPDB database - since this database is reinitialized when the SQL Server instance is restarted (emphasis: any data in this table will be lost), so make sure to copy the data elsewhere if you want it to be persisted.

CREATE TABLE [tempdb].[dbo].[WhoIsActive] (
    [dd hh:mm:ss.mss] VARCHAR(8000) NULL
     ,[session_id] SMALLINT NOT NULL
    ,[sql_text] XML NULL
    ,[sql_command] XML NULL
    ,[login_name] NVARCHAR(128) NOT NULL
    ,[wait_info] NVARCHAR(4000) NULL
    ,[CPU] VARCHAR(30) NULL
    ,[tempdb_allocations] VARCHAR(30) NULL
    ,[tempdb_current] VARCHAR(30) NULL
    ,[blocking_session_id] SMALLINT NULL
    ,[blocked_session_count] VARCHAR(30) NULL
    ,[reads] VARCHAR(30) NULL
    ,[writes] VARCHAR(30) NULL
    ,[physical_reads] VARCHAR(30) NULL
    ,[query_plan] XML NULL
    ,[used_memory] VARCHAR(30) NULL
    ,[status] VARCHAR(30) NOT NULL
    ,[open_tran_count] VARCHAR(30) NULL
    ,[percent_complete] VARCHAR(30) NULL
    ,[host_name] NVARCHAR(128) NULL
    ,[database_name] NVARCHAR(128) NULL
    ,[program_name] NVARCHAR(128) NULL
    ,[start_time] DATETIME NOT NULL
    ,[login_time] DATETIME NULL
    ,[request_id] INT NULL
    ,[collection_time] DATETIME NOT NULL
    )
GO

This CREATE TABLE script was created using sp_WhoIsActive with the input parameters shown in the below example:

DECLARE @CreateTableSchema varchar(MAX);

EXEC sp_WhoIsActive @get_plans=1,
    @get_outer_command=1,
    @find_block_leaders=1,
    @return_schema = 1,
    @schema = @CreateTableSchema OUTPUT

PRINT @CreateTableSchema;

The PRINT output is returned as a single line, so it was then formatted using the Poor Man’s T-SQL Formatter to make it more readable/presentable.

Run multiple iterations of the sp_WhoIsActive stored procedure

Using a combination of the TSQL WAITFOR DELAY function and the SQL Server Management Studio GO batch seperator, we can run the same command multiple times, evey few seconds.

WAITFOR DELAY '00:00:05';
EXEC sp_WhoIsActive @get_plans=1,
    @get_outer_command=1,
    @find_block_leaders=1,
    @destination_table='tempdb.dbo.WhoIsActive';
GO 200

The above code will run the sp_WhoIsActive stored procedure, with the parameters shown, for 200 times and with a delay of 5 seconds between iterations.

Here is a different and slightly more verbose approach to the above example:

SET NOCOUNT ON;
WAITFOR TIME '16:30:00';
GO
DELCARE @Continue bit = 1;
DECLARE @StopTIme datetime = '2021-02-15 19:30:00';
WHILE (@Continue = 1)
BEGIN
    -- collect a snapshot of running sessions
    EXEC sp_WhoIsActive @get_plans=1,
        @get_outer_command=1,
        @find_block_leaders=1,
        @destination_table='tempdb.dbo.WhoIsActive';
    -- wait 5 seconds
    WAITFOR DELAY '00:00:05';
    -- check time of day
    IF (CURRENT_TIMESTAMP >= @StopTime)
        SET @Continue = 0;
END
GO

We can see that the commands will start execution at 16:30, then run a WHILE loop checking for the value of the @Continue variable, capture the output of the sp_WhoIsActive stored procedure, and finally compare the @StopTime variable value with the current time to determine whether to exit the loop. This is a more flexible approach than the previous example since it allows you to define the capture start and stop times.

Also note that the input parameters used when exeuting the sp_WhoIsActive stored procedure are the same used to generate the CREATE TABLE above. This is because the result set changes depending on the parameters used, so we need the inputs to be consistent.

View the results

Finally we can review the results, which include both the original query executed, the blocking statement, or the statement being blocked, the Execution Plan, the account and host name used to initiate the connection, and other helpful information.

SELECT TOP(100) * FROM tempdb.dbo.WhoIsActive WHERE 1=1
/* add your filters here */
ORDER BY [collection_time] DESC, [dd hh:mm:ss.mss] DESC;

Of course this will return a row for every iteration of the snapshot capture. If we want to limit our results to the most recent record we’d have to aggregate the results as shown below.

WITH cteWhoIsActive AS (
    SELECT * FROM [tempdb].[dbo].[WhoIsActive] WHERE 1=1
    /* add your filters here */
),
cteWhoIsActiveGrouped AS (
    SELECT 
        MAX([dd hh:mm:ss.mss]) AS [dd hh:mm:ss.mss]
        ,[session_id]
        ,CAST([sql_text] AS nvarchar(max)) AS [sql_text]
        ,CAST([sql_command] AS nvarchar(max)) AS [sql_command]
        ,[login_name]
        ,CAST([query_plan] AS nvarchar(max)) AS [query_plan]
        ,[host_name]
        ,[database_name]
        ,[program_name]
        ,[start_time]
        ,[login_time]
        ,[request_id]
        ,MAX([collection_time]) AS [collection_time]
    
    FROM cteWhoIsActive
    
    GROUP BY
        [session_id]
        ,CAST([sql_text] AS nvarchar(max))
        ,CAST([sql_command] AS nvarchar(max))
        ,[login_name]
        ,CAST([query_plan] AS nvarchar(max))
        ,[host_name]
        ,[database_name]
        ,[program_name]
        ,[start_time]
        ,[login_time]
        ,[request_id]
)
SELECT 
    g.[dd hh:mm:ss.mss]
    ,g.[session_id]
    ,CAST(g.[sql_text] AS xml) AS [sql_text]
    ,CAST(g.[sql_command] AS xml) AS [sql_command]
    ,g.[login_name]
    ,w.[wait_info]
    ,w.[CPU]
    ,w.[tempdb_allocations]
    ,w.[tempdb_current]
    ,w.[blocking_session_id]
    ,w.[blocked_session_count]
    ,w.[reads]
    ,w.[writes]
    ,w.[physical_reads]
    ,CAST(g.[query_plan] AS xml) AS [query_plan]
    ,w.[used_memory]
    ,w.[status]
    ,w.[open_tran_count]
    ,w.[percent_complete]
    ,g.[host_name]
    ,g.[database_name]
    ,g.[program_name]
    ,g.[start_time]
    ,g.[login_time]
    ,g.[request_id]
    ,g.[collection_time]

FROM cteWhoIsActiveGrouped g
    INNER JOIN cteWhoIsActive w ON g.[session_id] = w.[session_id] 
        AND w.[collection_time] = g.[collection_time]

ORDER BY g.[collection_time] DESC, g.[dd hh:mm:ss.mss] DESC;

As we can see, we’ve had to sacrifice a few columns in order to group the results; these had to be removed from the Grouping query since they either do not satisfy the rules for the GROUP BY clause, or contain incrementing/varying values which skew the grouping results.

The columns were later added back in by creating a self-join with the original CTE, using columns which are not that efficient.

 

BONUS TIP: It is a good practice to run the stored procedure as typed here: sp_WhoIsActive. Since the stored procedure was created with Capitalization of the letters W, I, ans A, running the SP using lower case letters will throw an “object not found” error where the SQL Server instance was installed with a Case Sensitive Collation.

References

sp_whoisactive Documentation

sp_whoisactive Source Code

WAITFOR (Transact-SQL)

SQL Server Utilities - GO