Back to all posts
Database maintenance

SQL Server Deadlock & Performance Monitoring Made Easy

Introduction In SQL Server, performance issues are very common. Sometimes queries run slow, sometimes users get stuck, and sometimes the system becomes unres...

Introduction

In SQL Server, performance issues are very common. Sometimes queries run slow, sometimes users get stuck, and sometimes the system becomes unresponsive.

Two major reasons:

  • Blocking (queries waiting on each other)

  • Poor performance (CPU, IO, memory issues)

In this blog, we will learn how to solve both problems using two powerful stored procedures:

👉 dbusp_BlockingTree → For blocking analysis
👉 dbusp_PerformanceStats → Full performance dashboard


Part 1: Understanding Blocking

Blocking happens when:

  • One query locks data

  • Another query tries to access same data

  • Second query waits

Result: system slowdown


Solution 1: Blocking Tree Script

We use:

👉 dbusp_BlockingTree

Full script:

SQL
IF OBJECT_ID('dbusp_BlockingTree', 'P') IS NOT NULL
    DROP PROCEDURE dbusp_BlockingTree;
GO

/*
================================================================================
SP Name  : dbusp_BlockingTree
Purpose  : Shows full blocking chain tree using modern DMVs.
           NOTE: This is a BLOCKING TREE viewer, not deadlock capture.
           For actual deadlocks → use Extended Events (system_health session).
Author   : DBA Team
Version  : 2.0 (DBA Grade)

Parameters:
  @MinWaitSec      - Show only sessions waiting >= N seconds (default 0)
  @DBName          - Filter by database name (NULL = all databases)
  @ShowAllSessions - 0 = only blocking chain | 1 = all active sessions
  @KillLeadBlocker - 1 = KILL the head blocker SPID
  @DBAConfirm      - Safety switch: must be 1 to allow KILL

Result Sets:
  #1 → Blocking Tree (visual chain with depth indentation)
  #2 → Lock Resource Details (which object is being locked)
  #3 → Lead Blocker Summary (quick action card for DBA)
================================================================================
*/

CREATE PROCEDURE dbusp_BlockingTree
    @MinWaitSec       INT     = 0,      -- Min wait threshold in seconds
    @DBName           SYSNAME = NULL,   -- NULL = all DBs
    @ShowAllSessions  BIT     = 0,      -- 1 = include non-blocked active sessions too
    @KillLeadBlocker  BIT     = 0,      -- 1 = attempt KILL on lead blocker
    @DBAConfirm       BIT     = 0       -- Safety: must be 1 to allow @KillLeadBlocker
