Back to all posts

SQL Server Deadlock & Performance Monitoring Made Easy

Learn how to detect SQL Server deadlocks and monitor performance with ready-to-use stored procedures for faster troubleshooting and optimization.

SQL Server pe kaam karte time ek badi common problem hoti hai Deadlocks aur doosri hoti hai Slow Performance.
Kabhi socha hai ki ek query kyun latakti rehti hai? Ya ekdum se system slow kyon ho jata hai?
Ye dono cheezein developer ya DBA ke liye headache ban jaati hain.

  1. dbusp_DeadLockTree → Deadlock aur blocking ka clear tree structure dikhayega.
  2. dbusp_PerformanceStats → Aapke system ke heavy queries aur badi tables ka full performance report nikaal dega.

🧩 Part 1: Deadlock Tree (dbusp_DeadLockTree)

Socho ek traffic signal pe 4 gadiyaan ek dusre ka rasta block kar rahi hain. Koi bhi pehle nahi hilega, sab wait kar rahe hain — yehi hota hai Deadlock.

Is SP ka kaam hai deadlock ka “map” banana jaha clearly dikh jaye kaun kisko block kar raha hai.

Query (Create Procedure)

SQL
IF OBJECT_ID('dbusp_DeadLockTree','P') IS NOT NULL
BEGIN
 drop proc dbusp_DeadLockTree
END 
GO

