Detecting Query Plan Changes

The solution to detect the query execution plan changes. There were instances execution plan changed over the period of time for stored procedure and created a slowness in production. SQL server 2012 and prior versions not provides out of box capability to detect the query plan changes. This solution developed for that reason. The collected data useful for detailed analysis of query execution metrics apart from plan change detection. Its similar to query store feature available in SQL 2014 which is not available in SQL 2012 and prior versions.

Pre-requisites and supported versions

  • Supported Versions: SQL Server 2008, 2008 R2, 2012
  • Pre-requisites: SQL agent service should be up and running Note: Query store feature in SQL 2014 and 2016+ provides similar feature. If QueryStore feature not enabled for some reason, This script can be used for 2014 and 2016 as well.

List of objects

  • Table(s):
    • [querystats]      
    • [intervals]
    • [activitylog]
    • [Plans] 
    • [Queries]                      
    • [PlanChangeDetected]
  • DBADB.dbo.CollectQuerystats – This is a stored procedure does the data collection and plan change detection.

Supported functionalities

  • Collect all the queries running in the system and execution plans
  • The execution metrics collected for queries every time the stored procedure runs
  • Execution plans will be collected only if current queryplan not in the [Plans] table
  • Detect the plan change by checking [Query_Plan_Hash] and compare execution metrics to report as bad plan(When new execution metrics > 1.5 times of 1 month average)
  • The metrics considered for comparison are as below, if the new result is 1.5 times higher than previous for either of the below metrics, the new plan is deemed as bad.
    • avg_elapsed_time
    • avg_worker_time
    • avg_logical_reads
    • avg_physical_reads
  • [dbo]. [Plans] table stores the execution plan for every statement within procedure ran in system. PlanGuide can be used with this data to force a good plan as advanced tuning method

            Required parameters

  • @secdecisionfactor – This parameter defines number of times the execution metrics of new plan greater than previous plan. Default value is 1.5 times if not specified

Sample information

  • Query [dbo]. [PlanChangeDetected] table. It gives details about changed queryplan at different times.
  • Planguide can be used. If the requirement is to force a plan, the good plan can be retrieved from a dbo.[plans] table for a specific query and pass it to sp_create_plan_guide system stored procedure.

Space utilization to retain history data

  • Space requirement varies for each system and depends on no. of queries being executed and plan size etc.
  • Default retention is 30 days for collected query execution metrics. This clean up runs between 6pm – 5m. Not allowed to run in day time.

Implementation procedure & scripts

            1) Run the following code( it creates stored procedure )

/*****************************************************************************
-------------------------------------------------------------------------------
Script		:   PlanChangeDetection_CollectQuerystats
Author		:   Mahadevan N
Created		:   14/02/2020
Purpose		:   Capture potential plan changes
Parameter(s)	:   @secdecisionfactor float=1.5
Prerequisites	:   a) SQL agent service should be up and running. 
		   
Usage       	:   The stored proc dbo.[CollectQuerystats] does data collection. Schedule this procedure to run every 4 hours(recommended) using SQL agent job. 
		    DBA/Developer can review the potential plan changes by querying [PlanChangeDetected] table manually. This plan change information will be useful 
		    to identify and review queries which requires fine tuning as part of ongoing performance tuning exercise
		    No alert will be triggered.   
------------------------------------------------------------------------------

*****************************************************************************/


use DBADB /*Change to right DB here*/

