web-dev-qa-db-fra.com

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

Interroger le sys.dm_tran_locks DMV 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 le most_recent_query_handle colonne du sys.dm_exec_connections DMV 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à le sp_whoisactive procédure (d'Adam Machanic) et il 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.

Le sp_whoisactive la procé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 de suivi: Framework pour identifier efficacement les requêtes bloquantes

15
tanitelle

SQL Server ne conserve pas d'historique des commandes qui ont été exécutées1,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 handle 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 le SELECT * FROM dbo.TestLock;, même si la transaction n'a pas été validée, et l'instruction INSERT bloque les lecteurs par rapport à dbo.TestLock table.

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 VERS VALEURS PAR DÉFAUT dbo.TestLock ║ 
 ║ 68 ║ Demande de session, tâche en attente ║ 67 ║ SELECT * ║ 
 ║ ║ ║ ║ FROM 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 * ║ 
 ║ ║ ║ ║ FROM dbo.TestLock; ║ 
 ║ 68 ║ Demande de session, tâche en attente ║ 67 ║ SELECT * ║ 
 ║ ║ ║ ║ FROM 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édures 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é.

2SQL Server 2016 et versions ultérieures offrent le Query Store , qui conserve l'historique complet des requêtes exécutées.

15
Max Vernon

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

J'utilise beta_lockinfo quand je veux plonger profondément dans le blocage et analyser ce qui 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 s'est produite 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 actuelles.

6
Kin Shah