如何找到仍持有锁的查询?


15

查询sys.dm_tran_locksDMV将向我们显示哪些会话(SPID)持有对表,页面和行等资源的锁定。

对于获得的每个锁,是否有任何方法可以确定哪个SQL语句(删除,插入,更新或选择)导致了该锁?

我知道,most_recent_query_handle在列sys.dm_exec_connectionsDMV给了我们执行的最后一个查询的文本,而是多次其他查询同一个会话(SPID)在跑前和仍持有锁。

我已经使用了该sp_whoisactive过程(来自Adam Machanic),它仅显示了当前在输入缓冲区上的查询(认为DBCC INPUTBUFFER @spid),但并非总是(在我的情况下通常是)获取锁定的查询。

例如:

  1. 公开交易/会话
  2. exec一条语句(持有资源锁)
  3. 在同一会话上执行另一条语句
  4. 打开另一个事务/会话,然后尝试修改在步骤2锁定的资源。

sp_whoisactive过程将在第3步指出该语句,它不是锁的责任,因此无用。

这个问题来自使用“ 阻塞的流程报告”功能进行分析,以查找生产中阻塞方案的根本原因。每个事务都运行几个查询,大多数情况下,最后一个查询(显示在BPR的输入缓冲区上)很少是持有锁的查询。

我有一个后续问题:有效识别阻塞查询的框架

Answers:


15

SQL Server不会保留已执行1,2的命令的历史记录。您可以确定哪些对象具有锁,但是您不一定必须查看导致这些锁的语句。

例如,如果执行以下语句:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

并通过最新的sql句柄查看SQL文本,您会看到该语句确实出现了。但是,如果会话执行此操作:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES
GO
SELECT *
FROM dbo.TestLock;
GO

SELECT * FROM dbo.TestLock;即使事务尚未提交,您也只会看到该语句,并且该INSERT语句正在阻止读者使用该dbo.TestLock表。

我用它来查找阻止其他会话的未提交事务:

/*
    This query shows sessions that are blocking other sessions, including sessions that are 
    not currently processing requests (for instance, they have an open, uncommitted transaction).

    By:  Max Vernon, 2017-03-20
*/
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; --reduce possible blocking by this query.

USE tempdb;

IF OBJECT_ID('tempdb..#dm_tran_session_transactions') IS NOT NULL
DROP TABLE #dm_tran_session_transactions;
SELECT *
INTO #dm_tran_session_transactions
FROM sys.dm_tran_session_transactions;

IF OBJECT_ID('tempdb..#dm_exec_connections') IS NOT NULL
DROP TABLE #dm_exec_connections;
SELECT *
INTO #dm_exec_connections
FROM sys.dm_exec_connections;

IF OBJECT_ID('tempdb..#dm_os_waiting_tasks') IS NOT NULL
DROP TABLE #dm_os_waiting_tasks;
SELECT *
INTO #dm_os_waiting_tasks
FROM sys.dm_os_waiting_tasks;

IF OBJECT_ID('tempdb..#dm_exec_sessions') IS NOT NULL
DROP TABLE #dm_exec_sessions;
SELECT *
INTO #dm_exec_sessions
FROM sys.dm_exec_sessions;

IF OBJECT_ID('tempdb..#dm_exec_requests') IS NOT NULL
DROP TABLE #dm_exec_requests;
SELECT *
INTO #dm_exec_requests
FROM sys.dm_exec_requests;

