Come trovare la query che contiene ancora un lucchetto?


15

L'interrogazione del sys.dm_tran_locksDMV ci mostra quali sessioni (SPID) tengono blocchi su risorse come tabella, pagina e riga.

Per ciascun blocco acquisito, esiste un modo per determinare quale istruzione SQL (eliminare, inserire, aggiornare o selezionare) ha causato quel blocco?

So che la most_recent_query_handlecolonna del sys.dm_exec_connectionsDMV ci fornisce il testo dell'ultima query eseguita, ma diverse volte altre query sono state eseguite prima nella stessa sessione (SPID) e continuano a contenere blocchi.

Uso già la sp_whoisactiveprocedura (da Adam Machanic) e mostra solo la query che si trova sul buffer di input al momento (pensa DBCC INPUTBUFFER @spid), che non sempre (e nel mio caso di solito mai) è la query che ha acquisito il blocco.

Per esempio:

  1. aprire transazione / sessione
  2. esegui un'istruzione (che contiene un blocco su una risorsa)
  3. esegui un'altra istruzione nella stessa sessione
  4. aprire un'altra transazione / sessione e provare a modificare la risorsa bloccata al passaggio 2.

La sp_whoisactiveprocedura indicherà la dichiarazione al passaggio 3, che non è responsabile del blocco, e quindi non utile.

Questa domanda è venuta dal fare un'analisi usando la funzione Rapporti di processo bloccati , per trovare la causa principale degli scenari di blocco nella produzione. Ogni transazione esegue diverse query e la maggior parte delle volte l'ultima (che viene mostrata sul buffer di input in BPR) è raramente quella che tiene il blocco.

Ho una domanda di follow-up: Framework per identificare efficacemente le query di blocco

Risposte:


15

SQL Server non mantiene una cronologia dei comandi che sono stati eseguiti 1,2 . Puoi determinare quali oggetti hanno i blocchi, ma non puoi necessariamente vedere quale istruzione ha causato quei blocchi.

Ad esempio, se si esegue questa affermazione:

BEGIN TRANSACTION
INSERT INTO dbo.TestLock DEFAULT VALUES

E guarda il testo SQL tramite l'handle sql più recente, vedrai che l'istruzione viene visualizzata. Tuttavia, se la sessione ha fatto questo:

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

Vedresti solo la SELECT * FROM dbo.TestLock;dichiarazione, anche se la transazione non è stata impegnata e la INSERTdichiarazione sta bloccando i lettori sul dbo.TestLocktavolo.

Lo uso per cercare transazioni senza commit che bloccano altre sessioni:

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

Se impostiamo un semplice banco di prova in SSMS con un paio di finestre di query, possiamo vedere che possiamo vedere solo l'ultima istruzione attiva.

Nella prima finestra della query, esegui questo:

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

Nella seconda finestra, esegui questo:

SELECT *
FROM  dbo.TestLock

Ora, se eseguiamo la query sulle transazioni di blocco senza commit dall'alto, vediamo il seguente output:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦══════════════════════════════════ ═══════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬══════════════════════════════════ ═══════╣
║ 67 ║ Transazione ║ 0 ║ INIZIA TRANSAZIONE ║
║ ║ ║ ║ INSERT INTO dbo.TestLock VALORI PREDEFINITI ║
║ 68 ║ Richiesta sessione, Attività in attesa ║ 67 ║ SELEZIONA * ║
║ ║ ║ ║ DA dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩══════════════════════════════════ ═══════╝

(Ho rimosso alcune colonne irrilevanti dalla fine dei risultati).

Ora, se cambiamo la prima finestra della query in questa:

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

e rieseguire la seconda finestra della query:

SELECT *
FROM  dbo.TestLock

Vedremo questo output dalla query sulle transazioni di blocco:

╔═══════════╦═══════════════════════════════╦═════ ═══════════════╦════════════════════╗
║ SessionID ║ ItemType ║ BlockedBySessionID ║ QueryText ║
╠═══════════╬═══════════════════════════════╬═════ ═══════════════╬════════════════════╣
║ 67 ║ Transazione ║ 0 ║ SELEZIONA * ║
║ ║ ║ ║ DA dbo.TestLock; ║
║ 68 ║ Richiesta sessione, Attività in attesa ║ 67 ║ SELEZIONA * ║
║ ║ ║ ║ DA dbo.TestLock ║
╚═══════════╩═══════════════════════════════╩═════ ═══════════════╩════════════════════╝

1 - non del tutto vero. Esiste la cache delle procedure, che può contenere l'istruzione responsabile del blocco. Tuttavia, potrebbe non essere facile determinare quale affermazione sia la vera causa del blocco poiché potrebbero esserci molte query nella cache che toccano la risorsa in questione.

La query seguente mostra il piano di query per le query di test sopra poiché la mia cache delle procedure non è molto occupata.

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;

I risultati di questa query possono consentire di trovare il colpevole, ma attenzione, ispezionare la cache delle procedure in questo modo può essere piuttosto impegnativo per un sistema occupato.

2 di SQL Server 2016 e sopra l'offerta di query deposito , che fa mantenere la storia completa di query eseguite.


Grazie @Max, molto ben spiegato. Questo dubbio è nato durante l'analisi delle Blocked Process Reportsfunzionalità, per trovare la causa principale degli scenari di blocco nella produzione. Ogni transazione esegue diverse query e la maggior parte delle volte l'ultima (che viene mostrata sul buffer di input in BPR) è raramente quella che tiene il blocco. Sembra che la mia ultima risorsa per risolvere questo problema sia impostare una sessione xEvents leggera per comunicarmi quali query sono state eseguite in ciascuna sessione. Se conosci un articolo che mostra un esempio di questo, te ne sarò grato.
tanitelle,

Anche per quanto riguarda Query Store, è molto utile, ma manca delle informazioni SPID. Grazie comunque.
tanitelle,


6

Per completare la risposta di Max , ho trovato di seguito le utilità estremamente utili:

Uso beta_lockinfo quando voglio approfondire il blocco e analizzare cosa e come sono sorti i blocchi, il che è estremamente utile.

beta_lockinfo è una procedura memorizzata che fornisce informazioni sui processi e sui blocchi che detengono nonché sulle loro transazioni attive. beta_lockinfo è progettato per raccogliere quante più informazioni possibili su una situazione di blocco, in modo da poter trovare immediatamente il colpevole e uccidere il processo di blocco se la situazione è disperata. Quindi puoi rilassarti e analizzare l'output di beta_lockinfo per capire come è nata la situazione di blocco e capire quali azioni intraprendere per impedire il ripetersi della situazione. L'output di beta_lockinfo mostra tutti i processi attivi e quelli passivi con blocchi, quali oggetti bloccano, quale comando hanno inviato per ultimo e quale istruzione stanno eseguendo. È inoltre possibile ottenere i piani di query per le dichiarazioni correnti.


1
caspita, quel proc di Erland Sommarskog è fantastico.
Max Vernon,

1
Sì ... lo uso quando devo immergermi in profondità nel bloccare i dettagli.
Kin Shah,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.