0

I'm monitoring my SQL Server environments (local setup, various versions like SQL Server 2017, 2019, 2022) and frequently encounter sessions that appear as sleeping but have sql_text and sql_command as NULL or empty. enter image description here

I've attached an image (refer to your screenshot here) that illustrates an example. In it, session_id 73 has a 'sleeping' status, but the sql_command and sql_text columns are empty, while database_name is 'master'. The duration of this idle session is already quite significant (42 seconds in the example).

The script I use to gather this information is based on SQL Server DMVs and is quite similar to what tools like sp_whoisactive provide. The relevant part of the script that attempts to retrieve the query text is as follows:

SELECT
    RIGHT('00' + CAST(DATEDIFF(SECOND, COALESCE(B.start_time, A.login_time), GETDATE()) / 86400 AS VARCHAR), 2) + ' ' + 
    RIGHT('00' + CAST((DATEDIFF(SECOND, COALESCE(B.start_time, A.login_time), GETDATE()) / 3600) % 24 AS VARCHAR), 2) + ':' + 
    RIGHT('00' + CAST((DATEDIFF(SECOND, COALESCE(B.start_time, A.login_time), GETDATE()) / 60) % 60 AS VARCHAR), 2) + ':' + 
    RIGHT('00' + CAST(DATEDIFF(SECOND, COALESCE(B.start_time, A.login_time), GETDATE()) % 60 AS VARCHAR), 2) + '.' + 
    RIGHT('000' + CAST(DATEDIFF(SECOND, COALESCE(B.start_time, A.login_time), GETDATE()) AS VARCHAR), 3) 
    AS Duration,
    B.command,
    A.session_id AS session_id,
    TRY_CAST('<?query --' + CHAR(10) + (
        SELECT TOP 1 SUBSTRING(X.[text], B.statement_start_offset / 2 + 1, ((CASE
                                                                          WHEN B.statement_end_offset = -1 THEN (LEN(CONVERT(NVARCHAR(MAX), X.[text])) * 2)
                                                                          ELSE B.statement_end_offset
                                                                      END
                                                                     ) - B.statement_start_offset
                                                                    ) / 2 + 1
                     )
    ) + CHAR(10) + '--?>' AS XML) AS sql_text,
    TRY_CAST('<?query --' + CHAR(10) + X.[text] + CHAR(10) + '--?>' AS XML) AS sql_command,
    FORMAT(COALESCE(B.cpu_time, 0), '###,###,###,###,###,###,###,##0') AS CPU,
    FORMAT(COALESCE(B.granted_query_memory, 0), '###,###,###,###,###,###,###,##0') AS used_memory,
    'KILL ' + CAST(A.session_id AS VARCHAR(10)) AS kill_command,
    (CASE 
        WHEN B.[deadlock_priority] <= -5 THEN 'Low'
        WHEN B.[deadlock_priority] > -5 AND B.[deadlock_priority] < 5 AND B.[deadlock_priority] < 5 THEN 'Normal'
        WHEN B.[deadlock_priority] >= 5 THEN 'High'
    END) + ' (' + CAST(B.[deadlock_priority] AS VARCHAR(3)) + ')' AS [deadlock_priority],
    A.[status],
    A.[host_name],
    COALESCE(DB_NAME(CAST(B.database_id AS VARCHAR)), 'master') AS [database_name],
    COALESCE(B.start_time, A.last_request_end_time) AS start_time,
    W.query_plan