CREATE PROCEDURE dbusp_DeadLockTree
AS
BEGIN
    SET NOCOUNT ON;

    -- Step 1: Process details with waits
    SELECT 
        R.spid AS SPID,
        R.blocked AS BLOCKED,
        R.waittime / 1000.0 AS WaitTimeSec,
        R.lastwaittype AS WaitType,
        R.loginame AS LoginName,
        R.hostname AS HostName,
        DB_NAME(R.dbid) AS DBName,
        REPLACE(REPLACE(T.TEXT, CHAR(10), ' '), CHAR(13), ' ') AS BatchText
    INTO #tmpProcess
    FROM sys.sysprocesses R
    CROSS APPLY sys.dm_exec_sql_text(R.sql_handle) T;

    -- Step 2: Recursive blocking chain
    WITH BlockerTree (SPID, BLOCKED, LEVEL, WaitTimeSec, WaitType, LoginName, HostName, DBName, BatchText)
    AS
    (
        SELECT SPID, BLOCKED,
               CAST(RIGHT('0000' + CAST(SPID AS VARCHAR), 4) AS VARCHAR(1000)) AS LEVEL,
               WaitTimeSec, WaitType, LoginName, HostName, DBName, BatchText
        FROM #tmpProcess R
        WHERE (BLOCKED = 0 OR BLOCKED = SPID)
          AND EXISTS (SELECT 1 FROM #tmpProcess R2 WHERE R2.BLOCKED = R.SPID AND R2.BLOCKED <> R2.SPID)

        UNION ALL

        SELECT R.SPID, R.BLOCKED,
               CAST(BlockerTree.LEVEL + RIGHT('0000' + CAST(R.SPID AS VARCHAR), 4) AS VARCHAR(1000)),
               R.WaitTimeSec, R.WaitType, R.LoginName, R.HostName, R.DBName, R.BatchText
        FROM #tmpProcess R
        INNER JOIN BlockerTree ON R.BLOCKED = BlockerTree.SPID
        WHERE R.BLOCKED > 0 AND R.BLOCKED <> R.SPID
    )

    -- Step 3: Final output
    SELECT 
       REPLICATE('   ', LEN(LEVEL)/4 - 1) + 
        CASE WHEN LEN(LEVEL)/4 - 1 = 0 THEN 'HEAD ---> ' ELSE '├── ' END +
        CAST(SPID AS NVARCHAR(10)) + 
        ' (Blocked by: ' + CAST(BLOCKED AS NVARCHAR(10)) + ')' AS BlockingTree,
        BLOCKED, WaitTimeSec, WaitType, LoginName, HostName, DBName, BatchText
    FROM BlockerTree
    ORDER BY LEVEL;

    DROP TABLE #tmpProcess;
END;
GO

👉 Run karne ke liye:

SQL
EXEC dbusp_DeadLockTree;

🧩 Part 2: Performance Stats (dbusp_PerformanceStats)

Ab maan lo aapki application slow chal rahi hai aur client bol raha hai "Query bahut slow hai".
Par kaunsi query? Kya problem hai? Table badi ho gayi hai ya koi CPU heavy query chal rahi hai?

Yaha ye SP aapka Doctor ban jaata hai.

Query (Create Procedure)

SQL
-- Drop if exists
IF OBJECT_ID('dbo.dbusp_PerformanceStats', 'P') IS NOT NULL
    DROP PROCEDURE dbo.dbusp_PerformanceStats;
GO

CREATE PROCEDURE dbo.dbusp_PerformanceStats
    @ThresholdAvgSec   FLOAT  = 2,
    @ThresholdCPUSec   FLOAT  = 1,
    @ThresholdReads    BIGINT = 10000,
    @TableSizeInMB     INT    = 1024,
    @RowCount          BIGINT = 10000000,
    @TopRows           INT    = 10
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY  
        -- 1. Large Tables
        SELECT  
            'Table Size' AS ,
            t.NAME AS ,
            SUM(p.rows) AS ,
            CAST(((SUM(a.total_pages) * 8.0) / 1024 / 1024) AS DECIMAL(10,2)) AS 
        FROM sys.tables t
        INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id
        INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
        INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
        GROUP BY t.Name
        HAVING ((SUM(a.total_pages) * 8.0) / 1024) >= @TableSizeInMB
            OR SUM(p.rows) >= @RowCount;

        -- 2. Top CPU Heavy Queries
        SELECT TOP (@TopRows)
            QS.execution_count,
            QS.total_worker_time / 1000000.0 AS TotalCPU_Sec,
            (QS.total_worker_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS AvgCPU_Sec,
            QS.total_elapsed_time / 1000000.0 AS TotalElapsed_Sec,
            T.text AS QueryText,
            DB_NAME(T.dbid) AS DBName
        FROM sys.dm_exec_query_stats QS
        CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
        ORDER BY TotalCPU_Sec DESC;

        -- 3. Top Duration Queries
        SELECT TOP (@TopRows)
            QS.execution_count,
            (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS AvgElapsed_Sec,
            QS.total_worker_time / 1000000.0 AS TotalCPU_Sec,
            T.text AS QueryText,
            DB_NAME(T.dbid) AS DBName
        FROM sys.dm_exec_query_stats QS
        CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
        ORDER BY AvgElapsed_Sec DESC;

        -- 4. Top IO Heavy Queries
        SELECT TOP (@TopRows)
            QS.execution_count,
            QS.total_logical_reads + QS.total_physical_reads AS TotalReads,
            (QS.total_logical_reads + QS.total_physical_reads) / NULLIF(QS.execution_count,0) AS AvgReads,
            T.text AS QueryText,
            DB_NAME(T.dbid) AS DBName
        FROM sys.dm_exec_query_stats QS
        CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
        ORDER BY TotalReads DESC;

        -- 5. Currently Running Queries + Blocking
        SELECT 
            R.session_id, R.status, R.command,
            DB_NAME(R.database_id) AS DBName,
            T.text AS QueryText,
            R.blocking_session_id
        FROM sys.dm_exec_requests R
        CROSS APPLY sys.dm_exec_sql_text(R.sql_handle) T
        WHERE R.session_id > 50 AND R.session_id <> @@SPID
        ORDER BY R.total_elapsed_time DESC;

        -- 6. Execution Plan for Top Queries
        SELECT TOP (@TopRows)
            (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS AvgElapsed_Sec,
            T.text AS QueryText,
            P.query_plan AS ExecutionPlanXml
        FROM sys.dm_exec_query_stats QS
        CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
        CROSS APPLY sys.dm_exec_query_plan(QS.plan_handle) P
        ORDER BY AvgElapsed_Sec DESC;

        -- 7. Stored Procedure Performance
        SELECT  
            DB_NAME(PS.database_id) AS DBName,
            OBJECT_NAME(PS.object_id, PS.database_id) AS ProcedureName,
            PS.execution_count,
            (PS.total_elapsed_time / NULLIF(PS.execution_count,0)) / 1000000.0 AS AvgElapsedSec,
            (PS.total_worker_time / NULLIF(PS.execution_count,0)) / 1000000.0 AS AvgCPUSec,
            (PS.total_logical_reads / NULLIF(PS.execution_count,0)) AS AvgLogicalReads
        FROM sys.dm_exec_procedure_stats PS
        WHERE PS.database_id = DB_ID()
        ORDER BY AvgElapsedSec DESC;
    END TRY  
    BEGIN CATCH  
        SELECT ERROR_NUMBER() AS ErrorNum, ERROR_MESSAGE() AS ErrorMessage;
    END CATCH  
END
GO

👉 Run karne ke liye:

SQL
EXEC dbo.dbusp_PerformanceStats;

🎯 Conclusion

SQL Server me issues aana normal hai — jaise traffic road pe hota hai.
Lekin agar aapke paas dbusp_DeadLockTree aur dbusp_PerformanceStats jaise tools hain, to aap easily pata kar sakte ho problem kahan hai aur usko fix kar sakte ho.

Keep building your data skillset

Explore more SQL, Python, analytics, and engineering tutorials.