@@ -1619,6 +1619,19 @@ CREATE TABLE #plan_usage
16191619);
16201620
16211621
1622+ CREATE TABLE #plan_usage_by_database
1623+ (
1624+ database_id INT NULL ,
1625+ database_name NVARCHAR (128 ) NULL ,
1626+ plan_count BIGINT NULL ,
1627+ duplicate_plan_hashes BIGINT NULL ,
1628+ percent_duplicate DECIMAL (9 , 2 ) NULL ,
1629+ single_use_plan_count BIGINT NULL ,
1630+ percent_single DECIMAL (9 , 2 ) NULL ,
1631+ spid INT
1632+ );
1633+
1634+
16221635IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys .all_objects o WHERE o .name = ' dm_hadr_database_replica_states' )
16231636BEGIN
16241637 RAISERROR (' Checking for Read intent databases to exclude' ,0 ,0 ) WITH NOWAIT ;
@@ -1627,90 +1640,114 @@ BEGIN
16271640 EXEC (' INSERT INTO #ReadableDBs VALUES (32767) ;' ); -- Exclude internal resource database as well
16281641END
16291642
1630- RAISERROR (N ' Checking plan cache age' , 0 , 1 ) WITH NOWAIT ;
1631- WITH x AS (
1632- SELECT SUM (CASE WHEN DATEDIFF (HOUR, deqs .creation_time , SYSDATETIME ()) <= 24 THEN 1 ELSE 0 END ) AS [plans_24],
1633- SUM (CASE WHEN DATEDIFF (HOUR, deqs .creation_time , SYSDATETIME ()) <= 4 THEN 1 ELSE 0 END ) AS [plans_4],
1634- SUM (CASE WHEN DATEDIFF (HOUR, deqs .creation_time , SYSDATETIME ()) <= 1 THEN 1 ELSE 0 END ) AS [plans_1],
1635- COUNT (deqs .creation_time ) AS [total_plans]
1636- FROM sys .dm_exec_query_stats AS deqs
1643+ RAISERROR (N ' Materializing plan cache attributes' , 0 , 1 ) WITH NOWAIT ;
1644+
1645+ /*
1646+ Materialize plan cache data into a temp table once to avoid repeated
1647+ scans of sys.dm_exec_query_stats and CROSS APPLY sys.dm_exec_plan_attributes.
1648+ This single pass feeds plan cache age, server-wide plan usage, and
1649+ per-database plan usage calculations.
1650+ Addresses #3878.
1651+ */
1652+ CREATE TABLE #plan_cache_by_db
1653+ (
1654+ database_id INT NOT NULL ,
1655+ query_hash BINARY (8 ) NOT NULL ,
1656+ query_plan_hash BINARY (8 ) NOT NULL ,
1657+ execution_count BIGINT NOT NULL ,
1658+ creation_time DATETIME NOT NULL ,
1659+ object_id INT NULL
1660+ );
1661+
1662+ INSERT #plan_cache_by_db
1663+ (
1664+ database_id,
1665+ query_hash,
1666+ query_plan_hash,
1667+ execution_count,
1668+ creation_time,
1669+ object_id
16371670)
1638- INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID )
1639- SELECT CONVERT (DECIMAL (5 ,2 ), NULLIF (x .plans_24 , 0 ) / (1 . * NULLIF (x .total_plans , 0 ))) * 100 AS [percent_24],
1640- CONVERT (DECIMAL (5 ,2 ), NULLIF (x .plans_4 , 0 ) / (1 . * NULLIF (x .total_plans , 0 ))) * 100 AS [percent_4],
1641- CONVERT (DECIMAL (5 ,2 ), NULLIF (x .plans_1 , 0 ) / (1 . * NULLIF (x .total_plans , 0 ))) * 100 AS [percent_1],
1642- x .total_plans ,
1643- @@SPID AS SPID
1644- FROM x
1671+ SELECT
1672+ CONVERT (INT , pa .value ),
1673+ qs .query_hash ,
1674+ qs .query_plan_hash ,
1675+ qs .execution_count ,
1676+ qs .creation_time ,
1677+ ps .object_id
1678+ FROM sys .dm_exec_query_stats AS qs
1679+ LEFT JOIN sys .dm_exec_procedure_stats AS ps
1680+ ON qs .plan_handle = ps .plan_handle
1681+ CROSS APPLY sys .dm_exec_plan_attributes (qs .plan_handle ) AS pa
1682+ WHERE pa .attribute = N ' dbid'
1683+ AND pa .value <> 32767 /* Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/
16451684OPTION (RECOMPILE );
16461685
1686+ RAISERROR (N ' Checking plan cache age' , 0 , 1 ) WITH NOWAIT ;
1687+ INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID )
1688+ SELECT CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 24 THEN 1 ELSE 0 END ), 0 )
1689+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1690+ CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 4 THEN 1 ELSE 0 END ), 0 )
1691+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1692+ CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 1 THEN 1 ELSE 0 END ), 0 )
1693+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1694+ COUNT_BIG (* ),
1695+ @@SPID
1696+ FROM #plan_cache_by_db AS pc
1697+ OPTION (RECOMPILE );
16471698
16481699RAISERROR (N ' Checking for single use plans and plans with many queries' , 0 , 1 ) WITH NOWAIT ;
1649- WITH total_plans AS
1650- (
1651- SELECT
1652- COUNT_BIG (deqs .query_plan_hash ) AS total_plans
1653- FROM sys .dm_exec_query_stats AS deqs
1654- ),
1655- many_plans AS
1656- (
1657- SELECT
1658- SUM (x .duplicate_plan_hashes ) AS duplicate_plan_hashes
1659- FROM
1660- (
1661- SELECT
1662- COUNT_BIG (qs .query_plan_hash ) AS duplicate_plan_hashes
1663- FROM sys .dm_exec_query_stats qs
1664- LEFT JOIN sys .dm_exec_procedure_stats ps ON qs .plan_handle = ps .plan_handle
1665- CROSS APPLY sys .dm_exec_plan_attributes (qs .plan_handle ) pa
1666- WHERE pa .attribute = N ' dbid'
1667- AND pa .value <> 32767 /* Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/
1668- AND qs .query_plan_hash <> 0x0000000000000000
1669- GROUP BY
1670- /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */
1671- qs .query_hash ,
1672- ps .object_id ,
1673- pa .value
1674- HAVING COUNT_BIG (qs .query_plan_hash ) > 5
1675- ) AS x
1676- ),
1677- single_use_plans AS
1678- (
1679- SELECT
1680- COUNT_BIG (* ) AS single_use_plan_count
1681- FROM sys .dm_exec_query_stats AS s
1682- WHERE s .execution_count = 1
1683- )
16841700INSERT
16851701 #plan_usage
16861702(
16871703 duplicate_plan_hashes,
1688- percent_duplicate,
1689- single_use_plan_count,
1690- percent_single,
1691- total_plans,
1692- spid
1704+ percent_duplicate,
1705+ single_use_plan_count,
1706+ percent_single,
1707+ total_plans,
1708+ spid
16931709)
16941710SELECT
1695- m .duplicate_plan_hashes ,
1711+ SUM ( x .duplicate_plan_hashes ),
16961712 CONVERT
1697- (
1698- decimal (5 ,2 ),
1699- m .duplicate_plan_hashes
1700- / (1 . * NULLIF (t .total_plans , 0 ))
1701- ) * 100 . AS percent_duplicate ,
1702- s .single_use_plan_count ,
1713+ (
1714+ DECIMAL (5 , 2 ),
1715+ SUM ( x .duplicate_plan_hashes )
1716+ / (1 . * NULLIF (t .total_plans , 0 ))
1717+ ) * 100 .,
1718+ t .single_use_plan_count ,
17031719 CONVERT
1704- (
1705- decimal (5 ,2 ),
1706- s .single_use_plan_count
1707- / (1 . * NULLIF (t .total_plans , 0 ))
1708- ) * 100 . AS percent_single ,
1720+ (
1721+ DECIMAL (5 , 2 ),
1722+ t .single_use_plan_count
1723+ / (1 . * NULLIF (t .total_plans , 0 ))
1724+ ) * 100 .,
17091725 t .total_plans ,
1710- @@SPID
1711- FROM many_plans AS m
1712- CROSS JOIN single_use_plans AS s
1713- CROSS JOIN total_plans AS t;
1726+ @@SPID
1727+ FROM
1728+ (
1729+ SELECT
1730+ COUNT_BIG (pc .query_plan_hash ) AS duplicate_plan_hashes
1731+ FROM #plan_cache_by_db AS pc
1732+ WHERE pc .query_plan_hash <> 0x0000000000000000
1733+ GROUP BY
1734+ /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */
1735+ pc .query_hash ,
1736+ pc .object_id ,
1737+ pc .database_id
1738+ HAVING COUNT_BIG (pc .query_plan_hash ) > 5
1739+ ) AS x
1740+ CROSS JOIN
1741+ (
1742+ SELECT
1743+ COUNT_BIG (* ) AS total_plans,
1744+ SUM (CASE WHEN pc .execution_count = 1 THEN 1 ELSE 0 END ) AS single_use_plan_count
1745+ FROM #plan_cache_by_db AS pc
1746+ ) AS t
1747+ GROUP BY
1748+ t .total_plans ,
1749+ t .single_use_plan_count
1750+ OPTION (RECOMPILE );
17141751
17151752
17161753/*
@@ -1723,6 +1760,71 @@ UPDATE #plan_usage
17231760 percent_single = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END;
17241761*/
17251762
1763+ RAISERROR (N ' Checking for per-database single use plans and duplicate plan hashes' , 0 , 1 ) WITH NOWAIT ;
1764+ INSERT
1765+ #plan_usage_by_database
1766+ (
1767+ database_id,
1768+ database_name ,
1769+ plan_count,
1770+ duplicate_plan_hashes,
1771+ percent_duplicate,
1772+ single_use_plan_count,
1773+ percent_single,
1774+ spid
1775+ )
1776+ SELECT
1777+ p .database_id ,
1778+ DB_NAME (p .database_id ),
1779+ p .plan_count ,
1780+ ISNULL (d .duplicate_plan_hashes , 0 ),
1781+ CONVERT
1782+ (
1783+ DECIMAL (9 , 2 ),
1784+ ISNULL (d .duplicate_plan_hashes , 0 )
1785+ / (1 . * NULLIF (p .plan_count , 0 ))
1786+ ) * 100 .,
1787+ p .single_use_plan_count ,
1788+ CONVERT
1789+ (
1790+ DECIMAL (9 , 2 ),
1791+ p .single_use_plan_count
1792+ / (1 . * NULLIF (p .plan_count , 0 ))
1793+ ) * 100 .,
1794+ @@SPID
1795+ FROM
1796+ (
1797+ SELECT
1798+ pc .database_id ,
1799+ COUNT_BIG (* ) AS plan_count,
1800+ SUM (CASE WHEN pc .execution_count = 1 THEN 1 ELSE 0 END ) AS single_use_plan_count
1801+ FROM #plan_cache_by_db AS pc
1802+ GROUP BY pc .database_id
1803+ ) AS p
1804+ LEFT JOIN
1805+ (
1806+ SELECT
1807+ x .database_id ,
1808+ SUM (x .duplicate_plan_hashes ) AS duplicate_plan_hashes
1809+ FROM
1810+ (
1811+ SELECT
1812+ pc .database_id ,
1813+ COUNT_BIG (pc .query_plan_hash ) AS duplicate_plan_hashes
1814+ FROM #plan_cache_by_db AS pc
1815+ WHERE pc .query_plan_hash <> 0x0000000000000000
1816+ GROUP BY
1817+ pc .query_hash ,
1818+ pc .object_id ,
1819+ pc .database_id
1820+ HAVING COUNT_BIG (pc .query_plan_hash ) > 5
1821+ ) AS x
1822+ GROUP BY x .database_id
1823+ ) AS d
1824+ ON p .database_id = d .database_id
1825+ OPTION (RECOMPILE );
1826+
1827+
17261828SET @OnlySqlHandles = LTRIM (RTRIM (@OnlySqlHandles)) ;
17271829SET @OnlyQueryHashes = LTRIM (RTRIM (@OnlyQueryHashes)) ;
17281830SET @IgnoreQueryHashes = LTRIM (RTRIM (@IgnoreQueryHashes)) ;
@@ -6834,6 +6936,58 @@ BEGIN
68346936 + ' To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = '' query hash'' ; '
68356937 FROM #plan_usage AS p ;
68366938
6939+ /* Per-database duplicate plan findings. Addresses #3878 */
6940+ IF EXISTS (SELECT 1 / 0
6941+ FROM #plan_usage_by_database p
6942+ WHERE p .percent_duplicate > 10
6943+ AND p .spid = @@SPID )
6944+ INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
6945+ SELECT p .spid ,
6946+ 1001 ,
6947+ CASE WHEN ISNULL (p .percent_duplicate , 0 ) > 75 THEN 1 ELSE 254 END AS Priority,
6948+ ' Plan Cache Information' ,
6949+ CASE WHEN ISNULL (p .percent_duplicate , 0 ) > 75
6950+ THEN ' Many Duplicate Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6951+ ELSE ' Duplicate Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6952+ END AS Finding,
6953+ ' https://www.brentozar.com/archive/2018/03/why-multiple-plans-for-one-query-are-bad/' ,
6954+ ' Database ' + ISNULL (p .database_name , N ' Unknown' )
6955+ + ' has ' + CONVERT (NVARCHAR (20 ), p .plan_count )
6956+ + ' plans in the cache, and '
6957+ + CONVERT (NVARCHAR (10 ), p .percent_duplicate )
6958+ + ' % are duplicates with more than 5 entries'
6959+ + ' , meaning similar queries in this database are generating the same plan repeatedly.'
6960+ + ' Forced Parameterization may fix the issue.'
6961+ FROM #plan_usage_by_database AS p
6962+ WHERE p .percent_duplicate > 10
6963+ AND p .spid = @@SPID ;
6964+
6965+ /* Per-database single-use plan findings. Addresses #3878 */
6966+ IF EXISTS (SELECT 1 / 0
6967+ FROM #plan_usage_by_database p
6968+ WHERE p .percent_single > 10
6969+ AND p .spid = @@SPID )
6970+ INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
6971+ SELECT p .spid ,
6972+ 1002 ,
6973+ CASE WHEN ISNULL (p .percent_single , 0 ) > 75 THEN 1 ELSE 254 END AS Priority,
6974+ ' Plan Cache Information' ,
6975+ CASE WHEN ISNULL (p .percent_single , 0 ) > 75
6976+ THEN ' Many Single-Use Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6977+ ELSE ' Single-Use Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6978+ END AS Finding,
6979+ ' https://www.brentozar.com/blitz/single-use-plans-procedure-cache/' ,
6980+ ' Database ' + ISNULL (p .database_name , N ' Unknown' )
6981+ + ' has ' + CONVERT (NVARCHAR (20 ), p .plan_count )
6982+ + ' plans in the cache, and '
6983+ + CONVERT (NVARCHAR (10 ), p .percent_single )
6984+ + ' % are single use plans'
6985+ + ' , meaning SQL Server thinks it'' s seeing a lot of "new" queries from this database.'
6986+ + ' Forced Parameterization and/or Optimize For Ad Hoc Workloads may fix the issue.'
6987+ FROM #plan_usage_by_database AS p
6988+ WHERE p .percent_single > 10
6989+ AND p .spid = @@SPID ;
6990+
68376991 IF @is_tokenstore_big = 1
68386992 INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
68396993 SELECT @@SPID ,
0 commit comments