;WITH IsolationLevels AS 
(
    SELECT v.*
    FROM (VALUES 
              (0, 'Unspecified')
            , (1, 'Read Uncomitted')
            , (2, 'Read Committed')
            , (3, 'Repeatable')
            , (4, 'Serializable')
            , (5, 'Snapshot')
        ) v(Level, Description)
)
, trans AS 
(
    SELECT dtst.session_id
        , blocking_sesion_id = 0
        , Type = 'Transaction'
        , QueryText = dest.text
    FROM #dm_tran_session_transactions dtst 
        LEFT JOIN #dm_exec_connections dec ON dtst.session_id = dec.session_id
    OUTER APPLY sys.dm_exec_sql_text(dec.most_recent_sql_handle) dest
)
, tasks AS 
(
    SELECT dowt.session_id
        , dowt.blocking_session_id
        , Type = 'Waiting Task'
        , QueryText = dest.text
    FROM #dm_os_waiting_tasks dowt
        LEFT JOIN #dm_exec_connections dec ON dowt.session_id = dec.session_id
    OUTER APPLY sys.dm_exec_sql_text(dec.most_recent_sql_handle) dest
    WHERE dowt.blocking_session_id IS NOT NULL
)
, requests AS 
(
SELECT des.session_id
    , der.blocking_session_id
    , Type = 'Session Request'
    , QueryText = dest.text
FROM #dm_exec_sessions des
    INNER JOIN #dm_exec_requests der ON des.session_id = der.session_id
OUTER APPLY sys.dm_exec_sql_text(der.sql_handle) dest
WHERE der.blocking_session_id IS NOT NULL
    AND der.blocking_session_id > 0 
)
, Agg AS (
    SELECT SessionID = tr.session_id
        , ItemType = tr.Type
        , CountOfBlockedSessions = (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = tr.session_id)
        , BlockedBySessionID = tr.blocking_sesion_id
        , QueryText = tr.QueryText
    FROM trans tr
    WHERE EXISTS (
        SELECT 1
        FROM requests r
        WHERE r.blocking_session_id = tr.session_id
        )
    UNION ALL
    SELECT ta.session_id
        , ta.Type
        , CountOfBlockedSessions = (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = ta.session_id)
        , BlockedBySessionID = ta.blocking_session_id
        , ta.QueryText
    FROM tasks ta
    UNION ALL
    SELECT rq.session_id
        , rq.Type
        , CountOfBlockedSessions =  (SELECT COUNT(*) FROM requests r WHERE r.blocking_session_id = rq.session_id)
        , BlockedBySessionID = rq.blocking_session_id
        , rq.QueryText
    FROM requests rq
)
SELECT agg.SessionID
    , ItemType = STUFF((SELECT ', ' + COALESCE(a.ItemType, '') FROM agg a WHERE a.SessionID = agg.SessionID ORDER BY a.ItemType FOR XML PATH ('')), 1, 2, '')
    , agg.BlockedBySessionID
    , agg.QueryText
    , agg.CountOfBlockedSessions
    , des.host_name
    , des.login_name
    , des.is_user_process
    , des.program_name
    , des.status
    , TransactionIsolationLevel = il.Description
FROM agg 
    LEFT JOIN #dm_exec_sessions des ON agg.SessionID = des.session_id
    LEFT JOIN IsolationLevels il ON des.transaction_isolation_level = il.Level
GROUP BY agg.SessionID
    , agg.BlockedBySessionID
    , agg.CountOfBlockedSessions
    , agg.QueryText
    , des.host_name
    , des.login_name
    , des.is_user_process
    , des.program_name
    , des.status
    , il.Description
ORDER BY 
    agg.BlockedBySessionID
    , agg.CountOfBlockedSessions
    , agg.SessionID;

如果我们在带有两个查询窗口的SSMS中设置一个简单的测试平台,我们可以看到我们只能看到最近活动的语句。

在第一个查询窗口中,运行以下命令:

CREATE TABLE dbo.TestLock
(
    id int NOT NULL IDENTITY(1,1)
);
BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

在第二个窗口中,运行以下命令:

SELECT *
FROM  dbo.TestLock

