Starting SQL 2008, we have had Query Hash column in sys.dm_exec_query_stats. It helps group common queries together, because when SQL Server is generating execution plan or caches queries it bases it on a HASH value it calculates. However, if you’re application is mostly using T-SQL (e.g. not Stored Procedures); then the HASH value be different every time. So the Query Stats DMV couldn’t provide proper grouping until 2008; script below takes advantage of that and gives you total stats for key columns.
-- SQLCanada, Mohit K. Gupta
-- Script from https://sqlcan.wordpress.com
-- Last Updated: May 12, 2012
------------------------------------------
WITH QSD (SQLStatement, PlanHandle, NumOfExecutions, Duration_ms, CPU_ms, Reads, Writes, QueryHash)
AS (SELECT SUBSTRING(st.text,
(qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN
DATALENGTH(st.text)
ELSE
qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1) AS SQLStatement
, qs.plan_handle AS PlanHandle
, execution_count AS NumOfExecutions
, total_elapsed_time/1000 AS Duration_ms
, total_worker_time/1000 AS CPU_ms
, total_logical_reads AS Reads
, total_logical_writes AS Writes
, query_hash AS QueryHash
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text (qs.sql_handle) st
WHERE query_hash != 0x0000000000000000)
SELECT QSD.QueryHash,
MIN(QSD.SQLStatement) AS SQLStatement,
MIN(QSD.PlanHandle) AS PlanHandle,
SUM(QSD.NumOfExecutions) AS TotalNumOfExecutions,
SUM(QSD.Duration_ms)/SUM(QSD.NumOfExecutions) AS AvgDuration_ms,
SUM(QSD.CPU_ms)/SUM(QSD.NumOfExecutions) AS AvgCPU_ms,
SUM(QSD.Reads)/SUM(QSD.NumOfExecutions) AS AvgReads,
SUM(QSD.Writes)/SUM(QSD.NumOfExecutions) AS AvgWrites
FROM QSD
GROUP BY QueryHash
Example Output:
If you notice I don’t have any kind of sort or Top 50 function. Reason is I don’t want to tax SQL Server; after this list is produced I can pull it into Microsoft Excel for analysis. On a 32-bit system you can get away with doing this right on server; however on 64-bit server with 128GB of memory, the data set returned can be large trying to do top 50 or other sorting can be costly.
Also please note I didn’t include sys.dm_exec_query_plan for similar reasons. After I find the statement I want to focus on I’ll use the value of PlanHandle to pull out the individual plans.