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.