现在,如果我们从上面运行未提交的阻塞事务查询,我们将看到以下输出:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦══════════════════════════════════ ═══════╗
║会话ID║项目类型║BlockedBySessionID║QueryText║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬══════════════════════════════════ ═══════╣
║67║交易║0║开始交易║
插入到dbo.TestLock默认值
║68║会话请求,等待任务║67║选择*║
来自dbo.TestLock
╚═══════════牛皮═══════════════════════════════牛皮═════ ═══════════════牛皮══════════════════════════════════ ═══════╝

(我从结果末尾删除了一些不相关的列)。

现在,如果我们将第一个查询窗口更改为此:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES
GO
SELECT *
FROM dbo.TestLock;
GO

并重新运行第二个查询窗口:

SELECT *
FROM  dbo.TestLock

我们将从阻塞交易查询中看到以下输出:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦════════════════════╗
║会话ID║项目类型║BlockedBySessionID║QueryText║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬════════════════════╣
║67║交易║0║选择*║
从dbo.TestLock; ║
║68║会话请求,等待任务║67║选择*║
来自dbo.TestLock
╚═══════════牛皮═══════════════════════════════牛皮═════ ═══════════════牛皮════════════════════╝

1-并非完全正确。有过程高速缓存,其中可能包含负责锁定的语句。但是,要确定哪个语句是导致锁定的真正原因可能并不容易,因为高速缓存中可能有许多查询触及所讨论的资源。

由于我的过程缓存不是很忙,下面的查询显示了上面测试查询的查询计划。

SELECT TOP(30) t.text
    , p.query_plan
    , deqs.execution_count
    , deqs.total_elapsed_time
    , deqs.total_logical_reads
    , deqs.total_logical_writes
    , deqs.total_logical_writes
    , deqs.total_rows
    , deqs.total_worker_time
    , deqs.*
FROM sys.dm_exec_query_stats deqs
OUTER APPLY sys.dm_exec_sql_text(deqs.sql_handle) t 
OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle) p
WHERE t.text LIKE '%dbo.TestLock%'  --change this to suit your needs
    AND t.text NOT LIKE '/\/\/\/\/EXCLUDE ME/\/\/\/\/\'
ORDER BY 
    deqs.total_worker_time DESC;

该查询的结果可能使您能够找到罪魁祸首,但是请注意,在繁忙的系统上,像这样检查过程高速缓存可能会非常困难。

2 SQL Server 2016及更高版本提供了Query Store,它确实保留了已执行查询的完整历史记录。


谢谢@Max,解释得很好。这个疑问是在进行Blocked Process Reports特征分析时发现的,以找出生产中阻塞方案的根本原因。每个事务都运行几个查询,大多数情况下,最后一个查询(显示在BPR的输入缓冲区上)很少是持有锁的查询。看来,解决此问题的最后资源是设置一个轻量级的xEvents会话,以告诉我每个会话下进行了哪些查询。如果您知道一篇显示此示例的文章,我将不胜感激。
tanitelle

同样对于查询存储,它非常有用,但是缺少SPID信息。不管怎么说,还是要谢谢你。
tanitelle


6

为了补充Max的答案,我发现以下实用程序非常有用:

当我想深入研究阻塞并分析产生阻塞的原因和方式时,我使用beta_lockinfo-这非常有用。

beta_lockinfo是一个存储过程,提供有关进程及其持有的锁以及活动事务的信息。beta_lockinfo旨在收集有关阻塞情况的尽可能多的信息,以便您可以立即找到罪魁祸首,并在情况紧急时杀死阻塞进程。然后,您可以坐下来分析beta_lockinfo的输出,以了解阻塞情况是如何产生的,并确定应采取哪些措施来防止再次发生该情况。beta_lockinfo的输出显示所有活动进程以及带锁的被动进程,它们锁定的对象,它们最后提交的命令以及正在执行的语句。您还将获得当前语句的查询计划。


1
哇,Erland Sommarskog的动作太棒了。
Max Vernon

1
是的..当我不得不深入研究阻止细节时,我会使用它。
金莎(Kin Shah)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.