Comment trouver la requête qui détient toujours un verrou?

15

L'interrogation du sys.dm_tran_locksDMV nous montre quelles sessions (SPID) détiennent des verrous sur des ressources comme la table, la page et la ligne.

Pour chaque verrou acquis, existe-t-il un moyen de déterminer quelle instruction SQL (supprimer, insérer, mettre à jour ou sélectionner) a provoqué ce verrou?

Je sais que la most_recent_query_handlecolonne du sys.dm_exec_connectionsDMV nous donne le texte de la dernière requête exécutée, mais plusieurs fois d'autres requêtes ont été exécutées auparavant sous la même session (SPID) et détiennent toujours des verrous.

J'utilise déjà la sp_whoisactiveprocédure (d'Adam Machanic) et elle ne montre que la requête qui se trouve sur le tampon d'entrée pour le moment (pensez DBCC INPUTBUFFER @spid), qui n'est pas toujours (et dans mon cas généralement jamais) la requête qui a acquis le verrou.

Par exemple:

  1. transaction / session ouverte
  2. exécute une instruction (qui détient un verrou sur une ressource)
  3. exécute une autre déclaration sur la même session
  4. ouvrez une autre transaction / session et essayez de modifier la ressource verrouillée à l'étape 2.

La sp_whoisactiveprocédure indiquera l'instruction à l'étape 3, qui n'est pas responsable du verrou, et donc inutile.

Cette question est venue d'une analyse à l'aide de la fonction Rapports de processus bloqués , pour trouver la cause première des scénarios de blocage en production. Chaque transaction exécute plusieurs requêtes, et la plupart du temps la dernière (qui est affichée sur le tampon d'entrée de BPR) est rarement celle qui détient le verrou.

J'ai une question complémentaire: Framework pour identifier efficacement les requêtes bloquantes

tanitelle
la source

Réponses:

15

SQL Server ne conserve pas d'historique des commandes qui ont été exécutées 1,2 . Vous pouvez déterminer quels objets ont des verrous, mais vous ne pouvez pas nécessairement voir quelle instruction a provoqué ces verrous.

Par exemple, si vous exécutez cette instruction:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

Et regardez le texte SQL via le plus récent descripteur SQL, vous verrez que cette déclaration apparaît. Cependant, si la session a fait ceci:

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

Vous ne verriez que l' SELECT * FROM dbo.TestLock;instruction, même si la transaction n'a pas été validée, et l' INSERTinstruction bloque les lecteurs sur la dbo.TestLocktable.

J'utilise ceci pour rechercher des transactions non validées qui bloquent d'autres sessions:

/*
    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;

Si nous configurons un banc d'essai simple dans SSMS avec quelques fenêtres de requête, nous pouvons voir que nous ne pouvons voir que la dernière instruction active.

Dans la première fenêtre de requête, exécutez ceci:

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

Dans la deuxième fenêtre, exécutez ceci:

SELECT *
FROM  dbo.TestLock

Maintenant, si nous exécutons la requête de transactions de blocage non validée par le dessus, nous voyons la sortie suivante:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦══════════════════════════════════ ═══════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬══════════════════════════════════ ═══════╣
║ 67 ║ Transaction ║ 0 ║ COMMENCER LA TRANSACTION ║
║ ║ ║ ║ INSÉRER DANS LES VALEURS PAR DÉFAUT dbo.TestLock ║
║ 68 ║ Demande de session, tâche en attente ║ 67 ║ SELECT * ║
║ ║ ║ ║ DE dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩══════════════════════════════════ ═══════╝

(J'ai supprimé certaines colonnes non pertinentes de la fin des résultats).

Maintenant, si nous changeons la première fenêtre de requête en ceci:

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

et réexécutez la deuxième fenêtre de requête:

SELECT *
FROM  dbo.TestLock

Nous verrons cette sortie de la requête de blocage des transactions:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦════════════════════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬════════════════════╣
║ 67 ║ Transaction ║ 0 ║ SELECT * ║
║ ║ ║ ║ DE dbo.TestLock; ║
║ 68 ║ Demande de session, tâche en attente ║ 67 ║ SELECT * ║
║ ║ ║ ║ DE dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩════════════════════╝

1 - pas entièrement vrai. Il y a le cache de procédure, qui peut contenir l'instruction responsable du verrou. Cependant, il peut ne pas être facile de déterminer quelle instruction est la cause réelle du verrouillage car il peut y avoir de nombreuses requêtes dans le cache qui touchent la ressource en question.

La requête ci-dessous montre le plan de requête pour les requêtes de test ci-dessus car mon cache de procédure n'est pas très occupé.

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;

Les résultats de cette requête peuvent vous permettre de trouver le coupable, mais sachez que l'inspection du cache de procédure comme celui-ci peut être assez exigeante sur un système occupé.

2 SQL Server 2016 et offre au- dessus du magasin de requête , qui ne conserve l' historique complet des requêtes exécutées.

Max Vernon
la source
Merci @Max, très bien expliqué. Ce doute est né lors de l'analyse de la Blocked Process Reportsfonctionnalité, pour trouver la cause première des scénarios de blocage en production. Chaque transaction exécute plusieurs requêtes, et la plupart du temps la dernière (qui est affichée sur le tampon d'entrée de BPR) est rarement celle qui détient le verrou. Il semble que ma dernière ressource pour résoudre ce problème soit de définir une session xEvents légère pour me dire quelles requêtes ont été exécutées sous chaque session. Si vous connaissez un article en montrant un exemple, je vous en serai reconnaissant.
tanitelle
En ce qui concerne également Query Store, il est très utile, mais il manque les informations SPID. Merci quand même.
tanitelle
à peu près une copie de dba.stackexchange.com/questions/187794/…
Mitch Wheat
6

Pour compléter la réponse de Max , j'ai trouvé ci-dessous les utilitaires extrêmement utiles:

J'utilise beta_lockinfo lorsque je veux approfondir le blocage et analyser ce qui est survenu et comment le blocage est survenu - ce qui est extrêmement utile.

beta_lockinfo est une procédure stockée qui fournit des informations sur les processus et les verrous qu'ils détiennent ainsi que leurs transactions actives. beta_lockinfo est conçu pour rassembler autant d'informations que possible sur une situation de blocage, afin que vous puissiez instantanément trouver le coupable et tuer le processus de blocage si la situation est désespérée. Ensuite, vous pouvez vous asseoir et analyser la sortie de beta_lockinfo pour comprendre comment la situation de blocage est survenue et déterminer les mesures à prendre pour éviter que la situation ne se reproduise. La sortie de beta_lockinfo montre tous les processus actifs ainsi que les processus passifs avec des verrous, quels objets ils verrouillent, quelle commande ils ont soumise en dernier et quelle instruction ils exécutent. Vous obtenez également les plans de requête pour les instructions en cours.

Kin Shah
la source
1
wow, que Erland Sommarskog proc est incroyable.
Max Vernon
1
Ouais .. je l'utilise quand je dois plonger profondément dans les détails de blocage.
Kin Shah