AS
BEGIN
    SET NOCOUNT ON;

    -- =========================================================================
    -- SECTION 0: Safety Guard for KILL
    -- =========================================================================
    IF @KillLeadBlocker = 1 AND @DBAConfirm = 0
    BEGIN
        RAISERROR(
            'KILL blocked! Set @DBAConfirm = 1 along with @KillLeadBlocker = 1 to confirm.',
            16, 1
        );
        RETURN;
    END;

    -- =========================================================================
    -- SECTION 1: Collect session + request data from modern DMVs
    -- sys.dm_exec_sessions  → login, host, DB, isolation level, open txns
    -- sys.dm_exec_requests  → wait info, CPU, reads, blocking_session_id
    -- sys.dm_exec_sql_text  → batch SQL text (safe NULL handle check)
    -- =========================================================================
    IF OBJECT_ID('tempdb..#tmpSessions') IS NOT NULL
    DROP TABLE #tmpSessions;

    SELECT
        s.session_id                                AS SPID,
        ISNULL(r.blocking_session_id, 0)            AS BlockedBy,

        -- Wait info
        ISNULL(r.wait_time, 0) / 1000.0             AS WaitTimeSec,
        ISNULL(r.wait_type, N'—')                   AS WaitType,
        ISNULL(r.wait_resource, N'—')               AS WaitResource,

        -- Session info
        s.login_name                                AS LoginName,
        s.host_name                                 AS HostName,
        s.program_name                              AS AppName,
        ISNULL(DB_NAME(r.database_id),
               DB_NAME(s.database_id))              AS DBName,

        -- Perf counters
        ISNULL(r.cpu_time, 0)                       AS CPUTimeMs,
        ISNULL(r.logical_reads, 0)                  AS LogicalReads,
        ISNULL(r.writes, 0)                         AS Writes,
        ISNULL(r.row_count, 0)                      AS RowsReturned,

        -- Transaction info
        s.open_transaction_count                    AS OpenTxnCount,
        CASE s.transaction_isolation_level
            WHEN 0 THEN 'Unspecified'
            WHEN 1 THEN 'READ UNCOMMITTED'
            WHEN 2 THEN 'READ COMMITTED'
            WHEN 3 THEN 'REPEATABLE READ'
            WHEN 4 THEN 'SERIALIZABLE'
            WHEN 5 THEN 'SNAPSHOT'
            ELSE 'Unknown'
        END                                         AS IsolationLevel,

        -- Status
        s.status                                    AS SessionStatus,
        ISNULL(r.status, N'—')                      AS RequestStatus,
        r.command                                   AS CommandType,

        -- SQL Text: full batch
        CASE
            WHEN r.sql_handle IS NOT NULL
              AND r.sql_handle <> 0x0000000000000000000000000000000000000000
            THEN REPLACE(REPLACE(
                    ISNULL(bt.TEXT, N'[Could not retrieve SQL]'),
                    CHAR(10), ' '), CHAR(13), ' ')
            ELSE N'[No active request / idle in txn]'
        END                                         AS BatchText,

        -- Current statement (slice from batch using offsets)
        CASE
            WHEN r.sql_handle IS NOT NULL
              AND r.sql_handle <> 0x0000000000000000000000000000000000000000
              AND bt.TEXT IS NOT NULL
            THEN REPLACE(REPLACE(
                    SUBSTRING(
                        bt.TEXT,
                        (r.statement_start_offset / 2) + 1,
                        CASE r.statement_end_offset
                            WHEN -1 THEN DATALENGTH(bt.TEXT)
                            ELSE (r.statement_end_offset - r.statement_start_offset) / 2 + 1
                        END
                    ), CHAR(10), ' '), CHAR(13), ' ')
            ELSE N'—'
        END                                         AS CurrentStatement,

        -- Start time of current request
        r.start_time                                AS RequestStartTime,

        -- Used in CTE tree building
        CAST(RIGHT('0000' + CAST(s.session_id AS VARCHAR(10)), 4)
             AS VARCHAR(4000))                      AS SortPath

    INTO #tmpSessions
    FROM sys.dm_exec_sessions s
    LEFT JOIN sys.dm_exec_requests r
           ON s.session_id = r.session_id
    OUTER APPLY (
        SELECT TEXT
        FROM sys.dm_exec_sql_text(r.sql_handle)
        WHERE r.sql_handle IS NOT NULL
          AND r.sql_handle <> 0x0000000000000000000000000000000000000000
    ) bt
    WHERE s.is_user_process = 1                     -- Exclude system SPIDs
      AND s.session_id <> @@SPID                    -- Exclude this SP itself
      AND (
            -- Only blocking chain members
            (@ShowAllSessions = 0 AND (
                ISNULL(r.blocking_session_id, 0) > 0
                OR EXISTS (
                    SELECT 1 FROM sys.dm_exec_requests r2
                    WHERE r2.blocking_session_id = s.session_id
                )
            ))
            OR
            -- All active sessions
            (@ShowAllSessions = 1 AND (
                s.status IN ('running', 'sleeping', 'suspended')
                OR r.session_id IS NOT NULL
            ))
          )
      AND ISNULL(r.wait_time, 0) / 1000.0 >= @MinWaitSec
      AND (@DBName IS NULL
           OR DB_NAME(r.database_id) = @DBName
           OR DB_NAME(s.database_id) = @DBName);

    -- =========================================================================
    -- SECTION 2: Recursive CTE — Build Blocking Tree
    -- =========================================================================
    WITH BlockerTree AS
    (
        -- Anchor: Root blockers (sessions that are blocking others but not blocked themselves)
        SELECT
            SPID,
            BlockedBy,
            WaitTimeSec,
            WaitType,
            WaitResource,
            LoginName,
            HostName,
            AppName,
            DBName,
            CPUTimeMs,
            LogicalReads,
            OpenTxnCount,
            IsolationLevel,
            SessionStatus,
            RequestStatus,
            CommandType,
            BatchText,
            CurrentStatement,
            RequestStartTime,
            CAST(RIGHT('0000' + CAST(SPID AS VARCHAR(10)), 4)
                 AS VARCHAR(4000))                  AS LEVEL
        FROM #tmpSessions
        WHERE (BlockedBy = 0 OR BlockedBy = SPID)  -- Self-block edge case guard
          AND EXISTS (
                SELECT 1 FROM #tmpSessions c
                WHERE c.BlockedBy = SPID
                  AND c.BlockedBy <> c.SPID
              )

        UNION ALL

        -- Recursive: blocked children
        SELECT
            c.SPID,
            c.BlockedBy,
            c.WaitTimeSec,
            c.WaitType,
            c.WaitResource,
            c.LoginName,
            c.HostName,
            c.AppName,
            c.DBName,
            c.CPUTimeMs,
            c.LogicalReads,
            c.OpenTxnCount,
            c.IsolationLevel,
            c.SessionStatus,
            c.RequestStatus,
            c.CommandType,
            c.BatchText,
            c.CurrentStatement,
            c.RequestStartTime,
            CAST(p.LEVEL + RIGHT('0000' + CAST(c.SPID AS VARCHAR(10)), 4)
                 AS VARCHAR(4000))
        FROM #tmpSessions c
        INNER JOIN BlockerTree p ON c.BlockedBy = p.SPID
        WHERE c.BlockedBy > 0
          AND c.BlockedBy <> c.SPID
    )
    -- =========================================================================
    -- RESULT SET 1: Visual Blocking Tree
    -- =========================================================================
    SELECT
        -- Visual tree node
        REPLICATE('   ', (LEN(LEVEL) / 4) - 1)
        + CASE
            WHEN (LEN(LEVEL) / 4) - 1 = 0
            THEN N'👑 HEAD BLOCKER ──▶ '
            ELSE N'    └── '
          END
        + CAST(SPID AS NVARCHAR(10))
        + N'  (Blocked by: '
        + CASE WHEN BlockedBy = 0 THEN 'None' ELSE CAST(BlockedBy AS NVARCHAR(10)) END
        + N')'                                      AS BlockingTree,

        SPID,
        BlockedBy,
        WaitTimeSec,
        WaitType,
        WaitResource,
        LoginName,
        HostName,
        AppName,
        DBName,
        SessionStatus,
        RequestStatus,
        CommandType,
        IsolationLevel,
        OpenTxnCount,
        CPUTimeMs,
        LogicalReads,
        RequestStartTime,
        CurrentStatement,
        BatchText
    FROM BlockerTree
    ORDER BY LEVEL;

    -- =========================================================================
    -- RESULT SET 2: Lock Resource Details
    -- Which object/index is being contested
    -- =========================================================================
    SELECT
        tl.request_session_id                       AS SPID,
        tl.resource_type                            AS LockResourceType,    -- TABLE / PAGE / KEY / ROW
        tl.resource_subtype                         AS LockSubType,
        tl.resource_database_id                     AS LockDBID,
        DB_NAME(tl.resource_database_id)            AS LockDBName,
        ISNULL(OBJECT_NAME(
            tl.resource_associated_entity_id,
            tl.resource_database_id), '—')          AS LockedObject,
        tl.resource_description                     AS LockDetail,
        tl.request_mode                             AS LockMode,            -- S, X, IX, SIX etc.
        tl.request_status                           AS LockStatus,          -- GRANT / WAIT / CONVERT
        tl.request_type                             AS LockType
    FROM sys.dm_tran_locks tl
    INNER JOIN #tmpSessions ts ON tl.request_session_id = ts.SPID
    ORDER BY tl.request_session_id, tl.resource_type;

    -- =========================================================================
    -- RESULT SET 3: Lead Blocker Summary (DBA Action Card)
    -- =========================================================================
    SELECT TOP 10
        ts.SPID                                     AS LeadBlockerSPID,
        ts.LoginName,
        ts.HostName,
        ts.AppName,
        ts.DBName,
        ts.IsolationLevel,
        ts.OpenTxnCount,
        ts.CPUTimeMs,
        ts.LogicalReads,
        ts.RequestStartTime,
        DATEDIFF(SECOND, ts.RequestStartTime, GETDATE())
                                                    AS HoldingTxnSec,
        COUNT(victims.SPID)                         AS BlockedSessionCount,
        N'KILL ' + CAST(ts.SPID AS NVARCHAR(10)) + N';'
                                                    AS KillCommand,
        ts.CurrentStatement                         AS BlockerCurrentStatement
    FROM #tmpSessions ts
    LEFT JOIN #tmpSessions victims ON victims.BlockedBy = ts.SPID
    WHERE (ts.BlockedBy = 0 OR ts.BlockedBy = ts.SPID)
      AND EXISTS (
            SELECT 1 FROM #tmpSessions c2
            WHERE c2.BlockedBy = ts.SPID AND c2.SPID <> ts.SPID
          )
    GROUP BY
        ts.SPID, ts.LoginName, ts.HostName, ts.AppName,
        ts.DBName, ts.IsolationLevel, ts.OpenTxnCount,
        ts.CPUTimeMs, ts.LogicalReads, ts.RequestStartTime,
        ts.CurrentStatement
    ORDER BY BlockedSessionCount DESC, HoldingTxnSec DESC;

    -- =========================================================================
    -- SECTION 3: KILL Lead Blocker (with double safety check)
    -- =========================================================================
    IF @KillLeadBlocker = 1 AND @DBAConfirm = 1
    BEGIN
        DECLARE @LeadSPID   INT;
        DECLARE @KillSQL    NVARCHAR(50);
        DECLARE @KillMsg    NVARCHAR(200);

        -- Pick the worst offender: most victims, longest running
        SELECT TOP 1
            @LeadSPID = ts.SPID
        FROM #tmpSessions ts
        WHERE (ts.BlockedBy = 0 OR ts.BlockedBy = ts.SPID)
          AND EXISTS (
                SELECT 1 FROM #tmpSessions c2
                WHERE c2.BlockedBy = ts.SPID AND c2.SPID <> ts.SPID
              )
        ORDER BY
            (SELECT COUNT(*) FROM #tmpSessions v WHERE v.BlockedBy = ts.SPID) DESC,
            DATEDIFF(SECOND, ts.RequestStartTime, GETDATE()) DESC;

        IF @LeadSPID IS NOT NULL
        BEGIN
            BEGIN TRY
                SET @KillSQL = N'KILL ' + CAST(@LeadSPID AS NVARCHAR(10)) + N';';
                EXEC sp_executesql @KillSQL;

                SET @KillMsg = N'✅ KILLED SPID ' + CAST(@LeadSPID AS NVARCHAR(10))
                             + N' at ' + CONVERT(NVARCHAR(30), GETDATE(), 120);
                RAISERROR(@KillMsg, 10, 1) WITH NOWAIT;
            END TRY
            BEGIN CATCH
                SET @KillMsg = N'❌ KILL failed for SPID '
                             + CAST(@LeadSPID AS NVARCHAR(10))
                             + N' → ' + ERROR_MESSAGE();
                RAISERROR(@KillMsg, 16, 1);
            END CATCH;
        END
        ELSE
        BEGIN
            RAISERROR('No lead blocker found to KILL.', 10, 1) WITH NOWAIT;
        END;
    END;

    -- Cleanup
    IF OBJECT_ID('tempdb..#tmpSessions') IS NOT NULL
    DROP TABLE #tmpSessions;

END;
GO

-- 1. Default: Show blocking chain only
EXEC dbusp_BlockingTree;

-- 2. Only sessions waiting 5+ seconds
EXEC dbusp_BlockingTree @MinWaitSec = 5;

-- 3. Specific database only
EXEC dbusp_BlockingTree @DBName = N'Vipul';

-- 4. All active sessions (not just blocked chain)
EXEC dbusp_BlockingTree @ShowAllSessions = 1;

-- 5. Kill lead blocker (double confirm required)
EXEC dbusp_BlockingTree
    @KillLeadBlocker = 1,
    @DBAConfirm      = 1;

-- 6. Full DBA investigation mode
EXEC dbusp_BlockingTree
    @MinWaitSec      = 3,
    @DBName          = N'Vipul',
    @ShowAllSessions = 0;

What This Script Does

It gives 3 outputs:

  1. Blocking Tree

    • Shows full chain

    • Who is blocking whom

  2. Lock Details

    • Table / row / index lock info

  3. Lead Blocker

    • Main problem query

    • Ready KILL command


How to Use

SQL
EXEC dbusp_BlockingTree;

Filter options:

SQL
EXEC dbusp_BlockingTree @MinWaitSec = 5;
EXEC dbusp_BlockingTree @DBName = 'PayrollDB';

Kill blocker (careful ⚠️):

SQL
EXEC dbusp_BlockingTree
    @KillLeadBlocker = 1,
    @DBAConfirm = 1;

When to Use This Script

  • Users stuck

  • Queries not finishing

  • LCK_M_* wait types

  • High wait time


Part 2: Full SQL Server Performance Monitoring

Blocking is only one part. A real DBA needs to monitor the complete system health.

This is used here:

dbusp_PerformanceStats

Full script:

SQL
-- ============================================================
-- dbusp_PerformanceStats  |  ENTERPRISE DBA-Grade Dashboard v3.1
-- Categories:
--   QUERY PERFORMANCE: 2, 3, 4, 5, 6, 7, 15, 26
--   INDEX HEALTH:      8, 9, 10, 16, 25
--   DATABASE HEALTH:   1, 11, 12, 13, 14, 17, 18, 19, 20, 27, 28
--   MEMORY/RESOURCES:  13, 21, 22, 23, 24
--   INFRASTRUCTURE:    11, 29, 30
--   CONCURRENCY:       5, 6, 14, 15, 24
-- SECTIONS:
--   1  Large Tables                            (Size Analysis)
--   2  Top CPU Heavy Queries                   (Query Perf)
--   3  Top Duration Queries                    (Query Perf)
--   4  Top IO Heavy Queries                    (Query Perf)
--   5  Currently Running + Blocking Queries    (Concurrency)
--   6  Execution Plans for Slow Queries        (Query Tuning)
--   7  Stored Procedure Performance            (SP Perf)
--   8  Missing Index Suggestions               (Index Health)
--   9  Index Fragmentation Report              (Index Health)
--  10  Unused Indexes                          (Index Cleanup)
--  11  Top Wait Statistics                     (Bottleneck Analysis)
--  12  TempDB Usage Per Session                (Resource Usage)
--  13  Memory Pressure Indicators              (Memory Health)
--  14  Deadlock History                        (Concurrency Issues)
--  15  Lock Escalation & Contention Pattern    (Locking Analysis)
--  16  Stale Statistics Report                 (DB Integrity)
--  17  Database Health & Configuration         (DB Status)
--  18  Transaction Log Status & VLF Check      (Log Health)
--  19  Page Compression Opportunities          (Storage Opt)
--  20  Plan Cache Regression / QS Fallback      (Query Analytics)
--  21  Parameter Sniffing Detection            (Query Issues)
--  22  Database IO Subsystem Analysis          (IO Performance)
--  23  Connection Pool Analysis                (Concurrency)
--  24  CPU & Scheduler Queue Analysis          (CPU Health)
--  25  Table Partitioning Opportunities        (Storage Opt)
--  26  Query Recompilation & Hints Analysis    (Query Stability)
--  27  Database Recovery Model & Backups       (Data Safety)
--  28  Object Permissions & Security Audit     (Security)
--  29  Server Configuration Issues             (Config Health)
--  30  Extended Events & Trace Configuration   (Monitoring)
-- ============================================================

IF OBJECT_ID('dbo.dbusp_PerformanceStats', 'P') IS NOT NULL
    DROP PROCEDURE dbo.dbusp_PerformanceStats;
GO

CREATE PROCEDURE dbo.dbusp_PerformanceStats
    @ThresholdAvgSec         FLOAT   = 2,        -- Avg elapsed time filter (seconds)
    @ThresholdCPUSec         FLOAT   = 1,        -- Avg CPU time filter (seconds)
    @ThresholdReads          BIGINT  = 10000,    -- Total logical+physical reads filter
    @TableSizeInMB           INT     = 1024,     -- Table size threshold (MB) -> 1 GB default
    @RowCount                BIGINT  = 10000000, -- Row count threshold -> 10 Million
    @TopRows                 INT     = 10,       -- Top N rows for query lists
    @DbName                  SYSNAME = N'',      -- Target DB; blank = current DB
    @FragmentationThreshold  FLOAT   = 30.0,     -- Index fragmentation % threshold
    @MinIndexSizePages       INT     = 500,      -- Skip small indexes in frag check
    @StaleStatsThresholdDays INT     = 7,        -- Statistics older than N days = stale
    @LockEscalationThreshold INT     = 5000,     -- Locks before escalation happens
    @QueryStoreThreshold     INT     = 10,       -- Query Store top N items
    @ShowSection             TINYINT = 0,        -- 0 = All sections; 1-30 = specific section
    @OnlyProblems            BIT     = 0,        -- 1 = hide INFO / healthy rows
    @IncludeRunbook          BIT     = 1         -- 1 = include DBA guidance text

AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY

        -- ── Resolve DB name ──────────────────────────────────────────
        IF @DbName = N'' OR @DbName IS NULL
            SET @DbName = DB_NAME();

        DECLARE @TargetDbId INT = DB_ID(@DbName);
        DECLARE @SqlMajorVersion INT;
        DECLARE @Edition NVARCHAR(128) = CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128));
        DECLARE @CpuCount INT;
        DECLARE @ServerStartTime DATETIME;

        SELECT
            @SqlMajorVersion = CONVERT(INT, PARSENAME(CONVERT(VARCHAR(32), SERVERPROPERTY('ProductVersion')), 4)),
            @CpuCount = cpu_count
        FROM sys.dm_os_sys_info;

        SELECT @ServerStartTime = create_date
        FROM sys.databases
        WHERE database_id = 2;

        -- ── Capture this SP own sql_handle to exclude from results ──
        DECLARE @ThisSqlHandle VARBINARY(64);
        SELECT @ThisSqlHandle = sql_handle
        FROM   sys.dm_exec_requests
        WHERE  session_id = @@SPID;

        -- ── Section Directory (always shown) ─────────────────────────
        SELECT ID, [Type], Title
        FROM (VALUES
            (1,  'Table Size',  'Large Tables (>' + CAST(@TableSizeInMB AS VARCHAR) + ' MB or >' + CAST(@RowCount AS VARCHAR) + ' rows)'),
            (2,  'Query Perf',  'Top CPU Heavy Queries'),
            (3,  'Query Perf',  'Top Duration Queries (Longest Avg)'),
            (4,  'Query Perf',  'Top IO Heavy Queries (Reads)'),
            (5,  'Concurrency', 'Currently Running + Blocking Queries'),
            (6,  'Query Tuning','Execution Plan XML for Top Slow Queries'),
            (7,  'SP Perf',     'Stored Procedure Performance Analysis'),
            (8,  'Index',       'Missing Index Suggestions'),
            (9,  'Index',       'Index Fragmentation Report'),
            (10, 'Index',       'Unused Indexes'),
            (11, 'Wait Stats',  'Top Wait Statistics (Server-Wide)'),
            (12, 'TempDB',      'TempDB Usage Per Session'),
            (13, 'Memory',      'Memory Pressure Indicators'),
            (14, 'Deadlock',    'Deadlock History'),
            (15, 'Concurrency', 'Lock Escalation & Contention Pattern'),
            (16, 'Integrity',   'Stale Statistics Report (>' + CAST(@StaleStatsThresholdDays AS VARCHAR) + ' days)'),
            (17, 'DB Health',   'Database Health & Configuration Status'),
            (18, 'DB Health',   'Transaction Log Status & VLF Health'),
            (19, 'Storage',     'Page Compression Opportunities'),
            (20, 'Query Store', 'Plan Cache Regression / Query Store Fallback'),
            (21, 'Query Issues','Parameter Sniffing Detection'),
            (22, 'IO Perf',     'Database IO Subsystem Analysis'),
            (23, 'Concurrency', 'Connection Pool Analysis'),
            (24, 'CPU Health',  'CPU & Scheduler Queue Analysis'),
            (25, 'Storage',     'Table Partitioning Opportunities'),
            (26, 'Query Stab',  'Query Recompilation & Query Hints Analysis'),
            (27, 'Data Safety', 'Database Recovery Model & Backup Status'),
            (28, 'Security',    'Object Permissions & Security Audit'),
            (29, 'Config',      'Server Configuration Issues & Best Practices'),
            (30, 'Monitoring',  'Extended Events & Trace Configuration')
        ) AS T(ID, [Type], Title);


        -- ============================================================
        -- SECTION 1 : Large Tables
        -- ============================================================
        IF @ShowSection IN (0, 1)
        BEGIN
            SELECT
                'Large Tables'                                                          AS [Section],
                t.NAME                                                                  AS [Table],
                SUM(CASE WHEN i.index_id < 2 THEN p.rows ELSE 0 END)                  AS [Row Count],
                CAST(SUM(a.total_pages) * 8.0 / 1024       AS DECIMAL(10,1))           AS [Total Size MB],
                CAST(SUM(a.total_pages) * 8.0 / 1024 / 1024 AS DECIMAL(10,3))          AS [Total Size GB],
                CAST(SUM(a.used_pages)  * 8.0 / 1024       AS DECIMAL(10,1))           AS [Used Size MB]
            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
            WHERE i.index_id < 2
            GROUP BY t.NAME
            HAVING
                (SUM(a.total_pages) * 8.0 / 1024) >= @TableSizeInMB
                OR SUM(CASE WHEN i.index_id < 2 THEN p.rows ELSE 0 END) >= @RowCount
            ORDER BY [Total Size MB] DESC;
        END


        -- ============================================================
        -- SECTION 2 : Top CPU Heavy Queries
        -- ============================================================
        IF @ShowSection IN (0, 2)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP (@TopRows)
                    'Top CPU Queries'                                                               AS [Section],
                    CASE
                        WHEN (QS.total_worker_time / NULLIF(QS.execution_count,0)) / 1000000.0 >= (@ThresholdCPUSec * 5) THEN 'CRITICAL'
                        WHEN (QS.total_worker_time / NULLIF(QS.execution_count,0)) / 1000000.0 >= (@ThresholdCPUSec * 2) THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                                             AS Severity,
                    'High CPU Query'                                                                AS IssueType,
                    QS.execution_count,
                    QS.creation_time                                                                AS FirstExecuted,
                    QS.last_execution_time                                                          AS LastExecuted,
                    CAST(QS.total_worker_time / 1000000.0 AS DECIMAL(18,2))                        AS TotalCPU_Sec,
                    CAST((QS.total_worker_time  / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgCPU_Sec,
                    CAST(QS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2))                       AS TotalElapsed_Sec,
                    CAST((QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsed_Sec,
                    QS.total_logical_reads + QS.total_physical_reads                                AS TotalReads,
                    QS.total_logical_writes                                                         AS TotalWrites,
                    DB_NAME(T.dbid)                                                                 AS DBName,
                    CASE
                        WHEN QS.total_logical_reads + QS.total_physical_reads > @ThresholdReads THEN 'Expensive scans, missing indexes, bad joins, or parameter sniffing can drive CPU.'
                        ELSE 'High CPU concentrated inside execution plan operators or repeated recompiles.'
                    END                                                                             AS LikelyCause,
                    CASE
                        WHEN QS.total_logical_reads + QS.total_physical_reads > @ThresholdReads THEN 'Review actual plan, reduce reads, and validate selective indexes before touching MAXDOP.'
                        ELSE 'Review execution plan for CPU-heavy operators and validate parameter stability.'
                    END                                                                             AS RecommendedAction,
                    'SET STATISTICS IO ON; SET STATISTICS TIME ON; -- Review actual plan for this statement' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Check plan cache entry, compare reads vs CPU, capture actual execution plan, and test with representative parameters in lower environment.'
                        ELSE NULL
                    END                                                                             AS DBARunbook,
                    T.text                                                                          AS FullQueryText,
                    ISNULL(NULLIF(LTRIM(RTRIM(
                        SUBSTRING(T.text,
                            (QS.statement_start_offset / 2) + 1,
                            CASE
                                WHEN QS.statement_end_offset = -1 THEN LEN(T.text)
                                ELSE (QS.statement_end_offset - QS.statement_start_offset) / 2
                            END
                        )
                    )), ''), T.text)                                                               AS ExactStatement
                FROM sys.dm_exec_query_stats QS
                CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
                WHERE (QS.total_worker_time / NULLIF(QS.execution_count,0)) / 1000000.0 > @ThresholdCPUSec
                  AND QS.sql_handle <> ISNULL(@ThisSqlHandle, 0x0)
                  AND DB_NAME(T.dbid) = @DbName
                  AND T.text IS NOT NULL
            ) AS CPUQ
            WHERE @OnlyProblems = 0 OR CPUQ.Severity <> 'INFO'
            ORDER BY CPUQ.TotalCPU_Sec DESC;
        END


        -- ============================================================
        -- SECTION 3 : Top Duration Queries
        -- ============================================================
        IF @ShowSection IN (0, 3)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP (@TopRows)
                    'Top Duration Queries'                                                          AS [Section],
                    CASE
                        WHEN (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 >= (@ThresholdAvgSec * 5) THEN 'CRITICAL'
                        WHEN (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 >= (@ThresholdAvgSec * 2) THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                                             AS Severity,
                    'Long Running Query'                                                            AS IssueType,
                    QS.execution_count,
                    QS.creation_time                                                                AS FirstExecuted,
                    QS.last_execution_time                                                          AS LastExecuted,
                    CAST(QS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2))                       AS TotalElapsed_Sec,
                    CAST((QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsed_Sec,
                    CAST(QS.total_worker_time  / 1000000.0 AS DECIMAL(18,2))                       AS TotalCPU_Sec,
                    QS.total_logical_reads + QS.total_physical_reads                                AS TotalReads,
                    DB_NAME(T.dbid)                                                                 AS DBName,
                    CASE
                        WHEN CAST(QS.total_worker_time AS FLOAT) / NULLIF(QS.total_elapsed_time,0) < 0.30 THEN 'Elapsed time is much higher than CPU, so waits, blocking, IO, or client fetch delay are likely.'
                        ELSE 'Query is expensive even when running, usually due to poor plan shape, large row counts, or repeated scans.'
                    END                                                                             AS LikelyCause,
                    CASE
                        WHEN CAST(QS.total_worker_time AS FLOAT) / NULLIF(QS.total_elapsed_time,0) < 0.30 THEN 'Correlate with wait stats and blocking section before changing indexes.'
                        ELSE 'Review joins, predicates, and statistics quality for this statement.'
                    END                                                                             AS RecommendedAction,
                    'SET STATISTICS IO ON; SET STATISTICS TIME ON; -- Capture actual plan and compare waits during execution' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'If duration is high but CPU is low, first check waits, blockers, TempDB spills, and storage latency. If both are high, tune plan shape and row reduction.'
                        ELSE NULL
                    END                                                                             AS DBARunbook,
                    T.text                                                                          AS FullQueryText,
                    ISNULL(NULLIF(LTRIM(RTRIM(
                        SUBSTRING(T.text,
                            (QS.statement_start_offset / 2) + 1,
                            CASE
                                WHEN QS.statement_end_offset = -1 THEN LEN(T.text)
                                ELSE (QS.statement_end_offset - QS.statement_start_offset) / 2
                            END
                        )
                    )), ''), T.text)                                                               AS ExactStatement
                FROM sys.dm_exec_query_stats QS
                CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
                WHERE (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 > @ThresholdAvgSec
                  AND QS.sql_handle <> ISNULL(@ThisSqlHandle, 0x0)
                  AND DB_NAME(T.dbid) = @DbName
                  AND T.text IS NOT NULL
            ) AS DurQ
            WHERE @OnlyProblems = 0 OR DurQ.Severity <> 'INFO'
            ORDER BY DurQ.AvgElapsed_Sec DESC;
        END


        -- ============================================================
        -- SECTION 4 : Top IO Heavy Queries
        -- ============================================================
        IF @ShowSection IN (0, 4)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP (@TopRows)
                    'Top IO Queries'                                                                AS [Section],
                    CASE
                        WHEN (QS.total_logical_reads + QS.total_physical_reads) >= (@ThresholdReads * 10) THEN 'CRITICAL'
                        WHEN (QS.total_logical_reads + QS.total_physical_reads) >= (@ThresholdReads * 3) THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                                             AS Severity,
                    'High Read Query'                                                               AS IssueType,
                    QS.execution_count,
                    QS.creation_time                                                                AS FirstExecuted,
                    QS.last_execution_time                                                          AS LastExecuted,
                    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,
                    QS.total_logical_writes                                                         AS TotalWrites,
                    CAST(QS.total_worker_time  / 1000000.0 AS DECIMAL(18,2))                       AS TotalCPU_Sec,
                    CAST(QS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2))                       AS TotalElapsed_Sec,
                    DB_NAME(T.dbid)                                                                 AS DBName,
                    'Large scans, non-selective predicates, stale statistics, or missing covering indexes are likely.' AS LikelyCause,
                    'Check missing/duplicate indexes, verify up-to-date statistics, and reduce returned rows.'          AS RecommendedAction,
                    'SET STATISTICS IO ON; -- Validate seekability and compare actual vs estimated row counts'          AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Start with estimated and actual execution plans, then validate index usage and stale stats before creating new indexes.'
                        ELSE NULL
                    END                                                                             AS DBARunbook,
                    T.text                                                                          AS FullQueryText,
                    ISNULL(NULLIF(LTRIM(RTRIM(
                        SUBSTRING(T.text,
                            (QS.statement_start_offset / 2) + 1,
                            CASE
                                WHEN QS.statement_end_offset = -1 THEN LEN(T.text)
                                ELSE (QS.statement_end_offset - QS.statement_start_offset) / 2
                            END
                        )
                    )), ''), T.text)                                                               AS ExactStatement
                FROM sys.dm_exec_query_stats QS
                CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
                WHERE (QS.total_logical_reads + QS.total_physical_reads) > @ThresholdReads
                  AND QS.sql_handle <> ISNULL(@ThisSqlHandle, 0x0)
                  AND DB_NAME(T.dbid) = @DbName
                  AND T.text IS NOT NULL
            ) AS IOQ
            WHERE @OnlyProblems = 0 OR IOQ.Severity <> 'INFO'
            ORDER BY IOQ.TotalReads DESC;
        END


        -- ============================================================
        -- SECTION 5 : Currently Running + Blocking Queries
        -- OUTER APPLY used for blocker — safe when blocking_session_id = NULL/0
        -- ============================================================
        IF @ShowSection IN (0, 5)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'Active + Blocking'                                         AS [Section],
                    CASE
                        WHEN R.blocking_session_id > 0 THEN 'CRITICAL'
                        WHEN R.wait_type LIKE 'LCK%' OR R.open_transaction_count > 0 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    CASE
                        WHEN R.blocking_session_id > 0 THEN 'Blocking'
                        WHEN R.wait_type LIKE 'LCK%' THEN 'Lock Wait'
                        ELSE 'Active Request'
                    END                                                         AS IssueType,
                    R.session_id,
                    R.status,
                    R.command,
                    DB_NAME(R.database_id)                                      AS DBName,
                    S.login_name                                                AS LoginName,
                    S.host_name                                                 AS HostName,
                    S.program_name                                              AS ProgramName,
                    T.text                                                      AS QueryText,
                    R.start_time,
                    CAST(R.total_elapsed_time / 1000.0 AS DECIMAL(18,1))        AS ElapsedTimeSec,
                    CAST(R.cpu_time           / 1000.0 AS DECIMAL(18,1))        AS CPUTimeSec,
                    R.granted_query_memory                                      AS GrantedMemoryKB,
                    R.reads                                                     AS CurrentReads,
                    R.writes                                                    AS CurrentWrites,
                    R.logical_reads                                             AS CurrentLogicalReads,
                    R.wait_type,
                    R.wait_time,
                    R.wait_resource,
                    R.blocking_session_id,
                    BT.text                                                     AS BlockingQueryText,
                    R.open_transaction_count                                    AS OpenTransactions,
                    CASE
                        WHEN R.blocking_session_id > 0 THEN 'Blocking chain detected. Open transaction, long-running DML, or uncommitted work is likely holding locks.'
                        WHEN R.wait_type LIKE 'LCK%' THEN 'Session is waiting on locks even if direct blocker text is not available.'
                        ELSE 'Request is active; review only if elapsed time or resource usage is high.'
                    END                                                         AS LikelyCause,
                    CASE
                        WHEN R.blocking_session_id > 0 THEN 'Review blocker session first, validate open transaction, and kill only after business confirmation.'
                        WHEN R.wait_type LIKE 'LCK%' THEN 'Trace blocker, commit/rollback offending transaction, and reduce transaction scope.'
                        ELSE 'Monitor request and capture plan if it remains active beyond normal runtime.'
                    END                                                         AS RecommendedAction,
                    CASE
                        WHEN R.blocking_session_id > 0 THEN 'DBCC INPUTBUFFER(' + CAST(R.blocking_session_id AS VARCHAR(20)) + ');'
                        ELSE 'DBCC INPUTBUFFER(' + CAST(R.session_id AS VARCHAR(20)) + ');'
                    END                                                         AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 AND R.blocking_session_id > 0 THEN 'Inspect blocker session, host, login, open transaction count, and business impact. Do not kill blocker before confirming rollback cost.'
                        WHEN @IncludeRunbook = 1 THEN 'Capture session text, wait type, and elapsed time. If request persists, correlate with waits, IO, and TempDB sections.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM       sys.dm_exec_requests  R
                CROSS APPLY sys.dm_exec_sql_text(R.sql_handle)  T
                LEFT  JOIN sys.dm_exec_sessions  S  ON R.session_id         = S.session_id
                LEFT  JOIN sys.dm_exec_requests  BR ON R.blocking_session_id = BR.session_id
                OUTER APPLY sys.dm_exec_sql_text(BR.sql_handle) BT
                WHERE R.session_id > 50
                  AND R.session_id <> @@SPID
                  AND DB_NAME(R.database_id) = @DbName
                  AND T.text IS NOT NULL
            ) AS BlockQ
            WHERE @OnlyProblems = 0 OR BlockQ.Severity <> 'INFO'
            ORDER BY BlockQ.ElapsedTimeSec DESC;
        END


        -- ============================================================
        -- SECTION 6 : Execution Plans for Slow Queries
        -- ============================================================
        IF @ShowSection IN (0, 6)
        BEGIN
            SELECT TOP (@TopRows)
                'Execution Plans'                                                               AS [Section],
                QS.execution_count,
                CAST(QS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2))                       AS TotalElapsed_Sec,
                CAST((QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsed_Sec,
                DB_NAME(T.dbid)                                                                 AS DBName,
                -- Full batch text
                T.text                                                                          AS FullQueryText,
                -- Exact slow statement
                ISNULL(NULLIF(LTRIM(RTRIM(
                    SUBSTRING(T.text,
                        (QS.statement_start_offset / 2) + 1,
                        CASE 
                            WHEN QS.statement_end_offset = -1 THEN LEN(T.text)
                            ELSE (QS.statement_end_offset - QS.statement_start_offset) / 2
                        END
                    )
                )), ''), T.text)                                                               AS ExactStatement,
                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
            WHERE (QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 > @ThresholdAvgSec
              AND QS.sql_handle <> ISNULL(@ThisSqlHandle, 0x0)
              AND DB_NAME(T.dbid) = @DbName
            ORDER BY AvgElapsed_Sec DESC;
        END


        -- ============================================================
        -- SECTION 7 : Stored Procedure Performance
        -- ============================================================
        IF @ShowSection IN (0, 7)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'SP Performance'                                                                AS [Section],
                    CASE
                        WHEN (PS.total_elapsed_time / NULLIF(PS.execution_count,0)) / 1000000.0 >= (@ThresholdAvgSec * 5) THEN 'CRITICAL'
                        WHEN (PS.total_elapsed_time / NULLIF(PS.execution_count,0)) / 1000000.0 >= (@ThresholdAvgSec * 2) THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                                             AS Severity,
                    'Slow Stored Procedure'                                                         AS IssueType,
                    DB_NAME(PS.database_id)                                                         AS DBName,
                    OBJECT_NAME(PS.object_id, PS.database_id)                                       AS ProcedureName,
                    PS.cached_time,
                    PS.last_execution_time,
                    PS.execution_count,
                    CAST(PS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2))                       AS TotalElapsedSec,
                    CAST((PS.total_elapsed_time / NULLIF(PS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsedSec,
                    CAST(PS.total_worker_time  / 1000000.0 AS DECIMAL(18,2))                       AS TotalCPUSec,
                    CAST((PS.total_worker_time / NULLIF(PS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgCPUSec,
                    PS.total_logical_reads                                                          AS TotalLogicalReads,
                    PS.total_logical_reads  / NULLIF(PS.execution_count,0)                         AS AvgLogicalReads,
                    PS.total_logical_writes                                                         AS TotalLogicalWrites,
                    PS.total_physical_reads                                                         AS TotalPhysicalReads,
                    PS.total_physical_reads / NULLIF(PS.execution_count,0)                         AS AvgPhysicalReads,
                    CASE
                        WHEN PS.total_physical_reads > 0 THEN 'Procedure is hitting storage and likely needs indexing, stats review, or row reduction.'
                        WHEN PS.total_logical_reads / NULLIF(PS.execution_count,0) > @ThresholdReads THEN 'Procedure is scan-heavy and may have poor parameter selectivity or stale stats.'
                        ELSE 'Procedure CPU/runtime is high and needs statement-level plan analysis.'
                    END                                                                             AS LikelyCause,
                    'Drill into top statements inside the procedure, review plan cache, and compare runtime by parameter set.' AS RecommendedAction,
                    'EXEC sp_helptext ''' + OBJECT_NAME(PS.object_id, PS.database_id) + ''';'     AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Capture the procedure text, identify the slowest statements, and test representative parameters with actual plans before changing indexes.'
                        ELSE NULL
                    END                                                                             AS DBARunbook
                FROM sys.dm_exec_procedure_stats PS
                WHERE DB_NAME(PS.database_id) = @DbName
                  AND (PS.total_elapsed_time / NULLIF(PS.execution_count,0)) / 1000000.0 > @ThresholdAvgSec
                  AND OBJECT_NAME(PS.object_id, PS.database_id) <> 'dbusp_PerformanceStats'
            ) AS SPQ
            WHERE @OnlyProblems = 0 OR SPQ.Severity <> 'INFO'
            ORDER BY SPQ.AvgElapsedSec DESC;
        END


        -- ============================================================
        -- SECTION 8 : Missing Index Suggestions
        -- SQL Engine internally track karta hai kaunsa index
        -- queries ke liye helpful hota — ye wahi list hai
        -- ImpactScore jitna bada = utna zaruri index
        -- ============================================================
        IF @ShowSection IN (0, 8)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP 20
                    'Missing Indexes'                                           AS [Section],
                    CASE
                        WHEN CAST(MIG.avg_total_user_cost * MIG.avg_user_impact * (MIG.user_seeks + MIG.user_scans) AS DECIMAL(18,1)) >= 100000 THEN 'CRITICAL'
                        WHEN CAST(MIG.avg_total_user_cost * MIG.avg_user_impact * (MIG.user_seeks + MIG.user_scans) AS DECIMAL(18,1)) >= 10000 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Missing Index Candidate'                                   AS IssueType,
                    DB_NAME(MID.database_id)                                    AS DBName,
                    OBJECT_NAME(MID.object_id, MID.database_id)                 AS [Table],
                    CAST(
                        MIG.avg_total_user_cost
                        * MIG.avg_user_impact
                        * (MIG.user_seeks + MIG.user_scans)
                    AS DECIMAL(18,1))                                           AS ImpactScore,
                    CAST(MIG.avg_user_impact AS DECIMAL(5,1))                   AS [Avg Impact %],
                    MIG.user_seeks                                              AS Seeks,
                    MIG.user_scans                                              AS Scans,
                    MID.equality_columns                                        AS EqualityCols,
                    MID.inequality_columns                                      AS InequalityCols,
                    MID.included_columns                                        AS IncludedCols,
                    'Optimizer repeatedly needed a better access path; existing indexes may be missing, non-covering, or stale.' AS LikelyCause,
                    'Validate duplicate/overlapping indexes and test this index in lower environment before production deployment.' AS RecommendedAction,
               'CREATE INDEX ' 
                + QUOTENAME(
                    LEFT(
                        'IX_Missing_' 
                        + OBJECT_NAME(MID.object_id, MID.database_id)
                        + '_'
                        + REPLACE(REPLACE(REPLACE(
                            ISNULL(REPLACE(MID.equality_columns, ', ', '_'), '') 
                            + CASE 
                                WHEN MID.inequality_columns IS NOT NULL 
                                THEN '_' + REPLACE(MID.inequality_columns, ', ', '_') 
                                ELSE '' 
                              END
                        , '[',''), ']',''), ',','')
                    , 120)  -- prevent >128 char error
                )
                + ' ON '
                + QUOTENAME(DB_NAME(MID.database_id)) + '.'
                + QUOTENAME(OBJECT_SCHEMA_NAME(MID.object_id, MID.database_id)) + '.'
                + QUOTENAME(OBJECT_NAME(MID.object_id, MID.database_id))
                + ' ('
                + ISNULL(MID.equality_columns, '')
                + CASE 
                    WHEN MID.inequality_columns IS NOT NULL THEN 
                        CASE WHEN MID.equality_columns IS NOT NULL THEN ', ' ELSE '' END
                        + MID.inequality_columns
                    ELSE '' 
                  END
                + ')'
                + CASE 
                    WHEN MID.included_columns IS NOT NULL 
                    THEN ' INCLUDE (' + MID.included_columns + ')'
                    ELSE '' 
                  END
                + ';' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Compare with existing indexes first. Avoid creating near-duplicate indexes, and measure write overhead after implementation.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM       sys.dm_db_missing_index_details       MID
                INNER JOIN sys.dm_db_missing_index_groups        MIG_GRP ON MID.index_handle           = MIG_GRP.index_handle
                INNER JOIN sys.dm_db_missing_index_group_stats   MIG     ON MIG_GRP.index_group_handle = MIG.group_handle
                WHERE DB_NAME(MID.database_id) = @DbName
            ) AS MIQ
            WHERE @OnlyProblems = 0 OR MIQ.Severity <> 'INFO'
            ORDER BY MIQ.ImpactScore DESC;
        END


        -- ============================================================
        -- SECTION 9 : Index Fragmentation
        -- 10-30% frag  -> REORGANIZE  (lightweight, online)
        -- >30%  frag   -> REBUILD     (heavier but thorough)
        -- ============================================================
        IF @ShowSection IN (0, 9)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'Index Fragmentation'                                       AS [Section],
                    CASE
                        WHEN IPS.avg_fragmentation_in_percent > @FragmentationThreshold THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Index Fragmentation'                                       AS IssueType,
                    @DbName                                                     AS DBName,
                    OBJECT_NAME(IPS.object_id, @TargetDbId)                     AS [Table],
                    I.name                                                      AS [Index],
                    IPS.index_type_desc                                         AS IndexType,
                    IPS.index_depth                                             AS [Depth],
                    IPS.page_count                                              AS Pages,
                    CAST(IPS.avg_fragmentation_in_percent AS DECIMAL(5,1))      AS [Frag %],
                    'Index has fragmented pages due to page splits and ongoing modifications.' AS LikelyCause,
                    CASE
                        WHEN IPS.avg_fragmentation_in_percent > @FragmentationThreshold THEN 'Schedule REBUILD in maintenance window; use REORGANIZE only for moderate fragmentation.'
                        ELSE 'Reorganize during low activity and then update statistics if needed.'
                    END                                                         AS RecommendedAction,
                    CASE
                        WHEN IPS.avg_fragmentation_in_percent > @FragmentationThreshold
                            THEN 'ALTER INDEX [' + I.name + '] ON [' + OBJECT_SCHEMA_NAME(IPS.object_id, @TargetDbId) + '].[' + OBJECT_NAME(IPS.object_id, @TargetDbId) + '] REBUILD;'
                        ELSE 'ALTER INDEX [' + I.name + '] ON [' + OBJECT_SCHEMA_NAME(IPS.object_id, @TargetDbId) + '].[' + OBJECT_NAME(IPS.object_id, @TargetDbId) + '] REORGANIZE;'
                    END                                                         AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Skip tiny indexes, avoid rebuilding during peak load, and review fill factor if the same index fragments quickly after maintenance.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.dm_db_index_physical_stats(@TargetDbId, NULL, NULL, NULL, 'LIMITED') IPS
                INNER JOIN sys.indexes I ON IPS.object_id = I.object_id AND IPS.index_id = I.index_id
                WHERE IPS.page_count >= @MinIndexSizePages
                  AND IPS.avg_fragmentation_in_percent >= 10
                  AND I.name IS NOT NULL
            ) AS FragQ
            WHERE @OnlyProblems = 0 OR FragQ.Severity <> 'INFO'
            ORDER BY FragQ.[Frag %] DESC;
        END


        -- ============================================================
        -- SECTION 10 : Unused Indexes
        -- Ye indexes likhe hain but queries use nahi karti
        -- INSERT/UPDATE/DELETE pe ye bhi maintain hote hain = overhead
        -- NOTE: Stats SQL Server restart pe reset hoti hain
        -- ============================================================
        IF @ShowSection IN (0, 10)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'Unused Indexes'                                            AS [Section],
                    CASE
                        WHEN ISNULL(IUS.user_updates,0) >= 1000 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Unused Index'                                              AS IssueType,
                    @DbName                                                     AS DBName,
                    OBJECT_NAME(I.object_id, @TargetDbId)                       AS [Table],
                    I.name                                                      AS [Index],
                    I.type_desc                                                 AS IndexType,
                    ISNULL(IUS.user_seeks,   0)                                 AS UserSeeks,
                    ISNULL(IUS.user_scans,   0)                                 AS UserScans,
                    ISNULL(IUS.user_lookups, 0)                                 AS UserLookups,
                    ISNULL(IUS.user_updates, 0)                                 AS UserUpdates,
                    IUS.last_user_seek                                          AS LastSeek,
                    IUS.last_user_scan                                          AS LastScan,
                    'Index is being maintained for writes but has shown no read benefit since the last server restart.' AS LikelyCause,
                    CASE
                        WHEN ISNULL(IUS.user_updates,0) >= 1000 THEN 'Validate usage history and dependency before dropping this index.'
                        ELSE 'Monitor across at least one business cycle before removal.'
                    END                                                         AS RecommendedAction,
                    'DROP INDEX [' + I.name + '] ON [' + OBJECT_SCHEMA_NAME(I.object_id, @TargetDbId) + '].[' + OBJECT_NAME(I.object_id, @TargetDbId) + '];' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Do not drop immediately after restart-based stats reset. Validate with query plans, job workloads, and reporting windows first.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM       sys.indexes I
                LEFT  JOIN sys.dm_db_index_usage_stats IUS
                        ON I.object_id = IUS.object_id
                       AND I.index_id = IUS.index_id
                       AND IUS.database_id = @TargetDbId
                INNER JOIN sys.objects O ON I.object_id = O.object_id
                WHERE O.type = 'U'
                  AND I.index_id > 0
                  AND I.is_primary_key = 0
                  AND I.is_unique_constraint = 0
                  AND (ISNULL(IUS.user_seeks,0) + ISNULL(IUS.user_scans,0) + ISNULL(IUS.user_lookups,0)) = 0
            ) AS UnusedQ
            WHERE @OnlyProblems = 0 OR UnusedQ.Severity <> 'INFO'
            ORDER BY UnusedQ.UserUpdates DESC;
        END


        -- ============================================================
        -- SECTION 11 : Top Wait Statistics
        -- Server kahan "wait" kar raha hai — root cause tool
        -- CXPACKET      -> Parallelism (check MAXDOP)
        -- LCK_M_*       -> Locking/Blocking
        -- PAGEIOLATCH_* -> Disk IO slow / missing indexes
        -- SOS_SCHEDULER -> CPU pressure
        -- WRITELOG      -> TLog IO slow
        -- RESOURCE_SEMAPHORE -> Memory grant waits
        -- ============================================================
        IF @ShowSection IN (0, 11)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP 20
                    'Wait Statistics'                                           AS [Section],
                    CASE
                        WHEN CAST(WS.wait_time_ms * 100.0 / NULLIF(SUM(WS.wait_time_ms) OVER(), 0) AS DECIMAL(5,1)) >= 25 THEN 'CRITICAL'
                        WHEN CAST(WS.wait_time_ms * 100.0 / NULLIF(SUM(WS.wait_time_ms) OVER(), 0) AS DECIMAL(5,1)) >= 10 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    CASE
                        WHEN WS.wait_type LIKE 'LCK%'             THEN 'Locking / Blocking'
                        WHEN WS.wait_type LIKE 'PAGEIOLATCH%'     THEN 'Disk IO'
                        WHEN WS.wait_type IN ('CXPACKET','CXCONSUMER') THEN 'Parallelism'
                        WHEN WS.wait_type LIKE 'SOS_SCHEDULER%'   THEN 'CPU Pressure'
                        WHEN WS.wait_type = 'WRITELOG'            THEN 'Transaction Log IO'
                        WHEN WS.wait_type = 'RESOURCE_SEMAPHORE'  THEN 'Memory Grant Pressure'
                        WHEN WS.wait_type LIKE 'ASYNC_NETWORK%'   THEN 'Client Fetch Delay'
                        ELSE 'General Wait'
                    END                                                         AS IssueType,
                    WS.wait_type,
                    CAST(WS.wait_time_ms / 1000.0 AS DECIMAL(18,1))             AS TotalWaitSec,
                    CAST(WS.signal_wait_time_ms / 1000.0 AS DECIMAL(18,1))      AS SignalWaitSec,
                    CAST((WS.wait_time_ms - WS.signal_wait_time_ms) / 1000.0 AS DECIMAL(18,1)) AS ResourceWaitSec,
                    WS.waiting_tasks_count                                      AS WaitingTasks,
                    CAST(WS.wait_time_ms * 100.0
                        / NULLIF(SUM(WS.wait_time_ms) OVER(), 0) AS DECIMAL(5,1)) AS [Wait %],
                    CASE
                        WHEN WS.wait_type LIKE 'LCK%'             THEN 'Blocking chains, long transactions, or hot rows/pages are dominating wait time.'
                        WHEN WS.wait_type LIKE 'PAGEIOLATCH%'     THEN 'Storage latency, large scans, or missing indexes are forcing physical reads.'
                        WHEN WS.wait_type IN ('CXPACKET','CXCONSUMER') THEN 'Parallel worker imbalance or low cost threshold can create excessive parallel waits.'
                        WHEN WS.wait_type LIKE 'SOS_SCHEDULER%'   THEN 'Runnable tasks are waiting for CPU time.'
                        WHEN WS.wait_type = 'WRITELOG'            THEN 'Transaction log flush latency or undersized log files are slowing commits.'
                        WHEN WS.wait_type = 'RESOURCE_SEMAPHORE'  THEN 'Large memory grants are queued behind limited workspace memory.'
                        WHEN WS.wait_type LIKE 'ASYNC_NETWORK%'   THEN 'Application/client is consuming result sets slowly.'
                        ELSE 'Review this wait with workload context before taking action.'
                    END                                                         AS LikelyCause,
                    CASE
                        WHEN WS.wait_type LIKE 'LCK%'             THEN 'Go to blocking and lock sections first; reduce transaction scope and indexing contention paths.'
                        WHEN WS.wait_type LIKE 'PAGEIOLATCH%'     THEN 'Review high-read queries, missing indexes, and storage latency.'
                        WHEN WS.wait_type IN ('CXPACKET','CXCONSUMER') THEN 'Validate MAXDOP and cost threshold after confirming the queries are worth parallelism.'
                        WHEN WS.wait_type LIKE 'SOS_SCHEDULER%'   THEN 'Review scheduler queue, expensive CPU queries, and server concurrency.'
                        WHEN WS.wait_type = 'WRITELOG'            THEN 'Inspect log disk latency, autogrowth pattern, and transaction batch size.'
                        WHEN WS.wait_type = 'RESOURCE_SEMAPHORE'  THEN 'Tune big memory-grant queries and validate max server memory settings.'
                        WHEN WS.wait_type LIKE 'ASYNC_NETWORK%'   THEN 'Review application fetch pattern and avoid returning unnecessary rows.'
                        ELSE 'Monitor with workload change history before remediation.'
                    END                                                         AS RecommendedAction,
                    CASE
                        WHEN WS.wait_type LIKE 'LCK%' THEN 'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 5;'
                        WHEN WS.wait_type LIKE 'PAGEIOLATCH%' THEN 'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 22;'
                        WHEN WS.wait_type = 'WRITELOG' THEN 'DBCC SQLPERF(LOGSPACE);'
                        ELSE '-- Investigate related section in this procedure'
                    END                                                         AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Wait stats are cumulative since last restart (' + CONVERT(VARCHAR(19), @ServerStartTime, 120) + '). Clear interpretation must include workload timing and recent incidents.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.dm_os_wait_stats WS
                WHERE WS.wait_type NOT IN (
                    'SLEEP_TASK','BROKER_TO_FLUSH','BROKER_EVENTHANDLER',
                    'REQUEST_FOR_DEADLOCK_SEARCH','LOGMGR_QUEUE','ONDEMAND_TASK_QUEUE',
                    'CHECKPOINT_QUEUE','DBMIRROR_EVENTS_QUEUE','SQLTRACE_BUFFER_FLUSH',
                    'CLR_AUTO_EVENT','DISPATCHER_QUEUE_SEMAPHORE','XE_DISPATCHER_WAIT',
                    'SLEEP_DBSTARTUP','SLEEP_DCOMSTARTUP','SLEEP_MASTERDBREADY',
                    'SLEEP_MASTERMDREADY','SLEEP_MASTERUPGRADED','SLEEP_MSDBSTARTUP',
                    'SLEEP_TEMPDBSTARTUP','SNI_HTTP_ACCEPT','SP_SERVER_DIAGNOSTICS_SLEEP',
                    'WAITFOR','XE_TIMER_EVENT','BROKER_RECEIVE_WAITFOR',
                    'WAITFOR_TASKSHUTDOWN','SLEEP_BUFFERPOOL_SERIALIZATIONTEST',
                    'SLEEP_SYSTEMTASK','HADR_FILESTREAM_IOMGR_IOCOMPLETION','HADR_WORK_QUEUE'
                )
            ) AS WaitQ
            WHERE @OnlyProblems = 0 OR WaitQ.Severity <> 'INFO'
            ORDER BY WaitQ.TotalWaitSec DESC;
        END


        -- ============================================================
        -- SECTION 12 : TempDB Usage Per Session
        -- Kaunsa session TempDB pe zyada load daal raha hai
        -- (#temp tables, sort spills, hash join spills)
        -- ============================================================
        IF @ShowSection IN (0, 12)
        BEGIN
            SELECT TOP (@TopRows)
                'TempDB Usage'                                                                  AS [Section],
                TSU.session_id,
                S.login_name,
                S.host_name,
                S.program_name,
                CAST(TSU.user_objects_alloc_page_count     * 8.0 / 1024 AS DECIMAL(10,1))      AS UserObjects_MB,
                CAST(TSU.internal_objects_alloc_page_count * 8.0 / 1024 AS DECIMAL(10,1))      AS InternalObjects_MB,
                CAST((TSU.user_objects_alloc_page_count
                    + TSU.internal_objects_alloc_page_count) * 8.0 / 1024 AS DECIMAL(10,1))    AS TotalTempDB_MB,
                T.text                                                                          AS CurrentQuery
            FROM       sys.dm_db_session_space_usage   TSU
            INNER JOIN sys.dm_exec_sessions             S   ON TSU.session_id = S.session_id
            LEFT  JOIN sys.dm_exec_requests             R   ON TSU.session_id = R.session_id
            OUTER APPLY sys.dm_exec_sql_text(R.sql_handle)  T
            WHERE TSU.session_id > 50
              AND (TSU.user_objects_alloc_page_count + TSU.internal_objects_alloc_page_count) > 0
            ORDER BY TotalTempDB_MB DESC;
        END


        -- ============================================================
        -- SECTION 13 : Memory Pressure Indicators
        -- ============================================================
        IF @ShowSection IN (0, 13)
        BEGIN
            -- 13a. Overall Memory Summary
            SELECT
                'Memory Summary'                                            AS [Section],
                CASE
                    WHEN memory_utilization_percentage >= 95 OR page_fault_count > 0 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'SQL Memory Pressure'                                       AS IssueType,
                physical_memory_in_use_kb / 1024                           AS [SQL Mem Used MB],
                memory_utilization_percentage                               AS [Mem Utilization %],
                page_fault_count                                            AS PageFaults,
                CASE
                    WHEN memory_utilization_percentage >= 95 THEN 'SQL Server is using almost all memory available within current target.'
                    WHEN page_fault_count > 0 THEN 'Process-level page faults indicate operating system memory churn.'
                    ELSE 'No immediate SQL memory pressure signal from process memory.'
                END                                                         AS LikelyCause,
                CASE
                    WHEN memory_utilization_percentage >= 95 THEN 'Validate max server memory against OS needs and correlate with pending memory grants.'
                    WHEN page_fault_count > 0 THEN 'Review OS memory pressure, antivirus exclusions, and non-SQL memory consumers.'
                    ELSE 'Monitor with memory grants and wait stats.'
                END                                                         AS RecommendedAction,
                'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 29;' AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'Treat this as an indicator only. Confirm with RESOURCE_SEMAPHORE waits, PLE trend, and max server memory configuration before changing memory.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM sys.dm_os_process_memory
            WHERE @OnlyProblems = 0
               OR memory_utilization_percentage >= 95
               OR page_fault_count > 0;

            -- 13b. Buffer Pool usage by Database
            SELECT
                'Buffer Pool by DB'                                         AS [Section],
                CASE database_id
                    WHEN 32767 THEN 'ResourceDB'
                    ELSE DB_NAME(database_id)
                END                                                         AS DBName,
                COUNT(*) * 8 / 1024                                         AS BufferPool_MB,
                SUM(CASE WHEN is_modified = 1 THEN 1 ELSE 0 END) * 8 / 1024 AS DirtyPages_MB
            FROM sys.dm_os_buffer_descriptors
            GROUP BY database_id
            ORDER BY BufferPool_MB DESC;

            -- 13c. Memory Grants (waiter_count > 0 = memory pressure)
            SELECT
                'Memory Grants'                                             AS [Section],
                CASE
                    WHEN SUM(waiter_count) > 0 THEN 'CRITICAL'
                    WHEN SUM(available_memory_kb) / 1024 < 512 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'Workspace Memory Grants'                                   AS IssueType,
                SUM(waiter_count)        AS PendingMemoryGrants,   -- > 0 is bad
                SUM(grantee_count)       AS GrantsOutstanding,
                SUM(target_memory_kb)  / 1024 AS TargetMemoryMB,
                SUM(total_memory_kb)   / 1024 AS TotalMemoryMB,
                SUM(available_memory_kb) / 1024 AS AvailableMemoryMB,
                CASE
                    WHEN SUM(waiter_count) > 0 THEN 'Queries are queued for memory grants, usually due to large sorts, hashes, or concurrent big reports.'
                    WHEN SUM(available_memory_kb) / 1024 < 512 THEN 'Available grant memory is low and may soon queue large queries.'
                    ELSE 'Memory grant subsystem currently looks stable.'
                END                                                         AS LikelyCause,
                CASE
                    WHEN SUM(waiter_count) > 0 THEN 'Tune largest memory grant queries, reduce spills, and validate max server memory.'
                    WHEN SUM(available_memory_kb) / 1024 < 512 THEN 'Review concurrent reporting workload and expensive sort/hash plans.'
                    ELSE 'Continue monitoring.'
                END                                                         AS RecommendedAction,
                'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 11;' AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'Capture currently running heavy queries when pending grants are non-zero; the blockers are often active reports or bad join/sort plans.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM sys.dm_exec_query_resource_semaphores
            HAVING @OnlyProblems = 0
                OR SUM(waiter_count) > 0
                OR SUM(available_memory_kb) / 1024 < 512;
        END


        -- ============================================================
        -- SECTION 14 : Deadlock History
        -- ============================================================
        IF @ShowSection IN (0, 14)
        BEGIN
            SELECT
                'Deadlock Guidance'                                          AS [Section],
                CASE
                    WHEN OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL THEN 'INFO'
                    ELSE 'WARNING'
                END                                                          AS Severity,
                'Deadlock Capture'                                           AS IssueType,
                CASE
                    WHEN OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL THEN 'Helper procedure is available for deadlock history review.'
                    ELSE 'No local deadlock helper procedure was found.'
                END                                                          AS LikelyCause,
                CASE
                    WHEN OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL THEN 'Review captured deadlock graph and identify object access order mismatch.'
                    ELSE 'Enable deadlock capture through system_health Extended Events or trace flag based monitoring.'
                END                                                          AS RecommendedAction,
                CASE
                    WHEN OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL THEN 'EXEC dbo.dbusp_DeadLockTree;'
                    ELSE 'SELECT CAST(target_data AS XML) FROM sys.dm_xe_session_targets st INNER JOIN sys.dm_xe_sessions s ON st.event_session_address = s.address WHERE s.name = ''system_health'';'
                END                                                          AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 AND OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL THEN 'Review victim and survivor statements, isolate index/order differences, and reduce transaction overlap.'
                    WHEN @IncludeRunbook = 1 THEN 'On SQL Server 2012, system_health usually captures deadlocks. Extract XML deadlock graph, then fix access order, indexing, or transaction scope.'
                    ELSE NULL
                END                                                          AS DBARunbook
            WHERE @OnlyProblems = 0
               OR OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NULL;

            IF OBJECT_ID('dbo.dbusp_DeadLockTree', 'P') IS NOT NULL
                EXEC dbo.dbusp_DeadLockTree;
        END


        -- ============================================================
        -- SECTION 15 : Lock Escalation & Contention Pattern
        -- Shows current locks, escalation trends, and blocking patterns
        -- ============================================================
        IF @ShowSection IN (0, 15)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'Lock Contention'                                           AS [Section],
                    CASE
                        WHEN SUM(1) OVER (PARTITION BY TL.resource_type, TL.resource_database_id) > @LockEscalationThreshold THEN 'CRITICAL'
                        WHEN TL.request_mode IN ('X','IX','SIX') THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Lock Contention'                                           AS IssueType,
                    R.session_id                                                AS SessionID,
                    R.database_id                                               AS DBId,
                    DB_NAME(R.database_id)                                      AS DBName,
                    CONVERT(VARCHAR(100), R.command)                            AS Command,
                    CONVERT(VARCHAR(100), R.status)                             AS [Status],
                    TL.request_mode                                             AS LockMode,
                    TL.request_type                                             AS LockType,
                    TL.request_status                                           AS LockStatus,
                    SUM(1) OVER (PARTITION BY TL.resource_type, TL.resource_database_id) AS LockCount,
                    CASE
                        WHEN SUM(1) OVER (PARTITION BY TL.resource_type, TL.resource_database_id) > @LockEscalationThreshold
                            THEN 'Large lock footprint suggests escalation risk or hot object contention.'
                        ELSE 'Current lock mix may still become a hotspot if transactions stay open.'
                    END                                                         AS LikelyCause,
                    CASE
                        WHEN SUM(1) OVER (PARTITION BY TL.resource_type, TL.resource_database_id) > @LockEscalationThreshold
                            THEN 'Reduce batch size, add selective indexes, and shorten transaction scope.'
                        ELSE 'Monitor lock footprint and correlate with blocker sessions.'
                    END                                                         AS RecommendedAction,
                    'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 5;' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Large lock counts usually come from wide updates/deletes or poor seekability. Review affected statements before considering lock hints.'
                        ELSE NULL
                    END                                                         AS DBARunbook,
                    T.text                                                      AS QueryText
                FROM       sys.dm_exec_requests R
                INNER JOIN sys.dm_tran_locks TL ON R.session_id = TL.request_session_id
                LEFT  JOIN sys.dm_exec_sessions S ON R.session_id = S.session_id
                OUTER APPLY sys.dm_exec_sql_text(R.sql_handle) T
                WHERE R.session_id > 50
                  AND R.database_id = @TargetDbId
                  AND TL.request_status <> 'WAIT'
            ) AS LockQ
            WHERE @OnlyProblems = 0 OR LockQ.Severity <> 'INFO'
            ORDER BY LockQ.LockCount DESC, LockQ.SessionID;
        END


        -- ============================================================
        -- SECTION 16 : Stale Statistics Report
        -- Shows tables where statistics haven't been updated in N days
        -- Stale stats = bad cardinality estimates = bad query plans
        -- ============================================================
        IF @ShowSection IN (0, 16)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP 50
                    'Stale Statistics'                                          AS [Section],
                    CASE
                        WHEN DATEDIFF(DAY, STATS_DATE(S.object_id, S.stats_id), GETDATE()) > (@StaleStatsThresholdDays * 4) THEN 'CRITICAL'
                        WHEN DATEDIFF(DAY, STATS_DATE(S.object_id, S.stats_id), GETDATE()) > @StaleStatsThresholdDays THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Stale Statistics'                                          AS IssueType,
                    @DbName                                                     AS DBName,
                    OBJECT_NAME(S.object_id, @TargetDbId)                       AS [Table],
                    S.name                                                      AS StatName,
                    CAST(STATS_DATE(S.object_id, S.stats_id) AS DATE)           AS LastUpdated,
                    DATEDIFF(DAY, STATS_DATE(S.object_id, S.stats_id), GETDATE()) AS StaleInDays,
                    'Cardinality estimates may drift after significant data change or prolonged time without stats refresh.' AS LikelyCause,
                    CASE
                        WHEN DATEDIFF(DAY, STATS_DATE(S.object_id, S.stats_id), GETDATE()) > @StaleStatsThresholdDays THEN 'Update statistics for this object and retest affected queries.'
                        ELSE 'Monitor high-change tables for auto-update effectiveness.'
                    END                                                         AS RecommendedAction,
                    'UPDATE STATISTICS [' + OBJECT_SCHEMA_NAME(S.object_id, @TargetDbId) + '].[' + OBJECT_NAME(S.object_id, @TargetDbId) + '] [' + S.name + '];' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Prioritize large or volatile tables first. If parameter-sensitive workloads improved after stats refresh, review auto-stats and maintenance cadence.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.stats S
                WHERE OBJECTPROPERTY(S.object_id, 'IsUserTable') = 1
                  AND STATS_DATE(S.object_id, S.stats_id) IS NOT NULL
            ) AS StatsQ
            WHERE @OnlyProblems = 0 OR StatsQ.Severity <> 'INFO'
            ORDER BY StatsQ.StaleInDays DESC;
        END


        -- ============================================================
        -- SECTION 17 : Database Health & Configuration Status
        -- Recovery model, compatibility, flags, space info
        -- ============================================================
        IF @ShowSection IN (0, 17)
        BEGIN
            SELECT
                'Database Health'                                           AS [Section],
                CASE
                    WHEN D.state_desc <> 'ONLINE' OR D.is_auto_close_on = 1 OR D.is_auto_shrink_on = 1 THEN 'CRITICAL'
                    WHEN D.page_verify_option_desc <> 'CHECKSUM' OR D.compatibility_level < 110 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'Database Configuration'                                    AS IssueType,
                D.name                                                      AS DBName,
                D.recovery_model_desc                                       AS RecoveryModel,
                D.state_desc                                                AS [State],
                D.compatibility_level                                       AS CompatLevel,
                D.is_read_only                                              AS IsReadOnly,
                D.is_auto_close_on                                          AS AutoClose_On,
                D.is_auto_shrink_on                                         AS AutoShrink_On,
                D.page_verify_option_desc                                   AS PageVerify,
                SUM(CAST(MF.size AS BIGINT)) * 8 / 1024 / 1024              AS TotalSizeMB,
                CASE
                    WHEN D.state_desc <> 'ONLINE' THEN 'Database is not fully online.'
                    WHEN D.is_auto_close_on = 1 THEN 'AUTO_CLOSE can cause repeated open/close overhead and plan cache churn.'
                    WHEN D.is_auto_shrink_on = 1 THEN 'AUTO_SHRINK can create fragmentation and unnecessary IO.'
                    WHEN D.page_verify_option_desc <> 'CHECKSUM' THEN 'Page verification is weaker than CHECKSUM.'
                    WHEN D.compatibility_level < 110 THEN 'Compatibility level is below SQL Server 2012 native level.'
                    ELSE 'Database health indicators are within expected baseline.'
                END                                                         AS LikelyCause,
                CASE
                    WHEN D.is_auto_close_on = 1 OR D.is_auto_shrink_on = 1 THEN 'Disable AUTO_CLOSE / AUTO_SHRINK unless a niche use case requires them.'
                    WHEN D.page_verify_option_desc <> 'CHECKSUM' THEN 'Set PAGE_VERIFY CHECKSUM.'
                    WHEN D.compatibility_level < 110 THEN 'Review application compatibility and raise database compatibility level when safe.'
                    ELSE 'Continue baseline monitoring.'
                END                                                         AS RecommendedAction,
                CASE
                    WHEN D.is_auto_close_on = 1 THEN 'ALTER DATABASE [' + D.name + '] SET AUTO_CLOSE OFF;'
                    WHEN D.is_auto_shrink_on = 1 THEN 'ALTER DATABASE [' + D.name + '] SET AUTO_SHRINK OFF;'
                    WHEN D.page_verify_option_desc <> 'CHECKSUM' THEN 'ALTER DATABASE [' + D.name + '] SET PAGE_VERIFY CHECKSUM;'
                    ELSE '-- No immediate fix required'
                END                                                         AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'Configuration fixes should be applied during controlled change windows and validated against vendor/application requirements.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM sys.databases D
            INNER JOIN sys.master_files MF ON D.database_id = MF.database_id
            WHERE D.name = @DbName
              AND (
                    @OnlyProblems = 0
                    OR D.state_desc <> 'ONLINE'
                    OR D.is_auto_close_on = 1
                    OR D.is_auto_shrink_on = 1
                    OR D.page_verify_option_desc <> 'CHECKSUM'
                    OR D.compatibility_level < 110
                  )
            GROUP BY D.name, D.recovery_model_desc, D.state_desc, D.compatibility_level,
                     D.is_read_only, D.is_auto_close_on, D.is_auto_shrink_on, D.page_verify_option_desc, D.database_id;
        END


        -- ============================================================
        -- SECTION 18 : Transaction Log Status & VLF Health
        -- High VLF count = more log fragmentation = worse performance
        -- IdeaL: <100 VLFs for healthy log
        -- ============================================================
        IF @ShowSection IN (0, 18)
        BEGIN
            CREATE TABLE #LogSpace
            (
                DatabaseName SYSNAME,
                LogSizeMB DECIMAL(18,5),
                LogSpaceUsedPct DECIMAL(18,5),
                [Status] INT
            );

            INSERT INTO #LogSpace
            EXEC ('DBCC SQLPERF(LOGSPACE) WITH NO_INFOMSGS');

            SELECT
                'TLog Status'                                               AS [Section],
                CASE
                    WHEN LS.LogSpaceUsedPct >= 90 THEN 'CRITICAL'
                    WHEN LS.LogSpaceUsedPct >= 70 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'Transaction Log Health'                                    AS IssueType,
                LS.DatabaseName                                             AS DBName,
                CAST(LS.LogSizeMB AS DECIMAL(10,1))                         AS LogSizeMB,
                CAST(LS.LogSizeMB * LS.LogSpaceUsedPct / 100.0 AS DECIMAL(10,1)) AS LogUsedMB,
                CAST(LS.LogSpaceUsedPct AS DECIMAL(10,2))                   AS LogUsedPct,
                'High log usage can come from large transactions, missing log backups, or undersized log/autogrowth pattern.' AS LikelyCause,
                CASE
                    WHEN LS.LogSpaceUsedPct >= 90 THEN 'Take immediate action: check active transaction, log backup chain, and log autogrowth history.'
                    WHEN LS.LogSpaceUsedPct >= 70 THEN 'Review log backup frequency and growth settings before log fills further.'
                    ELSE 'Continue monitoring log utilization.'
                END                                                         AS RecommendedAction,
                'DBCC SQLPERF(LOGSPACE); DBCC LOGINFO(' + QUOTENAME(@DbName, '''') + ');' AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'If recovery model is FULL, confirm recent log backups. If log reuse is blocked, inspect log_reuse_wait_desc and active long transactions.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM #LogSpace LS
            WHERE LS.DatabaseName = @DbName
              AND (@OnlyProblems = 0 OR LS.LogSpaceUsedPct >= 70);

            DROP TABLE #LogSpace;
        END


        -- ============================================================
        -- SECTION 19 : Page Compression Opportunities
        -- Identifies tables that could benefit from compression
        -- Needs 2008+ Enterprise Edition
        -- ============================================================
        IF @ShowSection IN (0, 19)
        BEGIN
            SELECT
                'Compression Opportunities'                                 AS [Section],
                OBJECT_SCHEMA_NAME(IPS.object_id)                          AS SchemaName,
                OBJECT_NAME(IPS.object_id)                                  AS [Table],
                I.name                                                      AS [Index],
                IPS.page_count                                              AS UncompressedPages,
                CAST(IPS.page_count * 8 / 1024.0 AS DECIMAL(10,1))         AS UncompressedSizeMB,
                CASE
                    WHEN IPS.page_count > 1000
                        THEN 'ALTER TABLE [' + OBJECT_SCHEMA_NAME(IPS.object_id) + '].[' 
                            + OBJECT_NAME(IPS.object_id) + '] REBUILD WITH (DATA_COMPRESSION = PAGE);'
                    ELSE 'Too small for compression benefit'
                END                                                         AS CompressionScript
            FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') IPS
            INNER JOIN sys.indexes I ON IPS.object_id = I.object_id AND IPS.index_id = I.index_id
            WHERE IPS.page_count > 1000
              AND IPS.alloc_unit_type_desc = 'IN_ROW_DATA'
            ORDER BY IPS.page_count DESC;
        END


        -- ============================================================
        -- SECTION 20 : Plan Cache Regression / Query Store Fallback
        -- SQL Server 2012 friendly fallback for regressed / unstable plans
        -- ============================================================
        IF @ShowSection IN (0, 20)
        BEGIN
            SELECT *
            FROM
            (
                SELECT TOP (@QueryStoreThreshold)
                    'Plan Cache Regression'                                   AS [Section],
                    CASE
                        WHEN QS.plan_generation_num >= 10 OR QS.max_elapsed_time >= (QS.min_elapsed_time * 20) THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                       AS Severity,
                    'Plan Regression / Instability'                           AS IssueType,
                    DB_NAME(T.dbid)                                           AS DBName,
                    QS.execution_count                                         AS ExecutionCount,
                    QS.plan_generation_num                                     AS RecompilationCount,
                    CAST(QS.min_elapsed_time / 1000000.0 AS DECIMAL(18,4))    AS MinElapsedSec,
                    CAST(QS.max_elapsed_time / 1000000.0 AS DECIMAL(18,4))    AS MaxElapsedSec,
                    CAST((QS.max_elapsed_time - QS.min_elapsed_time) / 1000000.0 AS DECIMAL(18,4)) AS VarianceSec,
                    sys.fn_varbintohexstr(QS.plan_handle)                      AS PlanHandleHex,
                    SUBSTRING(T.text, 1, 200)                                  AS QueryPreview,
                    'Plan cache shows high runtime variance or repeated recompiles, often due to parameter sensitivity, stats changes, or unstable memory grants.' AS LikelyCause,
                    'Compare good vs bad parameter sets, inspect cached plan, and consider stats refresh or targeted recompilation strategy.' AS RecommendedAction,
                    'DBCC FREEPROCCACHE(' + sys.fn_varbintohexstr(QS.plan_handle) + '); -- clear only this plan after validation' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'On SQL Server 2012 this section is a Query Store fallback. Validate parameter sniffing, stale statistics, and plan cache volatility before forcing plan-level changes.'
                        ELSE NULL
                    END                                                       AS DBARunbook
                FROM sys.dm_exec_query_stats QS
                CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
                WHERE DB_NAME(T.dbid) = @DbName
                  AND T.text IS NOT NULL
                  AND QS.execution_count > 5
                  AND QS.sql_handle <> ISNULL(@ThisSqlHandle, 0x0)
            ) AS PlanQ
            WHERE @OnlyProblems = 0 OR PlanQ.Severity <> 'INFO'
            ORDER BY PlanQ.VarianceSec DESC, PlanQ.RecompilationCount DESC;
        END


        -- ============================================================
        -- SECTION 21 : Parameter Sniffing Detection
        -- Queries with high variance in execution time/reads across invocations
        -- = potential parameter sniffing victims
        -- ============================================================
        IF @ShowSection IN (0, 21)
        BEGIN
            SELECT TOP (@TopRows)
                'Parameter Sniffing Risk'                                   AS [Section],
                CAST(QS.total_elapsed_time / 1000000.0 AS DECIMAL(18,2)) AS TotalElapsedSec,
                QS.execution_count,
                CAST((QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsedSec,
                QS.total_logical_reads,
                CAST((QS.total_logical_reads / NULLIF(QS.execution_count,0)) AS DECIMAL(10,0)) AS AvgReads,
                DB_NAME(T.dbid) AS DBName,
                SUBSTRING(T.text, 1, 100) AS QueryPreview,
                'HIGH VARIANCE - Monitor for sniffing' AS Recommendation
            FROM sys.dm_exec_query_stats QS
            CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
            WHERE DB_NAME(T.dbid) = @DbName
              AND T.text IS NOT NULL
              AND QS.execution_count > 10
              AND QS.min_elapsed_time < QS.max_elapsed_time * 0.1
            ORDER BY (QS.max_elapsed_time - QS.min_elapsed_time) DESC;
        END


        -- ============================================================
        -- SECTION 22 : Database IO Subsystem Analysis
        -- Physical reads, writes, latency metrics from DMVs
        -- ============================================================
        IF @ShowSection IN (0, 22)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'IO Subsystem'                                              AS [Section],
                    CASE
                        WHEN CAST(MFFP.io_stall_read_ms AS FLOAT) / NULLIF(MFFP.num_of_reads,0) >= 50
                          OR CAST(MFFP.io_stall_write_ms AS FLOAT) / NULLIF(MFFP.num_of_writes,0) >= 20 THEN 'CRITICAL'
                        WHEN CAST(MFFP.io_stall_read_ms AS FLOAT) / NULLIF(MFFP.num_of_reads,0) >= 20
                          OR CAST(MFFP.io_stall_write_ms AS FLOAT) / NULLIF(MFFP.num_of_writes,0) >= 10 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Database File Latency'                                     AS IssueType,
                    DB_NAME(MFFP.database_id)                                   AS DBName,
                    MFFP.file_id,
                    MF.name                                                     AS FileName,
                    MF.physical_name                                            AS PhysicalPath,
                    MF.type_desc                                                AS FileType,
                    MFFP.num_of_reads                                           AS PhysicalReads,
                    CAST(MFFP.num_of_bytes_read / 1024.0 / 1024.0 AS DECIMAL(10,1)) AS DataReadMB,
                    CAST(MFFP.io_stall_read_ms / 1000.0 AS DECIMAL(10,1))       AS ReadLatencySec,
                    CAST(CAST(MFFP.io_stall_read_ms AS FLOAT) / NULLIF(MFFP.num_of_reads,0) AS DECIMAL(10,2)) AS AvgReadLatencyMs,
                    MFFP.num_of_writes                                          AS PhysicalWrites,
                    CAST(MFFP.num_of_bytes_written / 1024.0 / 1024.0 AS DECIMAL(10,1)) AS DataWrittenMB,
                    CAST(MFFP.io_stall_write_ms / 1000.0 AS DECIMAL(10,1))      AS WriteLatencySec,
                    CAST(CAST(MFFP.io_stall_write_ms AS FLOAT) / NULLIF(MFFP.num_of_writes,0) AS DECIMAL(10,2)) AS AvgWriteLatencyMs,
                    CAST(MFFP.io_stall / 1000.0 AS DECIMAL(10,1))               AS TotalLatencySec,
                    'Slow storage, overloaded LUN, large scans, or repeated autogrowth can increase file stall time.' AS LikelyCause,
                    'Review storage latency, file growth settings, and top read/write queries before moving files.' AS RecommendedAction,
                    'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 4;' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Use average latency ms per read/write as the first signal. Data files above ~20ms read or log files above ~10ms write deserve investigation.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.dm_io_virtual_file_stats(@TargetDbId, NULL) MFFP
                INNER JOIN sys.master_files MF ON MFFP.database_id = MF.database_id AND MFFP.file_id = MF.file_id
            ) AS IOFileQ
            WHERE @OnlyProblems = 0 OR IOFileQ.Severity <> 'INFO'
            ORDER BY IOFileQ.TotalLatencySec DESC;
        END


        -- ============================================================
        -- SECTION 23 : Connection Pool Analysis
        -- Current connections, idle sessions, potential connection issues
        -- ============================================================
        IF @ShowSection IN (0, 23)
        BEGIN
            SELECT
                'Connection Analysis'                                       AS [Section],
                COUNT(*)                                                    AS TotalConnections,
                SUM(CASE WHEN S.status = 'running' THEN 1 ELSE 0 END)      AS RunningConnections,
                SUM(CASE WHEN S.status = 'sleeping' THEN 1 ELSE 0 END)     AS SleepingConnections,
                SUM(CASE WHEN S.status = 'dormant' THEN 1 ELSE 0 END)      AS DormantConnections,
                MAX(DATEDIFF(SECOND, S.last_request_end_time, GETDATE())) AS MaxIdleSecond
            FROM sys.dm_exec_sessions S
            WHERE S.session_id > 50;

            -- Top connections by database
            SELECT TOP 20
                'Connections by DB'                                         AS [Section],
                DB_NAME(S.database_id) AS DBName,
                S.login_name,
                S.host_name,
                S.program_name,
                COUNT(*) AS ConnectionCount,
                SUM(S.memory_usage) * 8 / 1024 AS MemoryUsageMB
            FROM sys.dm_exec_sessions S
            WHERE S.session_id > 50
            GROUP BY DB_NAME(S.database_id), S.login_name, S.host_name, S.program_name
            ORDER BY ConnectionCount DESC;
        END


        -- ============================================================
        -- SECTION 24 : CPU & Scheduler Queue Analysis
        -- CPU throttling, runnable tasks, scheduler health
        -- ============================================================
        IF @ShowSection IN (0, 24)
        BEGIN
            -- Scheduler stats
            SELECT *
            FROM
            (
                SELECT
                    'Scheduler Health'                                          AS [Section],
                    CASE
                        WHEN runnable_tasks_count > 10 THEN 'CRITICAL'
                        WHEN runnable_tasks_count > 5 THEN 'WARNING'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'CPU Scheduler Queue'                                        AS IssueType,
                    scheduler_id,
                    cpu_id,
                    status,
                    runnable_tasks_count,
                    current_tasks_count,
                    CAST(work_queue_count AS DECIMAL(10,0))                     AS QueuedTasks,
                    CASE
                        WHEN runnable_tasks_count > 5 THEN 'Schedulers have runnable tasks waiting for CPU, indicating CPU pressure or excessive parallelism.'
                        ELSE 'Scheduler queue depth is within normal range.'
                    END                                                         AS LikelyCause,
                    CASE
                        WHEN runnable_tasks_count > 5 THEN 'Review CPU-heavy queries, MAXDOP, and concurrent workload bursts.'
                        ELSE 'Continue monitoring.'
                    END                                                         AS RecommendedAction,
                    'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 2;' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Sustained runnable queue is more important than a one-time spike. Re-sample during business peak before making server-wide changes.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.dm_os_schedulers
                WHERE scheduler_id < 256
            ) AS SchedQ
            WHERE @OnlyProblems = 0 OR SchedQ.Severity <> 'INFO'
            ORDER BY SchedQ.runnable_tasks_count DESC;

            -- CPU utilization over time
            SELECT
                'CPU Utilization'                                           AS [Section],
                CASE
                    WHEN CAST(SUM(signal_wait_time_ms) * 100.0 / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,1)) >= 25 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'CPU Signal Waits'                                          AS IssueType,
                CAST(SUM(signal_wait_time_ms) * 100.0 / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,1)) AS CPUUtilizationPercent,
                COUNT(DISTINCT wait_type)                                   AS UniqueWaitTypes,
                'High signal wait ratio means workers are waiting for CPU after getting in line.' AS LikelyCause,
                'If ratio remains high with runnable tasks, tune expensive CPU queries or reduce concurrency.' AS RecommendedAction,
                'EXEC dbo.dbusp_PerformanceStats @DbName = ''' + @DbName + ''', @ShowSection = 11;' AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'This is not exact OS CPU percent; it is SQL wait-based pressure signal. Read it together with scheduler queues and top CPU queries.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM (
                SELECT signal_wait_time_ms, wait_time_ms, wait_type
                FROM sys.dm_os_wait_stats
                WHERE wait_type NOT IN ('SLEEP_TASK', 'WAITFOR', 'SOS_SCHEDULER_YIELD')
            ) WS
            HAVING @OnlyProblems = 0
                OR CAST(SUM(signal_wait_time_ms) * 100.0 / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,1)) >= 25;
        END


        -- ============================================================
        -- SECTION 25 : Table Partitioning Opportunities
        -- Large tables that could benefit from partitioning
        -- ============================================================
        IF @ShowSection IN (0, 25)
        BEGIN
            SELECT TOP 20
                'Partitioning Candidates'                                   AS [Section],
                OBJECT_SCHEMA_NAME(T.object_id)                            AS SchemaName,
                T.name                                                      AS TableName,
                SUM(CAST(P.rows AS BIGINT))                                 AS [RowCount],
                CAST(SUM(AU.total_pages) * 8 / 1024.0 / 1024.0 AS DECIMAL(10,2)) AS SizeGB,
                CASE
                    WHEN SUM(AU.total_pages) * 8 / 1024.0 / 1024.0 > 10
                        THEN 'Good candidate for partitioning (Range on DateAdded or ID)'
                    ELSE 'Monitor'
                END AS Recommendation
            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 AU ON P.partition_id = AU.container_id
            WHERE I.index_id < 2
            GROUP BY T.object_id, T.name, OBJECT_SCHEMA_NAME(T.object_id)
            HAVING SUM(AU.total_pages) * 8 / 1024.0 / 1024.0 > 1  -- > 1 GB
            ORDER BY SizeGB DESC;
        END


        -- ============================================================
        -- SECTION 26 : Query Recompilation & Query Hints Analysis
        -- Recompilation counts, forced plans, optimization hints
        -- ============================================================
        IF @ShowSection IN (0, 26)
        BEGIN
            SELECT TOP (@TopRows)
                'Query Recompilation Risk'                                  AS [Section],
                QS.plan_generation_num                                      AS RecompilationCount,
                QS.execution_count,
                CAST((QS.total_elapsed_time / NULLIF(QS.execution_count,0)) / 1000000.0 AS DECIMAL(18,4)) AS AvgElapsedSec,
                DB_NAME(T.dbid)                                             AS DBName,
                SUBSTRING(T.text, 1, 100)                                   AS QueryPreview,
                CASE
                    WHEN QS.plan_generation_num > 10
                        THEN 'HIGH RECOMPILE COUNT - Use WITH(RECOMPILE) or add OPTION(RECOMPILE)'
                    ELSE 'Normal'
                END AS Recommendation
            FROM sys.dm_exec_query_stats QS
            CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) T
            WHERE DB_NAME(T.dbid) = @DbName
              AND T.text IS NOT NULL
            ORDER BY RecompilationCount DESC;
        END


        -- ============================================================
        -- SECTION 27 : Database Recovery Model & Backup Status
        -- Shows backup history, LSN progression, recovery readiness
        -- ============================================================
        IF @ShowSection IN (0, 27)
        BEGIN
            SELECT
                'Recovery Model & Backups'                                  AS [Section],
                CASE
                    WHEN BS.LastFullBackup IS NULL THEN 'CRITICAL'
                    WHEN D.recovery_model_desc = 'FULL' AND BL.LastLogBackup IS NULL THEN 'CRITICAL'
                    WHEN DATEDIFF(HOUR, BS.LastFullBackup, GETDATE()) > 24 THEN 'WARNING'
                    ELSE 'INFO'
                END                                                         AS Severity,
                'Backup Compliance'                                         AS IssueType,
                DB_NAME(D.database_id)                                      AS DBName,
                D.recovery_model_desc                                       AS RecoveryModel,
                BS.LastFullBackup                                           AS LastFullBackup,
                BL.LastLogBackup                                            AS LastLogBackup,
                DATEDIFF(HOUR, BS.LastFullBackup, GETDATE())                AS HoursSinceFullBackup,
                DATEDIFF(DAY, BS.LastFullBackup, GETDATE())                 AS DaysSinceFullBackup,
                BS.first_lsn,
                BS.last_lsn,
                CASE
                    WHEN BS.LastFullBackup IS NULL THEN 'No full backup found in msdb history.'
                    WHEN D.recovery_model_desc = 'FULL' AND BL.LastLogBackup IS NULL THEN 'FULL recovery model without log backup history breaks point-in-time recovery.'
                    WHEN DATEDIFF(HOUR, BS.LastFullBackup, GETDATE()) > 24 THEN 'Full backup is older than expected policy threshold.'
                    ELSE 'Backup history looks present for the current policy baseline.'
                END                                                         AS LikelyCause,
                CASE
                    WHEN BS.LastFullBackup IS NULL THEN 'Take an immediate full backup after business approval.'
                    WHEN D.recovery_model_desc = 'FULL' AND BL.LastLogBackup IS NULL THEN 'Start regular transaction log backups immediately.'
                    WHEN DATEDIFF(HOUR, BS.LastFullBackup, GETDATE()) > 24 THEN 'Review SQL Agent/job schedule and backup retention failures.'
                    ELSE 'Continue monitoring backup jobs and test restores.'
                END                                                         AS RecommendedAction,
                CASE
                    WHEN D.recovery_model_desc = 'FULL' AND BL.LastLogBackup IS NULL THEN 'BACKUP LOG [' + D.name + '] TO DISK = ''<path>'' WITH INIT;'
                    ELSE 'BACKUP DATABASE [' + D.name + '] TO DISK = ''<path>'' WITH INIT;'
                END                                                         AS FixScript,
                CASE
                    WHEN @IncludeRunbook = 1 THEN 'Backup history in msdb is necessary but not sufficient. Also validate restore tests, job alerts, and backup file availability.'
                    ELSE NULL
                END                                                         AS DBARunbook
            FROM sys.databases D
            LEFT JOIN (
                SELECT database_name, MAX(backup_finish_date) AS LastFullBackup,
                       MAX(first_lsn) as first_lsn, MAX(last_lsn) as last_lsn
                FROM msdb.dbo.backupset
                WHERE type = 'D'  -- Full backup
                GROUP BY database_name
            ) BS ON D.name = BS.database_name
            LEFT JOIN (
                SELECT database_name, MAX(backup_finish_date) AS LastLogBackup
                FROM msdb.dbo.backupset
                WHERE type = 'L'
                GROUP BY database_name
            ) BL ON D.name = BL.database_name
            WHERE D.name = @DbName
              AND (
                    @OnlyProblems = 0
                    OR BS.LastFullBackup IS NULL
                    OR (D.recovery_model_desc = 'FULL' AND BL.LastLogBackup IS NULL)
                    OR DATEDIFF(HOUR, BS.LastFullBackup, GETDATE()) > 24
                  );
        END


        -- ============================================================
        -- SECTION 28 : Object Permissions & Security Audit
        -- Shows exposed permissions, excessive grants, security risks
        -- ============================================================
        IF @ShowSection IN (0, 28)
        BEGIN
            SELECT TOP 50
                'Permission Audit'                                          AS [Section],
                SCHEMA_NAME(O.schema_id)                                    AS SchemaName,
                O.name                                                      AS ObjectName,
                O.type_desc AS ObjectType,
                DP.name                                                     AS PrincipalName,
                DP.type_desc                                                AS PrincipalType,
                PR.permission_name                                          AS Permission,
                PR.state_desc                                               AS PermState,
                CASE
                    WHEN DP.name = 'public'
                        THEN 'SECURITY RISK - Excessive public permissions'
                    WHEN PR.permission_name IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE') AND DP.type = 'U'
                        THEN 'Review - User has DML'
                    ELSE 'Normal'
                END AS SecurityNote
            FROM sys.database_principals DP
            INNER JOIN sys.database_permissions PR ON DP.principal_id = PR.grantee_principal_id
            INNER JOIN sys.objects O ON PR.major_id = O.object_id
            WHERE (DP.name = 'public' OR DP.type = 'U')
              AND PR.state_desc = 'GRANT'
            ORDER BY DP.name, O.name;
        END


        -- ============================================================
        -- SECTION 29 : Server Configuration Issues & Best Practices
        -- Checks for bad server configs that impact DB performance
        -- ============================================================
        IF @ShowSection IN (0, 29)
        BEGIN
            SELECT *
            FROM
            (
                SELECT
                    'Config Issues'                                             AS [Section],
                    CASE
                        WHEN SCI.name = 'priority boost' AND SCI.value_in_use = 1 THEN 'CRITICAL'
                        WHEN SCI.name = 'max degree of parallelism' AND SCI.value_in_use > CASE WHEN @CpuCount > 8 THEN 8 ELSE @CpuCount END THEN 'WARNING'
                        WHEN SCI.name = 'cost threshold for parallelism' AND SCI.value_in_use < 5 THEN 'WARNING'
                        WHEN SCI.name = 'max server memory (MB)' AND SCI.value_in_use < 2048 THEN 'WARNING'
                        WHEN SCI.name = 'optimize for ad hoc workloads' AND SCI.value_in_use = 0 THEN 'INFO'
                        ELSE 'INFO'
                    END                                                         AS Severity,
                    'Server Configuration'                                      AS IssueType,
                    SCI.name                                                    AS ConfigName,
                    SCI.value_in_use                                            AS CurrentValue,
                    SCI.value                                                   AS ConfigValue,
                    SCI.description                                             AS Description,
                    CASE
                        WHEN SCI.name = 'priority boost' AND SCI.value_in_use = 1 THEN 'Priority boost is not recommended for SQL Server and can destabilize scheduling.'
                        WHEN SCI.name = 'max degree of parallelism' AND SCI.value_in_use > CASE WHEN @CpuCount > 8 THEN 8 ELSE @CpuCount END THEN 'MAXDOP may be too high for current CPU topology.'
                        WHEN SCI.name = 'cost threshold for parallelism' AND SCI.value_in_use < 5 THEN 'Very low cost threshold can push too many queries into parallel plans.'
                        WHEN SCI.name = 'max server memory (MB)' AND SCI.value_in_use < 2048 THEN 'SQL Server max memory is set very low for most workloads.'
                        WHEN SCI.name = 'optimize for ad hoc workloads' AND SCI.value_in_use = 0 THEN 'Ad hoc plan cache bloat risk exists on chatty ad hoc workloads.'
                        ELSE 'Configuration looks acceptable, but validate against workload baseline.'
                    END                                                         AS LikelyCause,
                    CASE
                        WHEN SCI.name = 'priority boost' AND SCI.value_in_use = 1 THEN 'Disable priority boost.'
                        WHEN SCI.name = 'max degree of parallelism' AND SCI.value_in_use > CASE WHEN @CpuCount > 8 THEN 8 ELSE @CpuCount END THEN 'Review and lower MAXDOP after workload testing.'
                        WHEN SCI.name = 'cost threshold for parallelism' AND SCI.value_in_use < 5 THEN 'Raise cost threshold after confirming plan behavior.'
                        WHEN SCI.name = 'max server memory (MB)' AND SCI.value_in_use < 2048 THEN 'Increase max server memory while preserving OS headroom.'
                        WHEN SCI.name = 'optimize for ad hoc workloads' AND SCI.value_in_use = 0 THEN 'Enable optimize for ad hoc workloads if plan cache churn is confirmed.'
                        ELSE 'No immediate server-wide change required.'
                    END                                                         AS RecommendedAction,
                    'EXEC sp_configure ''' + SCI.name + ''', <new_value>; RECONFIGURE;' AS FixScript,
                    CASE
                        WHEN @IncludeRunbook = 1 THEN 'Treat server-level changes carefully. Validate after-hours or in staging first, because these affect all databases on the instance.'
                        ELSE NULL
                    END                                                         AS DBARunbook
                FROM sys.configurations SCI
                WHERE SCI.name IN (
                    'max server memory (MB)',
                    'min server memory (MB)',
                    'max degree of parallelism',
                    'cost threshold for parallelism',
                    'optimize for ad hoc workloads',
                    'default full-text language',
                    'network packet size (B)',
                    'priority boost'
                )
            ) AS ConfigQ
            WHERE @OnlyProblems = 0 OR ConfigQ.Severity <> 'INFO'
            ORDER BY CASE ConfigQ.Severity WHEN 'CRITICAL' THEN 1 WHEN 'WARNING' THEN 2 ELSE 3 END, ConfigQ.ConfigName;
        END


        -- ============================================================
        -- SECTION 30 : Extended Events & Trace Configuration
        -- Shows tracing setup, extended events sessions, audit status
        -- ============================================================
        IF @ShowSection IN (0, 30)
        BEGIN
            -- Extended Events sessions (if available)
            IF OBJECT_ID('sys.dm_xe_sessions', 'V') IS NOT NULL
            BEGIN
                SELECT
                    'Extended Events'                                       AS [Section],
                    name                                                    AS SessionName,
                    'Active'                                                AS SessionStatus
                FROM sys.dm_xe_sessions;
            END
            ELSE
            BEGIN
                SELECT 'Extended Events not available on this SQL Server version' AS [Info];
            END

            -- Server Audits (if available)
            IF OBJECT_ID('sys.server_audits', 'V') IS NOT NULL
            BEGIN
                SELECT
                    'Server Audits'                                         AS [Section],
                    name,
                    CASE WHEN is_state_enabled = 1 THEN 'ENABLED' ELSE 'DISABLED' END
                FROM sys.server_audits;
            END
            ELSE
            BEGIN
                SELECT 'Extended Events / Audits not available on this SQL Server version' AS [Info];
            END

            PRINT N'TIP: Enable Extended Events or SQL Trace for troubleshooting:';
            PRINT N'  - xe_session_health: Tracks system issues';
            PRINT N'  - trace_events: Deadlock, long-running queries';
        END

    END TRY
    BEGIN CATCH
        SELECT
            ERROR_NUMBER()    AS ErrorNum,
            ERROR_SEVERITY()  AS Severity,
            ERROR_STATE()     AS [State],
            ERROR_PROCEDURE() AS [Procedure],
            ERROR_LINE()      AS [Line],
            ERROR_MESSAGE()   AS ErrorMessage;
    END CATCH
END
GO

-- ── Quick run examples ───────────────────────────────────────
-- Full enterprise diagnostic dashboard
-- EXEC dbo.dbusp_PerformanceStats;

-- Specific DB
-- EXEC dbo.dbusp_PerformanceStats @DbName = 'Vipul';

-- Specific diagnostic sections:
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 5;   -- Blocking queries
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 15;  -- Lock contention
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 16;  -- Stale statistics
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 17;  -- Database health
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 22;  -- IO analysis
-- EXEC dbo.dbusp_PerformanceStats @ShowSection = 29;  -- Config issues
-- EXEC dbo.dbusp_PerformanceStats @OnlyProblems = 1;  -- Hide healthy rows

-- Custom thresholds & full diagnostics
-- EXEC dbo.dbusp_PerformanceStats @DbName = 'PayrollDB', @ThresholdAvgSec = 5, 
--     @TopRows = 20, @StaleStatsThresholdDays = 14, @ShowSection = 0, @IncludeRunbook = 1;

What This Script Does (Very Powerful )

This is a complete DBA dashboard with 30 sections.

Query Performance

  • Top CPU queries

  • Slow queries

  • IO heavy queries

  • Execution plans

Index Health

  • Missing indexes

  • Fragmentation

  • Unused indexes

Memory & CPU

  • Memory pressure

  • CPU usage

  • Scheduler load

Concurrency

  • Blocking queries

  • Deadlocks

  • Lock contention

Database Health

  • Large tables

  • Log usage

  • Backup status

Security & Config

  • Permissions audit

  • Server configuration issues


How to Use Performance Script

Run full dashboard:

SQL
EXEC dbo.dbusp_PerformanceStats;

Check specific section:

SQL
-- Blocking queries
EXEC dbo.dbusp_PerformanceStats @ShowSection = 5;

-- Wait stats
EXEC dbo.dbusp_PerformanceStats @ShowSection = 11;

-- Index fragmentation
EXEC dbo.dbusp_PerformanceStats @ShowSection = 9;

-- IO analysis
EXEC dbo.dbusp_PerformanceStats @ShowSection = 22;

Real-Life DBA Workflow (Very Important )

Step 1: Check Blocking

SQL
EXEC dbusp_BlockingTree;

👉 If blocking found → fix immediately


Step 2: Check Wait Stats

SQL
EXEC dbo.dbusp_PerformanceStats @ShowSection = 11;

👉 Find root cause:

  • LCK → Blocking

  • PAGEIOLATCH → IO issue

  • CXPACKET → Parallelism


Step 3: Check Slow Queries

SQL
EXEC dbo.dbusp_PerformanceStats @ShowSection = 3;

Step 4: Check Index Issues

SQL
EXEC dbo.dbusp_PerformanceStats @ShowSection = 8;
EXEC dbo.dbusp_PerformanceStats @ShowSection = 9;

Step 5: Check Memory & CPU

SQL
EXEC dbo.dbusp_PerformanceStats @ShowSection = 13;
EXEC dbo.dbusp_PerformanceStats @ShowSection = 24;

Best Practices (Golden Rules)

  • Keep transactions short

  • Always use proper indexes

  • Avoid large updates in peak time

  • Monitor wait stats regularly

  • Never kill session without checking


Conclusion

For any SQL Server DBA:

👉 dbusp_BlockingTree = Quick troubleshooting tool
👉 dbusp_PerformanceStats = Complete health dashboard

If you use both together, you can:

  • Detect problems fast

  • Fix root cause

  • Improve system performance

0 likes

Rate this post

No rating

Tap a star to rate

0 comments

Latest comments

0 comments

No comments yet.

Keep building your data skillset

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