FROM
    sys.dm_exec_sessions AS A WITH (NOLOCK)
    LEFT JOIN sys.dm_exec_requests AS B WITH (NOLOCK) ON A.session_id = B.session_id
    JOIN sys.dm_exec_connections AS C WITH (NOLOCK) ON A.session_id = C.session_id AND A.endpoint_id = C.endpoint_id
    LEFT JOIN msdb.dbo.sysjobs AS D ON RIGHT(D.job_id, 10) = RIGHT(SUBSTRING(A.[program_name], 30, 34), 10)
    LEFT JOIN (
        SELECT
            session_id, 
            wait_type,
            wait_duration_ms,
            resource_description,
            ROW_NUMBER() OVER(PARTITION BY session_id ORDER BY (CASE WHEN wait_type LIKE 'PAGE%LATCH%' THEN 0 ELSE 1 END), wait_duration_ms) AS Ranking
        FROM 
            sys.dm_os_waiting_tasks
    ) E ON A.session_id = E.session_id AND E.Ranking = 1
    LEFT JOIN (
        SELECT
            session_id,
            request_id,
            SUM(internal_objects_alloc_page_count + user_objects_alloc_page_count) AS tempdb_allocations,
            SUM(internal_objects_dealloc_page_count + user_objects_dealloc_page_count) AS tempdb_current
        FROM
            sys.dm_db_task_space_usage
        GROUP BY
            session_id,
            request_id
    ) F ON B.session_id = F.session_id AND B.request_id = F.request_id
    LEFT JOIN (
        SELECT 
            blocking_session_id,
            COUNT(*) AS blocked_session_count
        FROM 
            sys.dm_exec_requests
        WHERE 
            blocking_session_id != 0
        GROUP BY
            blocking_session_id
    ) G ON A.session_id = G.blocking_session_id
    OUTER APPLY sys.dm_exec_sql_text(COALESCE(B.[sql_handle], C.most_recent_sql_handle)) AS X
    OUTER APPLY sys.dm_exec_query_plan(B.plan_handle) AS W
    LEFT JOIN sys.dm_resource_governor_workload_groups H ON A.group_id = H.group_id
WHERE
    A.session_id > 50
    AND A.session_id <> @@SPID
    AND (A.[status] != 'sleeping' OR (A.[status] = 'sleeping' AND A.open_transaction_count > 0))

My main questions are:

  • What exactly causes sql_text and sql_command to return NULL for sessions that are in a 'sleeping' state, especially when the open_transaction_count > 0 condition indicates an open transaction?
  • How can I more effectively investigate the origin of these NULL sessions?
  • Is there a way to retrieve the query text or identify the operation that left the transaction open, even if sql_handle or most_recent_sql_handle are no longer in the cache or don't point to anything valid?
  • Does this usually point to an issue with how the application manages its connections and transactions (e.g., missing COMMIT/ROLLBACK in try-catch-finally blocks), or can it be an expected behavior in certain SQL Server scenarios?
1
  • It can be anything, you can join CROSS APPLY sys.dm_exec_input_buffer(A.session_id, NULL) AS ib to see the original query, perhaps it will give you some hint Commented Jun 9 at 13:03

1 Answer 1

0

What exactly causes sql_text and sql_command to return NULL for sessions that are in a 'sleeping' state, especially when the open_transaction_count > 0 condition indicates an open transaction?

Someone opened a transaction and forgot to commit or rollback it. If open_transaction_count is 0 then you should ignore these connections, in most cases it's just connection pooling on the client side.

How can I more effectively investigate the origin of these NULL sessions?

Run an SQL Agent job every couple minutes looking for open transactions that have been open for a long time, and take a note of the session's hostname, last SQL batch, and last input buffer.

Is there a way to retrieve the query text or identify the operation that left the transaction open, even if sql_handle or most_recent_sql_handle are no longer in the cache or don't point to anything valid?

No, although if you have Query Store you may be able to retrieve it from there. If the connection with an open transaction has been open so long that its SQL batch is not in the plan cache then you probably have serious issues. I suggest you look at why your plan cache has such a high turnover.

Does this usually point to an issue with how the application manages its connections and transactions (e.g., missing COMMIT/ROLLBACK in try-catch-finally blocks), or can it be an expected behavior in certain SQL Server scenarios?

Yes. Most likely, your app is not disposing connection and transaction objects correctly. Eg, in C# you need using to correctly dispose everything even in the case of errors.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.