@@ -145,6 +145,13 @@ CREATE TABLE ##bou_BlitzCacheProcs (
145145 function_count INT ,
146146 clr_function_count INT ,
147147 is_table_variable BIT ,
148+ no_stats_warning BIT ,
149+ relop_warnings BIT ,
150+ is_table_scan BIT ,
151+ backwards_scan BIT ,
152+ forced_index BIT ,
153+ forced_seek BIT ,
154+ forced_scan BIT ,
148155 SetOptions VARCHAR (MAX ),
149156 Warnings VARCHAR (MAX )
150157 );
@@ -732,12 +739,20 @@ BEGIN
732739 function_count INT ,
733740 clr_function_count INT ,
734741 is_table_variable BIT ,
742+ no_stats_warning BIT ,
743+ relop_warnings BIT ,
744+ is_table_scan BIT ,
745+ backwards_scan BIT ,
746+ forced_index BIT ,
747+ forced_seek BIT ,
748+ forced_scan BIT ,
735749 SetOptions VARCHAR (MAX ),
736750 Warnings VARCHAR (MAX )
737751 );
738752END
739753
740754DECLARE @DurationFilter_i INT ,
755+ @MinMemoryPerQuery INT ,
741756 @msg NVARCHAR (4000 ) ;
742757
743758RAISERROR (N ' Setting up temporary tables for sp_BlitzCache' ,0 ,1 ) WITH NOWAIT ;
@@ -756,6 +771,7 @@ BEGIN
756771 RETURN ;
757772END
758773
774+ SELECT @MinMemoryPerQuery = CONVERT (INT , c .value ) FROM sys .configurations AS c WHERE c .name = ' min memory per query (KB)' ;
759775
760776SET @SortOrder = LOWER (@SortOrder);
761777SET @SortOrder = REPLACE (REPLACE (@SortOrder, ' average' , ' avg' ), ' .' , ' ' );
@@ -1784,15 +1800,19 @@ UPDATE p
17841800SET busy_loops = CASE WHEN (x .estimated_executions / 100 .0 ) > x .estimated_rows THEN 1 END ,
17851801 tvf_join = CASE WHEN x .tvf_join = 1 THEN 1 END ,
17861802 warning_no_join_predicate = CASE WHEN x .no_join_warning = 1 THEN 1 END ,
1787- p .is_table_variable = CASE WHEN x .is_table_variable = 1 THEN 1 END
1803+ is_table_variable = CASE WHEN x .is_table_variable = 1 THEN 1 END ,
1804+ no_stats_warning = CASE WHEN x .no_stats_warning = 1 THEN 1 END ,
1805+ relop_warnings = CASE WHEN x .relop_warnings = 1 THEN 1 END
17881806FROM ##bou_BlitzCacheProcs p
17891807 JOIN (
17901808 SELECT qs .SqlHandle ,
17911809 relop .value (' sum(/p:RelOp/@EstimateRows)' , ' float' ) AS estimated_rows ,
17921810 relop .value (' sum(/p:RelOp/@EstimateRewinds)' , ' float' ) + relop .value (' sum(/p:RelOp/@EstimateRebinds)' , ' float' ) + 1 .0 AS estimated_executions ,
17931811 relop .exist (' /p:RelOp[contains(@LogicalOp, "Join")]/*/p:RelOp[(@LogicalOp[.="Table-valued function"])]' ) AS tvf_join,
17941812 relop .exist (' /p:RelOp/p:Warnings[(@NoJoinPredicate[.="1"])]' ) AS no_join_warning,
1795- relop .exist (' /p:RelOp//*[local-name() = "Object"]/@Table[contains(., "@")]' ) AS is_table_variable
1813+ relop .exist (' /p:RelOp//*[local-name() = "Object"]/@Table[contains(., "@")]' ) AS is_table_variable,
1814+ relop .exist (' /p:RelOp/p:Warnings/p:ColumnsWithNoStatistics' ) AS no_stats_warning ,
1815+ relop .exist (' /p:RelOp/p:Warnings' ) AS relop_warnings
17961816 FROM #relop qs
17971817 ) AS x ON p .SqlHandle = x .SqlHandle
17981818OPTION (RECOMPILE );
@@ -1819,7 +1839,7 @@ SET key_lookup_cost = x.key_lookup_cost
18191839FROM (
18201840SELECT
18211841 qs .SqlHandle ,
1822- relop .value (' /p:RelOp[1] /@EstimatedTotalSubtreeCost' , ' float' ) AS key_lookup_cost
1842+ relop .value (' sum( /p:RelOp/@EstimatedTotalSubtreeCost) ' , ' float' ) AS key_lookup_cost
18231843FROM #relop qs
18241844WHERE [relop].exist(' /p:RelOp/p:IndexScan[(@Lookup[.="1"])]' ) = 1
18251845) AS x
@@ -1833,7 +1853,7 @@ SET remote_query_cost = x.remote_query_cost
18331853FROM (
18341854SELECT
18351855 qs .SqlHandle ,
1836- relop .value (' /p:RelOp[1] /@EstimatedTotalSubtreeCost' , ' float' ) AS remote_query_cost
1856+ relop .value (' sum( /p:RelOp/@EstimatedTotalSubtreeCost) ' , ' float' ) AS remote_query_cost
18371857FROM #relop qs
18381858WHERE [relop].exist(' /p:RelOp[(@PhysicalOp[.="Remote Query"])]' ) = 1
18391859) AS x
@@ -1851,6 +1871,38 @@ ON b.QueryHash = qs.QueryHash
18511871CROSS APPLY qs .statement .nodes (' /p:StmtCursor' ) AS n1(fn)
18521872OPTION (RECOMPILE ) ;
18531873
1874+ ;WITH XMLNAMESPACES(' http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p)
1875+ UPDATE b
1876+ SET
1877+ b .is_table_scan = x .is_table_scan ,
1878+ b .backwards_scan = x .backwards_scan ,
1879+ b .forced_index = x .forced_index ,
1880+ b .forced_seek = x .forced_seek ,
1881+ b .forced_scan = x .forced_scan
1882+ FROM ##bou_BlitzCacheProcs b
1883+ JOIN (
1884+ SELECT
1885+ qs .SqlHandle ,
1886+ 0 AS is_table_scan,
1887+ q .n .exist(' @ScanDirection[.="BACKWARD"]' ) AS backwards_scan,
1888+ q .n .value (' @ForcedIndex' , ' bit' ) AS forced_index,
1889+ q .n .value (' @ForceSeek' , ' bit' ) AS forced_seek,
1890+ q .n .value (' @ForceScan' , ' bit' ) AS forced_scan
1891+ FROM #relop qs
1892+ CROSS APPLY qs .relop .nodes (' //p:IndexScan' ) AS q(n)
1893+ UNION ALL
1894+ SELECT
1895+ qs .SqlHandle ,
1896+ 1 AS is_table_scan,
1897+ q .n .exist(' @ScanDirection[.="BACKWARD"]' ) AS backwards_scan,
1898+ q .n .value (' @ForcedIndex' , ' bit' ) AS forced_index,
1899+ q .n .value (' @ForceSeek' , ' bit' ) AS forced_seek,
1900+ q .n .value (' @ForceScan' , ' bit' ) AS forced_scan
1901+ FROM #relop qs
1902+ CROSS APPLY qs .relop .nodes (' //p:TableScan' ) AS q(n)
1903+ ) AS x ON b .SqlHandle = x .SqlHandle
1904+ OPTION (RECOMPILE ) ;
1905+
18541906
18551907IF @v >= 12
18561908BEGIN
@@ -2051,7 +2103,7 @@ SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold
20512103 is_key_lookup_expensive = CASE WHEN QueryPlanCost > (@ctp / 2 ) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END ,
20522104 is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END ,
20532105 is_forced_serial = CASE WHEN is_forced_serial = 1 AND QueryPlanCost > (@ctp / 2 ) THEN 1 END ,
2054- is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > 0 THEN 1 END
2106+ is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END
20552107OPTION (RECOMPILE ) ;
20562108
20572109
@@ -2126,7 +2178,14 @@ SET Warnings = SUBSTRING(
21262178 CASE WHEN function_count > 0 THEN ' , Calls ' + CONVERT (VARCHAR (10 ), function_count) + ' function(s)' ELSE ' ' END +
21272179 CASE WHEN clr_function_count > 0 THEN ' , Calls ' + CONVERT (VARCHAR (10 ), clr_function_count) + ' CLR function(s)' ELSE ' ' END +
21282180 CASE WHEN PlanCreationTimeHours <= 4 THEN ' , Plan created last 4hrs' ELSE ' ' END +
2129- CASE WHEN is_table_variable = 1 THEN ' , Table Variables' ELSE ' ' END
2181+ CASE WHEN is_table_variable = 1 THEN ' , Table Variables' ELSE ' ' END +
2182+ CASE WHEN no_stats_warning = 1 THEN ' , Columns With No Statistics' ELSE ' ' END +
2183+ CASE WHEN relop_warnings = 1 THEN ' , Operator Warnings' ELSE ' ' END +
2184+ CASE WHEN is_table_scan = 1 THEN ' , Table Scans' ELSE ' ' END +
2185+ CASE WHEN backwards_scan = 1 THEN ' , Backwards Scans' ELSE ' ' END +
2186+ CASE WHEN forced_index = 1 THEN ' , Forced Indexes' ELSE ' ' END +
2187+ CASE WHEN forced_seek = 1 THEN ' , Forced Seeks' ELSE ' ' END +
2188+ CASE WHEN forced_scan = 1 THEN ' , Forced Scans' ELSE ' ' END
21302189 , 2 , 200000 )
21312190 OPTION (RECOMPILE ) ;
21322191
@@ -2135,12 +2194,6 @@ SET Warnings = SUBSTRING(
21352194
21362195
21372196
2138-
2139-
2140-
2141-
2142-
2143-
21442197Results:
21452198IF @OutputDatabaseName IS NOT NULL
21462199 AND @OutputSchemaName IS NOT NULL
@@ -2440,7 +2493,13 @@ BEGIN
24402493 CASE WHEN function_count > 0 IS NOT NULL THEN '' , 31'' ELSE '' '' END +
24412494 CASE WHEN clr_function_count > 0 THEN '' , 32'' ELSE '' '' END +
24422495 CASE WHEN PlanCreationTimeHours <= 4 THEN '' , 33'' ELSE '' '' END +
2443- CASE WHEN is_table_variable = 1 then '' , 34'' ELSE '' '' END
2496+ CASE WHEN is_table_variable = 1 THEN '' , 34'' ELSE '' '' END +
2497+ CASE WHEN no_stats_warning = 1 THEN '' , 35'' ELSE '' '' END +
2498+ CASE WHEN relop_warnings = 1 THEN '' , 36'' ELSE '' '' END +
2499+ CASE WHEN is_table_scan = 1 THEN '' , 37'' ELSE '' '' END +
2500+ CASE WHEN backwards_scan = 1 THEN '' , 38'' ELSE '' '' END +
2501+ CASE WHEN forced_index = 1 THEN '' , 39'' ELSE '' '' END +
2502+ CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '' , 40'' ELSE '' '' END
24442503 , 2, 200000) AS opserver_warning , ' + @nl ;
24452504 END
24462505
@@ -2902,7 +2961,7 @@ BEGIN
29022961 100 ,
29032962 ' Unused memory grants' ,
29042963 ' Queries are asking for more memory than they'' re using' ,
2905- ' No URL yet. ' ,
2964+ ' https://www.brentozar.com/blitzcache/unused-memory-grants/ ' ,
29062965 ' Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.' ) ;
29072966
29082967 IF EXISTS (SELECT 1 / 0
@@ -2915,7 +2974,7 @@ BEGIN
29152974 100 ,
29162975 ' Compute Scalar That References A Function' ,
29172976 ' This could be trouble if you'' re using Scalar Functions or MSTVFs' ,
2918- ' No URL yet. ' ,
2977+ ' https://www.brentozar.com/blitzcache/compute-scalar-functions/ ' ,
29192978 ' Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates' ) ;
29202979
29212980 IF EXISTS (SELECT 1 / 0
@@ -2928,7 +2987,7 @@ BEGIN
29282987 100 ,
29292988 ' Compute Scalar That References A CLR Function' ,
29302989 ' This could be trouble if your CLR functions perform data access' ,
2931- ' No URL yet. ' ,
2990+ ' https://www.brentozar.com/blitzcache/compute-scalar-functions/ ' ,
29322991 ' May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates' ) ;
29332992
29342993
@@ -2942,9 +3001,89 @@ BEGIN
29423001 100 ,
29433002 ' Table Variables detected' ,
29443003 ' Beware nasty side effects' ,
2945- ' No URL yet. ' ,
3004+ ' https://www.brentozar.com/blitzcache/table-variables/ ' ,
29463005 ' All modifications are single threaded, and selects have really low row estimates.' ) ;
29473006
3007+ IF EXISTS (SELECT 1 / 0
3008+ FROM ##bou_BlitzCacheProcs p
3009+ WHERE p .no_stats_warning = 1
3010+ AND SPID = @@SPID )
3011+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3012+ VALUES (@@SPID ,
3013+ 35 ,
3014+ 100 ,
3015+ ' Columns with no statistics' ,
3016+ ' Poor cardinality estimates may ensue' ,
3017+ ' https://www.brentozar.com/blitzcache/columns-no-statistics/' ,
3018+ ' Sometimes this happens with indexed views, other times because auto create stats is turned off.' ) ;
3019+
3020+ IF EXISTS (SELECT 1 / 0
3021+ FROM ##bou_BlitzCacheProcs p
3022+ WHERE p .relop_warnings = 1
3023+ AND SPID = @@SPID )
3024+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3025+ VALUES (@@SPID ,
3026+ 36 ,
3027+ 100 ,
3028+ ' Operator Warnings' ,
3029+ ' SQL is throwing operator level plan warnings' ,
3030+ ' http://brentozar.com/blitzcache/query-plan-warnings/' ,
3031+ ' Check the plan for more details.' ) ;
3032+
3033+ IF EXISTS (SELECT 1 / 0
3034+ FROM ##bou_BlitzCacheProcs p
3035+ WHERE p .is_table_scan = 1
3036+ AND SPID = @@SPID )
3037+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3038+ VALUES (@@SPID ,
3039+ 37 ,
3040+ 100 ,
3041+ ' Table Scans' ,
3042+ ' Your database has HEAPs' ,
3043+ ' https://www.brentozar.com/archive/2012/05/video-heaps/' ,
3044+ ' This may not be a problem. Run sp_BlitzIndex for more information.' ) ;
3045+
3046+ IF EXISTS (SELECT 1 / 0
3047+ FROM ##bou_BlitzCacheProcs p
3048+ WHERE p .backwards_scan = 1
3049+ AND SPID = @@SPID )
3050+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3051+ VALUES (@@SPID ,
3052+ 38 ,
3053+ 100 ,
3054+ ' Backwards Scans' ,
3055+ ' Indexes are being read backwards' ,
3056+ ' https://www.brentozar.com/blitzcache/backwards-scans/' ,
3057+ ' This isn'' t always a problem. They can cause serial zones in plans, and may need an index to match sort order.' ) ;
3058+
3059+ IF EXISTS (SELECT 1 / 0
3060+ FROM ##bou_BlitzCacheProcs p
3061+ WHERE p .forced_index = 1
3062+ AND SPID = @@SPID )
3063+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3064+ VALUES (@@SPID ,
3065+ 39 ,
3066+ 100 ,
3067+ ' Index forcing' ,
3068+ ' Someone is using hints to force index usage' ,
3069+ ' https://www.brentozar.com/blitzcache/optimizer-forcing/' ,
3070+ ' This can cause inefficient plans, and will prevent missing index requests.' ) ;
3071+
3072+ IF EXISTS (SELECT 1 / 0
3073+ FROM ##bou_BlitzCacheProcs p
3074+ WHERE p .forced_seek = 1
3075+ OR p .forced_scan = 1
3076+ AND SPID = @@SPID )
3077+ INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
3078+ VALUES (@@SPID ,
3079+ 40 ,
3080+ 100 ,
3081+ ' Seek/Scan forcing' ,
3082+ ' Someone is using hints to force index seeks/scans' ,
3083+ ' https://www.brentozar.com/blitzcache/optimizer-forcing/' ,
3084+ ' This can cause inefficient plans by taking seek vs scan choice away from the optimizer.' ) ;
3085+
3086+
29483087 IF EXISTS (SELECT 1 / 0
29493088 FROM #plan_creation p
29503089 WHERE p .percent_24 > 0
@@ -2956,11 +3095,10 @@ BEGIN
29563095 254 ,
29573096 ' Plan Cache Information' ,
29583097 ' You have ' + CONVERT (NVARCHAR (10 ), p .percent_24 ) + ' % plans created in the past 24 hours, and ' + CONVERT (NVARCHAR (10 ), p .percent_4 ) + ' % created in the past 4 hours.' ,
2959- ' No URL yet. ' ,
3098+ ' ' ,
29603099 ' If these percentages are high, it may be a sign of memory pressure or plan cache instability.'
29613100 FROM #plan_creation p ;
29623101
2963-
29643102 IF EXISTS (SELECT 1 / 0
29653103 FROM #trace_flags AS tf
29663104 WHERE tf .global_trace_flags IS NOT NULL
0 commit comments