High CPU usage is a very common problem in SQL Servers. Any query with a bad query plan can steal CPU time and harm system response time.
Fortunately we can find queries with high CPU time using sys.dm_exec_query_stats DMV. This DMV, created in SQL Server 2008, keeps performance statistics for cached query plans, allowing us to find the queries and query plans that are most harming our system.
This DMV keeps some useful statistic information about the queries. All the values are stored in fields called “Total” and “Last”, this means we have the total value for all executions of the query and the value of the last execution of the query. For example, we have total_worker_time and last_worker_time.
Total_worker_time field can be used to find the most harmful query in the system, but this doesn’t mean this is the most CPU intensive query, this means this query is executed too many times. To find the most CPU intensive query we can use last_worker_time, but this query would not be a problem if it was executed only one or two times.
We can retrieve the query statement using a cross apply with sys.dm_exec_sql_text DMF, but this DMF will return the text for the entire batch, while each row in sys.dm_exec_query_stats keeps information about only one query. To solve this difference we have the fields statement_start_offset and statement_end_offset that points the exact position of the query inside the batch. We will need to do some string manipulation to retrieve the query text.
For last, but not least, we need to retrieve the query plan, so we can analyze the query plan and find the reason of the high CPU consumption. To do so, we need to do a cross apply with the sys.dm_exec_query_plan DMF.
To test this query, you can execute the following batch in a test environment to produce one query with high cpu usage:
-- Clear the query plan cache (don't execute this in production!)
-- CPU intensive query over adventureworks database
order by modifiedDate desc
Now, let’s check the most cpu intensive queries in our system:
-- using statement_start_offset and
-- statement_end_offset we get the query text
-- from inside the entire batch
WHEN -1 THEN DATALENGTH(qt.TEXT)
-- converting microseconds to seconds
FROM sys.dm_exec_query_stats qs
-- Retrieve the query text
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
-- Retrieve the query plan
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_worker_time DESC -- CPU time