High CPU Troubleshooting

Step 1: Is it SQL Server?

The scheduler monitor ring buffer records SQL Server’s CPU use and total CPU use about once a minute for the last few hours:

SELECT TOP (30)
       DATEADD(ms, -1 * (si.ms_ticks - rb.[timestamp]), GETDATE()) AS event_time,
       rb.record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS sql_cpu_pct,
       100 - rb.record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS total_cpu_pct
FROM (SELECT [timestamp], CONVERT(xml, record) AS record
      FROM sys.dm_os_ring_buffers
      WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
        AND record LIKE N'%<SystemHealth>%') AS rb
CROSS JOIN sys.dm_os_sys_info si
ORDER BY rb.[timestamp] DESC;

If total CPU is high but SQL Server’s share is low, the cause is outside SQL Server: another process, antivirus scanning, a backup agent.

Step 2: What is using CPU right now

SELECT r.session_id, r.status, r.cpu_time, r.total_elapsed_time,
       r.wait_type, r.dop, DB_NAME(r.database_id) AS database_name,
       SUBSTRING(t.text, (r.statement_start_offset / 2) + 1,
           ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text)
             ELSE r.statement_end_offset END - r.statement_start_offset) / 2) + 1) AS running_statement
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id <> @@SPID
ORDER BY r.cpu_time DESC;

Step 3: Top CPU queries in the plan cache

SELECT TOP (10)
       qs.total_worker_time / 1000 AS total_cpu_ms,
       qs.execution_count,
       qs.total_worker_time / qs.execution_count / 1000 AS avg_cpu_ms,
       SUBSTRING(t.text, (qs.statement_start_offset / 2) + 1,
           ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(t.text)
             ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS statement_text,
       qp.query_plan
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) qp
ORDER BY qs.total_worker_time DESC;

Totals cover only the time since each plan was cached. Query Store (see Query Store) gives history across restarts and shows plan changes.

Step 4: Common causes

  • A plan change (parameter sniffing): the same query suddenly costs far more. Compare plans in Query Store, and force the previous good plan if needed.
  • Missing indexes or scans: large scans in the plan (see Identifying Missing Indexes).
  • Out-of-date statistics after a large data change.
  • Too much parallelism: high CXPACKET or CXCONSUMER waits with cost threshold for parallelism still at the default of 5.
  • High compilations: SQL Compilations/sec close to Batch Requests/sec in Performance Monitor, usually ad hoc SQL that isn’t parameterised.
  • A runaway job or report started at the time CPU went up.

Never, without approval

  • Clearing the plan cache as a fix (see When the SQL Server Cache Needs to Be Cleared).

From my SQL Server DBA handbook.