go
/*Creation of Objects to store collection data*/
 IF OBJECT_ID( '[querystats]' ) IS NOT NULL DROP TABLE [querystats];  	
 IF OBJECT_ID( '[intervals]' ) IS NOT NULL DROP TABLE [intervals];
 IF OBJECT_ID( '[activitylog]' ) IS NOT NULL DROP TABLE [activitylog];
 IF OBJECT_ID( '[Plans]' ) IS NOT NULL DROP TABLE [Plans];  	
 IF OBJECT_ID( '[Queries]' ) IS NOT NULL DROP TABLE [Queries];  		
 IF OBJECT_ID( '[PlanChangeDetected]' ) IS NOT NULL DROP TABLE [PlanChangeDetected];  	


 CREATE TABLE [activitylog](
	[log_id] [int] IDENTITY(1,1) NOT NULL,
	[log_run_id] [int] NULL,
	[log_timestamp] [datetime] NULL,
	[log_message] [varchar](250) NULL,
 CONSTRAINT [pk_log_id] PRIMARY KEY CLUSTERED 
(
	[log_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE TABLE [intervals](
	[interval_id] [int] IDENTITY(1,1) NOT NULL,
	[interval_start] [datetime] NULL,
	[interval_end] [datetime] NULL,
 CONSTRAINT [pk_intervals] PRIMARY KEY CLUSTERED 
(
	[interval_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO 
CREATE TABLE [dbo].[querystats](
	[query_id] [int] NOT NULL,
	[interval_id] [int] references [intervals] (interval_id) NOT NULL,
	[creation_time] [datetime] NOT NULL,
	[last_execution_time] [datetime] NOT NULL,
	[execution_count] [bigint] NOT NULL,
	[total_elapsed_time] [bigint] NOT NULL,
	[last_elapsed_time] [bigint] NOT NULL,
	[min_elapsed_time] [bigint] NOT NULL,
	[max_elapsed_time] [bigint] NOT NULL,
	[avg_elapsed_time] [bigint] NOT NULL,
	[total_rows] [bigint] NOT NULL,
	[last_rows] [bigint] NOT NULL,
	[min_rows] [bigint] NOT NULL,
	[max_rows] [bigint] NOT NULL,
	[avg_rows] [bigint] NOT NULL,
	[total_worker_time] [bigint] NOT NULL,
	[last_worker_time] [bigint] NOT NULL,
	[min_worker_time] [bigint] NOT NULL,
	[max_worker_time] [bigint] NOT NULL,
	[avg_worker_time] [bigint] NOT NULL,
	[total_physical_reads] [bigint] NOT NULL,
	[last_physical_reads] [bigint] NOT NULL,
	[min_physical_reads] [bigint] NOT NULL,
	[max_physical_reads] [bigint] NOT NULL,
	[avg_physical_reads] [bigint] NOT NULL,
	[total_logical_reads] [bigint] NOT NULL,
	[last_logical_reads] [bigint] NOT NULL,
	[min_logical_reads] [bigint] NOT NULL,
	[max_logical_reads] [bigint] NOT NULL,
	[avg_logical_reads] [bigint] NOT NULL,
	[total_logical_writes] [bigint] NOT NULL,
	[last_logical_writes] [bigint] NOT NULL,
	[min_logical_writes] [bigint] NOT NULL,
	[max_logical_writes] [bigint] NOT NULL,
	[avg_logical_writes] [bigint] NOT NULL,
	[query_plan_hash] [varbinary](8) NOT NULL,
 CONSTRAINT [pk_querystats] PRIMARY KEY CLUSTERED 
(
	[query_id] ASC,
	[interval_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO


CREATE TABLE [Queries]
	(
	[queryId] bigint Identity(1,1) Primary Key,
	[query_hash] binary(8) NOT NULL,
	[statement_start_offset] int NOT NULL,
	[statement_end_offset] int NOT NULL,
	[statement_text]  nvarchar(max),
	[dbid] int   NULL,
	[objectID] int  NOT NULL
	)

	create index IX_Queries on [Queries] ( objectid,[query_hash],[statement_start_offset],[statement_end_offset])


CREATE TABLE [Plans]
(
	[PlanID] bigint IDENTITY(1,1) Primary key,
	[QueryID] bigint references [Queries]([queryId]) ,
	[Query_Plan_Hash] binary(8),
	query_plan_fingerprintid binary(16),
	[sql_plan] varchar(max),
	[plan_foundfirst] datetime default GETDATE() NOT NULL
)

create index [ix_plans] on [Plans] ([QueryID],[query_plan_fingerprintid])

GO
CREATE TABLE [dbo].[PlanChangeDetected](
	[queryId] [bigint] NOT NULL,
	[query_hash] [binary](8) NOT NULL,
	[statement_start_offset] [int] NOT NULL,
	[statement_end_offset] [int] NOT NULL,
	[statement_text] [nvarchar](max) NULL,
	[dbid] [int] NULL,
	[objectID] [int] NOT NULL,
	[PlanID] [bigint] NOT NULL,
	[Query_Plan_Hash] [binary](8) NULL,
	[sql_plan] [varchar](max) NULL,
	[plan_foundfirst] [datetime] NOT NULL,
	[interval_id] [int] NOT NULL,
	[avg_elapsed_time] [bigint] NOT NULL,
	[avg_worker_time] [bigint] NOT NULL,
	[avg_physical_reads] [bigint] NOT NULL,
	[avg_logical_reads] [bigint] NOT NULL,
	[avg_logical_writes] [bigint] NOT NULL,
	[dbname] [nvarchar](128) NULL,
	[object_Name] [nvarchar](128) NULL,
	[sql_handle] [varbinary](64)  NULL,
	[plan_handle] [varbinary](64) NULL,
	[timestamp] [datetime] NULL
)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
GO 

/*Stored procedure which does collection and plan change deduction*/

IF NOT EXISTS ( SELECT  TOP 1 1 FROM    sys.procedures WHERE   name = 'CollectQuerystats' )
BEGIN 
EXEC ('CREATE PROCEDURE CollectQuerystats as RETURN')
END 
GO

ALTER PROCEDURE CollectQuerystats
@debug int =0,
@secdecisionfactor float=1.5
as 

DECLARE @log_logrunid int,
@log_queries int;


	SELECT [sql_handle],
       [statement_start_offset],
       [statement_end_offset],
       [plan_generation_num],
       [plan_handle],
       [creation_time],
       [last_execution_time],
       [execution_count],
       [total_worker_time],
       [last_worker_time],
       [min_worker_time],
       [max_worker_time],
       [total_physical_reads],
       [last_physical_reads],
       [min_physical_reads],
       [max_physical_reads],
       [total_logical_writes],
       [last_logical_writes],
       [min_logical_writes],
       [max_logical_writes],
       [total_logical_reads],
       [last_logical_reads],
       [min_logical_reads],
       [max_logical_reads],
       --[total_clr_time],
       --[last_clr_time],
       --[min_clr_time],
       --[max_clr_time],
       [total_elapsed_time],
       [last_elapsed_time],
       [min_elapsed_time],
       [max_elapsed_time],[query_hash],[query_plan_hash],[total_rows],[last_rows],[min_rows],[max_rows],
	   --[statement_sql_handle],[statement_context_id],
	   --[total_dop],[last_dop],[min_dop],[max_dop],
	   
	   --[total_grant_kb],[last_grant_kb],[min_grant_kb],[max_grant_kb],[total_used_grant_kb],[last_used_grant_kb],[min_used_grant_kb],[max_used_grant_kb],[total_ideal_grant_kb],[last_ideal_grant_kb],[min_ideal_grant_kb],[max_ideal_grant_kb],[total_reserved_threads],[last_reserved_threads],[min_reserved_threads],[max_reserved_threads],[total_used_threads],[last_used_threads],[min_used_threads],[max_used_threads],
	   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 statement_text,
		 b.dbid,
		 ISNULL(b.objectid,0) objectID,
		 [query_hash]+[query_plan_hash] query_plan_fingerprintid,
		 b.query_plan
		    INTO #querystats
		 FROM sys.dm_exec_query_stats qs cross apply 
				  sys.dm_exec_text_query_plan  (plan_handle,statement_start_offset,statement_end_offset) b
				  cross apply
				  sys.dm_exec_sql_text(sql_handle) st
				  where b.dbid >4 --and b.dbid=db_id()

		DELETE FROM #querystats 
		where execution_count=1 and objectID=0

		DELETE FROM #querystats 
		where  objectID=object_ID('CollectQuerystats')

   SET @log_logrunid = (   SELECT ISNULL( MAX( [AL].[log_run_id] ), 0 ) + 1
                                                FROM   [activitylog] AS [AL] );
INSERT INTO [activitylog] ( [log_run_id],
                                                                       [log_timestamp],
                                                                       [log_message] )
                                    VALUES ( @log_logrunid, GETDATE(), 'Querystats capture script started...' );
IF @debug=1
BEGIN
SELECT 'queries'
SELECT DISTINCT [query_hash],[statement_start_offset],[statement_end_offset],[statement_text],[dbid], ISNULL([objectID],0) [objectID]
from #querystats a
WHERE NOT EXISTS  (SELECT [query_hash] from [Queries]  b WHERE  
a.dbid=b.dbid 
and a.objectid=b.objectID 
and a.[query_hash]=b.[query_hash]
and a.[statement_start_offset]=b.[statement_start_offset]
and a.[statement_end_offset]=b.[statement_end_offset]
)
END

INSERT INTO [Queries]
SELECT DISTINCT [query_hash],[statement_start_offset],[statement_end_offset],[statement_text],[dbid], ISNULL([objectID],0) [objectID]
from #querystats a
WHERE NOT EXISTS  (SELECT [query_hash] from [Queries]  b WHERE  
a.dbid=b.dbid 
and a.objectid=b.objectID 
and a.[query_hash]=b.[query_hash]
and a.[statement_start_offset]=b.[statement_start_offset]
and a.[statement_end_offset]=b.[statement_end_offset]
)


   SET @log_queries = @@RowCount;

                         
                    
                                    INSERT INTO [activitylog] ( [log_run_id],
                                                                       [log_timestamp],
                                                                       [log_message] )
                                    VALUES ( @log_logrunid, GETDATE(), 'Querystats captured ' + CONVERT( varchar, @log_queries ) + ' new queries(s)...' );
                             

IF @debug=1
BEGIN
SELECT 'plans'
select distinct b.queryId,a.[Query_Plan_Hash],a.Query_Plan_FingerprintID,a.query_plan from #querystats a join queries b
on a.dbid=b.dbid 
and a.objectid=b.objectID 
and a.[query_hash]=b.[query_hash]
and a.[statement_start_offset]=b.[statement_start_offset]
and a.[statement_end_offset]=b.[statement_end_offset]
WHERE NOT EXISTS  (SELECT [query_plan_fingerprintid] from [Plans]  c 
WHERE  a.[query_plan_fingerprintid]=c.[query_plan_fingerprintid]
and b.queryId=c.QueryID
)
END


INSERT INTO Plans (QueryID,[Query_Plan_Hash],Query_Plan_FingerprintID,sql_plan)
select distinct b.queryId,a.[Query_Plan_Hash],a.Query_Plan_FingerprintID,a.query_plan from #querystats a join queries b
on a.dbid=b.dbid 
and a.objectid=b.objectID 
and a.[query_hash]=b.[query_hash]
and a.[statement_start_offset]=b.[statement_start_offset]
and a.[statement_end_offset]=b.[statement_end_offset]
WHERE NOT EXISTS  (SELECT [query_plan_fingerprintid] from [Plans]  c 
WHERE  a.[query_plan_fingerprintid]=c.[query_plan_fingerprintid]
and b.queryId=c.QueryID
)

 SET @log_queries = @@RowCount;

                         
                    
                                    INSERT INTO [activitylog] ( [log_run_id],
                                                                       [log_timestamp],
                                                                       [log_message] )
                                    VALUES ( @log_logrunid, GETDATE(), 'QueryStats captured ' + CONVERT( varchar, @log_queries ) + ' new plans(s)...' );
                             


   INSERT INTO [intervals] ( [interval_start] ) VALUES ( GETDATE());
   



                            DECLARE @Interval_ID int;
                            SET @Interval_ID = IDENT_CURRENT( '[intervals]' );
						

	INSERT INTO [querystats] ( [query_id],
                                                                      [interval_id],
                                                                      [creation_time],
                                                                      [last_execution_time],
                                                                      [execution_count],
                                                                      [total_elapsed_time],
                                                                      [last_elapsed_time],
                                                                      [min_elapsed_time],
                                                                      [max_elapsed_time],
                                                                      [avg_elapsed_time],
                                                                      [total_rows],
                                                                      [last_rows],
                                                                      [min_rows],
                                                                      [max_rows],
                                                                      [avg_rows],
                                                                      [total_worker_time],
                                                                      [last_worker_time],
                                                                      [min_worker_time],
                                                                      [max_worker_time],
                                                                      [avg_worker_time],
                                                                      [total_physical_reads],
                                                                      [last_physical_reads],
                                                                      [min_physical_reads],
                                                                      [max_physical_reads],
                                                                      [avg_physical_reads],
                                                                      [total_logical_reads],
                                                                      [last_logical_reads],
                                                                      [min_logical_reads],
                                                                      [max_logical_reads],
                                                                      [avg_logical_reads],
                                                                      [total_logical_writes],
                                                                      [last_logical_writes],
                                                                      [min_logical_writes],
                                                                      [max_logical_writes],
                                                                      [avg_logical_writes],
																	  [query_plan_hash] )
                                        SELECT [oqs_q].[queryid],
                                               @Interval_ID,
                                               MIN([qs].[creation_time]),
                                               MAX([qs].[last_execution_time]),
                                               SUM([qs].[execution_count]),
                                               SUM([qs].[total_elapsed_time]),
                                               SUM([qs].[last_elapsed_time]),
                                               MIN([qs].[min_elapsed_time]),
                                               MAX([qs].[max_elapsed_time]),
                                               SUM(ISNULL(( qs.[total_elapsed_time] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [Avg. Time],
                                               SUM([qs].[total_rows]),
                                               SUM([qs].[last_rows]),
                                               MIN([qs].[min_rows]),
                                               MAX([qs].[max_rows]),
											   SUM(ISNULL(( qs.[total_rows] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [avg_rows],
                                               SUM([qs].[total_worker_time]),
                                               SUM([qs].[last_worker_time]),
                                               MIN([qs].[min_worker_time]),
                                               MAX([qs].[max_worker_time]),
											   SUM(ISNULL(( qs.[total_worker_time] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [Avg. Worker Time],
                                               SUM([qs].[total_physical_reads]),
                                               SUM([qs].[last_physical_reads]),
                                               MIN([qs].[min_physical_reads]),
                                               MAX([qs].[max_physical_reads]),
											   SUM(ISNULL(( qs.[total_physical_reads] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [Avg. Phys reads],
                                               SUM([qs].[total_logical_reads]),
                                               SUM([qs].[last_logical_reads]),
                                               MIN([qs].[min_logical_reads]),
                                               MAX([qs].[max_logical_reads]),
											   SUM(ISNULL(( qs.[total_logical_reads] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [Avg. Log reads],
                                               SUM([qs].[total_logical_writes]),
                                               SUM([qs].[last_logical_writes]),
                                               MIN([qs].[min_logical_writes]),
                                               MAX([qs].[max_logical_writes]),
											   SUM(ISNULL(( qs.[total_logical_writes] / NULLIF(( qs.[execution_count] ), 0)), 0 ))     AS [Avg. Log writes],
											   max(qs.[query_plan_hash]) AS		[query_plan_hash]
                                        FROM   [queries]                AS [oqs_q]
                                               INNER JOIN [#querystats] AS [qs] ON 
											   (   [oqs_q].[query_hash] = [qs].[query_hash]
                                                                                             
													AND [oqs_q].[statement_start_offset] = [qs].[statement_start_offset]
                                                    AND [oqs_q].[statement_end_offset] = [qs].[statement_end_offset]
													AND oqs_q.dbid = qs.dbid
													AND oqs_q.objectID =qs.objectID
                                                    )
                                        GROUP BY [oqs_q].[queryid];



										       
                            UPDATE [intervals]
                            SET    [interval_end] = GETDATE()
                            WHERE  [interval_id] = ( SELECT MAX( [interval_id] ) - 1 FROM [intervals] );


IF OBJECT_ID( 'tempdb..#OQS_Runtime_Stats' ) IS NOT NULL DROP TABLE #OQS_Runtime_Stats;
  WITH [CTE_Update_Runtime_Stats] ( [query_id], [interval_id],[creation_time], [execution_count], [total_elapsed_time], [total_rows], [total_worker_time], [total_physical_reads], [total_logical_reads], [total_logical_writes] )
                            AS ( SELECT [QRS].[query_id],
                                        [QRS].[interval_id],
										[QRS].[creation_time],
                                        [QRS].[execution_count],	
                                        [QRS].[total_elapsed_time],
                                        [QRS].[total_rows],
                                        [QRS].[total_worker_time],
                                        [QRS].[total_physical_reads],
                                        [QRS].[total_logical_reads],
                                        [QRS].[total_logical_writes]
                                 FROM   [querystats] AS [QRS]
                                 WHERE  [QRS].[interval_id] = ( SELECT MAX( [interval_id] ) - 1 FROM [intervals] ))
                            SELECT [cte].[query_id],
                                   [qrs].[interval_id],
								   [QRS].[creation_time],
                                   ( [qrs].[execution_count] - [cte].[execution_count] )                                                                                            AS [Delta Exec Count],
                                   ( [qrs].[total_elapsed_time] - [cte].[total_elapsed_time] )                                                                                      AS [Delta Time],
                                   ISNULL((( [qrs].[total_elapsed_time] - [cte].[total_elapsed_time] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 )     AS [Avg. Time],
                                   ( [qrs].[total_rows] - [cte].[total_rows] )                                                                                                      AS [Delta Total Rows],
                                   ISNULL((( [qrs].[total_rows] - [cte].[total_rows] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 )                     AS [Avg. Rows],
                                   ( [qrs].[total_worker_time] - [cte].[total_worker_time] )                                                                                        AS [Delta Total Worker Time],
                                   ISNULL((( [qrs].[total_worker_time] - [cte].[total_worker_time] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 )       AS [Avg. Worker Time],
                                   ( [qrs].[total_physical_reads] - [cte].[total_physical_reads] )                                                                                  AS [Delta Total Phys Reads],
                                   ISNULL((( [qrs].[total_physical_reads] - [cte].[total_physical_reads] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 ) AS [Avg. Phys reads],
                                   ( [qrs].[total_logical_reads] - [cte].[total_logical_reads] )                                                                                    AS [Delta Total Log Reads],
                                   ISNULL((( [qrs].[total_logical_reads] - [cte].[total_logical_reads] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 )   AS [Avg. Log reads],
                                   ( [qrs].[total_logical_writes] - [cte].[total_logical_writes] )                                                                                  AS [Delta Total Log Writes],
                                   ISNULL((( [qrs].[total_logical_writes] - [cte].[total_logical_writes] ) / NULLIF(( [qrs].[execution_count] - [cte].[execution_count] ), 0)), 0 ) AS [Avg. Log writes]
                            INTO   [#OQS_Runtime_Stats]
                            FROM   [CTE_Update_Runtime_Stats]             AS [cte]
                                   INNER JOIN [querystats] AS [qrs] ON [cte].[query_id] = [qrs].[query_id]
								   and [QRS].[creation_time]=[cte].creation_time
                            WHERE  [qrs].[interval_id] = ( SELECT MAX( [interval_id] ) FROM [intervals] );

							  UPDATE [qrs]
                            SET    [qrs].[execution_count] = [tqrs].[Delta Exec Count],
                                   [qrs].[total_elapsed_time] = [tqrs].[Delta Time],
                                   [qrs].[avg_elapsed_time] = [tqrs].[Avg. Time],
                                   [qrs].[total_rows] = [tqrs].[Delta Total Rows],
                                   [qrs].[avg_rows] = [tqrs].[Avg. Rows],
                                   [qrs].[total_worker_time] = [tqrs].[Delta Total Worker Time],
                                   [qrs].[avg_worker_time] = [tqrs].[Avg. Worker Time],
                                   [qrs].[total_physical_reads] = [tqrs].[Delta Total Phys Reads],
                                   [qrs].[avg_physical_reads] = [tqrs].[Avg. Phys reads],
                                   [qrs].[total_logical_reads] = [tqrs].[Delta Total Log Reads],
                                   [qrs].[avg_logical_reads] = [tqrs].[Avg. Log reads],
                                   [qrs].[total_logical_writes] = [tqrs].[Delta Total Log Writes],
                                   [qrs].[avg_logical_writes] = [tqrs].[Avg. Log writes]
                            FROM   [querystats]     AS [qrs]
                                   INNER JOIN [#OQS_Runtime_Stats] AS [tqrs] ON (   [qrs].[interval_id] = [tqrs].[interval_id]
                                                                                    AND [qrs].[query_id] = [tqrs].[query_id] );

;WITH Compare_CTE AS			(

										SELECT [QRS].[query_id],
                                        [QRS].[interval_id],								
                                        [QRS].[avg_elapsed_time],
                                        [QRS].[avg_worker_time],
                                        [QRS].[avg_physical_reads],
                                        [QRS].[avg_logical_reads],
                                        [QRS].[avg_logical_writes],
										[qrs].query_plan_hash
                                 FROM   [querystats] AS [QRS]
                                 WHERE  [QRS].[interval_id] = ( SELECT MAX( [interval_id] ) - 1 FROM [intervals] )
								 --and qrs.execution_count >2
								 )

					SELECT 
					cte.query_id,cte.interval_id,cte.avg_elapsed_time,
					cte.avg_worker_time,cte.avg_physical_reads,
					cte.avg_logical_reads,cte.avg_logical_writes,cte.query_plan_hash,
					qrs.query_id cquery_id,qrs.interval_id cinterval_id,
					qrs.avg_elapsed_time cavg_elapsed_time,
					qrs.avg_worker_time cavg_worker_time,
					qrs.avg_physical_reads cavg_physical_reads,
					qrs.avg_logical_reads cavg_logical_reads,
					qrs.avg_logical_writes cavg_logical_writes,
					qrs.query_plan_hash cquery_plan_hash
					
					into #PlanChanges
					FROM Compare_CTE             AS [cte]
					INNER JOIN [querystats] AS [qrs] ON [cte].[query_id] = [qrs].[query_id]
                    WHERE  [qrs].[interval_id] = ( SELECT MAX( [interval_id] ) FROM [intervals] )
					--and qrs.execution_count >2
					and ( [QRS].[avg_elapsed_time] > @secdecisionfactor*cte.avg_elapsed_time or qrs.avg_worker_time >
					@secdecisionfactor*cte.[avg_worker_time] or qrs.avg_logical_reads >@secdecisionfactor*cte.avg_logical_reads or
					 qrs.avg_physical_reads > @secdecisionfactor*cte.avg_physical_reads)
					and qrs.query_plan_hash <>cte.query_plan_hash 
							
			
			DECLARE @Timest datetime = GETDATE()

			SELECT 
				q.queryId,
				q.query_hash,
				q.statement_start_offset,
				q.statement_end_offset,
				q.statement_text,
				q.dbid,
				q.objectID,
				p.PlanID,
				p.Query_Plan_Hash,
				p.sql_plan,
				p.plan_foundfirst,
				s.interval_id,s.avg_elapsed_time,
				s.avg_worker_time,s.avg_physical_reads,
				s.avg_logical_reads,s.avg_logical_writes
			 INTO #PlanChangeDetected from queries q
			JOIN plans p 
			on q.queryId=p.QueryID
--			drop table #PlanChangeDetected
			JOIN 
			
			(			

				SELECT 		cte.query_id,cte.interval_id,cte.avg_elapsed_time,
					cte.avg_worker_time,cte.avg_physical_reads,
					cte.avg_logical_reads,cte.avg_logical_writes,cte.query_plan_hash from #PlanChanges cte

				UNION 
						SELECT 		 cquery_id, cinterval_id,
					 cavg_elapsed_time,
					 cavg_worker_time,
					 cavg_physical_reads,
					 cavg_logical_reads,
					 cavg_logical_writes,
					 cquery_plan_hash
					 from #PlanChanges qrs
			 ) s
			 on q.queryId=s.query_id
			 and p.Query_Plan_Hash=s.query_plan_hash



			 INSERT INTO PlanChangeDetected
			 select pd.*,db_name(pd.dbid) dbname, OBJECT_NAME(pd.objectID,pd.dbid) [object_Name],
			 qs.sql_handle,qs.plan_handle,
			 @Timest as [timestamp]
			 FROM #PlanChangeDetected PD 
						LEFT JOIN sys.dm_exec_query_stats qs ON
						 PD.query_hash=qs.query_hash 
						 and pd.Query_Plan_Hash=qs.query_plan_hash
						 and pd.statement_start_offset=qs.statement_start_offset
						 and pd.statement_end_offset=qs.statement_end_offset

--IF EXISTS ( SELECT top 1 1 from #PlanChangeDetected)
--BEGIN
--SELECT *, 
--case when charindex('<ParameterList>', sql_plan) >0 then 
--CAST (
		  
--		  substring(sql_plan,
		  
--		  (charindex('<ParameterList>', sql_plan) + len('<ParameterList>')),
		  
--		  (
		  

--		  (charindex('</ParameterList>', sql_plan))-(charindex('<ParameterList>', sql_plan) + len('<ParameterList>'))
		  
--		  )
--		  ) 
		  
--		  as XML)  end as CompliledParams
--		  ,CAST((SELECT statement_text FOR XML PATH('')) as XML) StatementTextXml

--FROM PlanChangeDetected WHERE [timestamp]=@Timest

--DROP TABLE #PlanChangeDetected
--END

            It does following action.

  • Creation of stored procedure and permanent tables listed in objects section

Script designed to run from [DBADB] database. This can be changed to other DBA specific database if exists in the environment.

2) DBA to create a SQL agent job to execute DBADB.dbo.CollectQuerystats stored procedure at different intervals. This monitoring is considerably heavy weight. The job frequency varies for each system and depends on load on the system. Recommended to run every 4 hours. If the system don’t allow to run monitoring during business hours, run at off hours specifically few hours’ after update statistics job completes.

Published by mahadevantech

I am Mahadevan a Microsoft certified Database professional. I work as a Senior SQL Server Database Administrator based in Singapore. MCSE: Data Management and Analytics — Certified 2020

Join the Conversation

  1. Unknown's avatar
  2. Unknown's avatar
  3. Unknown's avatar
  4. skkavin63's avatar
  5. Unknown's avatar
  6. Unknown's avatar

7 Comments

Leave a comment

Leave a reply to Prasanna Narayanan Cancel reply

Design a site like this with WordPress.com
Get started