@@ -1326,10 +1326,11 @@ DROP TABLE IF EXISTS #missing_index_usage;
13261326DROP TABLE IF EXISTS #missing_index_detail;
13271327DROP TABLE IF EXISTS #missing_index_pretty;
13281328DROP TABLE IF EXISTS #index_spool_ugly;
1329-
13301329DROP TABLE IF EXISTS #ReadableDBs;
13311330DROP TABLE IF EXISTS #plan_usage;
1332-
1331+ DROP TABLE IF EXISTS #plan_usage_by_database;
1332+ DROP TABLE IF EXISTS #plan_cache_by_db;
1333+
13331334CREATE TABLE #only_query_hashes (
13341335 query_hash BINARY (8 )
13351336);
@@ -1619,6 +1620,19 @@ CREATE TABLE #plan_usage
16191620);
16201621
16211622
1623+ CREATE TABLE #plan_usage_by_database
1624+ (
1625+ database_id INT NULL ,
1626+ database_name NVARCHAR (128 ) NULL ,
1627+ plan_count BIGINT NULL ,
1628+ duplicate_plan_hashes BIGINT NULL ,
1629+ percent_duplicate DECIMAL (9 , 2 ) NULL ,
1630+ single_use_plan_count BIGINT NULL ,
1631+ percent_single DECIMAL (9 , 2 ) NULL ,
1632+ spid INT
1633+ );
1634+
1635+
16221636IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys .all_objects o WHERE o .name = ' dm_hadr_database_replica_states' )
16231637BEGIN
16241638 RAISERROR (' Checking for Read intent databases to exclude' ,0 ,0 ) WITH NOWAIT ;
@@ -1627,90 +1641,123 @@ BEGIN
16271641 EXEC (' INSERT INTO #ReadableDBs VALUES (32767) ;' ); -- Exclude internal resource database as well
16281642END
16291643
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
1644+ RAISERROR (N ' Materializing plan cache attributes' , 0 , 1 ) WITH NOWAIT ;
1645+
1646+ /*
1647+ Materialize plan cache data into a temp table once to avoid repeated
1648+ scans of sys.dm_exec_query_stats and CROSS APPLY sys.dm_exec_plan_attributes.
1649+ This single pass feeds plan cache age, server-wide plan usage, and
1650+ per-database plan usage calculations.
1651+ Addresses #3878.
1652+ */
1653+ CREATE TABLE #plan_cache_by_db
1654+ (
1655+ database_id INT NOT NULL ,
1656+ query_hash BINARY (8 ) NOT NULL ,
1657+ query_plan_hash BINARY (8 ) NOT NULL ,
1658+ execution_count BIGINT NOT NULL ,
1659+ creation_time DATETIME NOT NULL ,
1660+ object_id INT NULL
1661+ );
1662+
1663+ INSERT #plan_cache_by_db
1664+ (
1665+ database_id,
1666+ query_hash,
1667+ query_plan_hash,
1668+ execution_count,
1669+ creation_time,
1670+ object_id
16371671)
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
1672+ SELECT
1673+ CONVERT (INT , pa .value ),
1674+ qs .query_hash ,
1675+ qs .query_plan_hash ,
1676+ qs .execution_count ,
1677+ qs .creation_time ,
1678+ ps .object_id
1679+ FROM sys .dm_exec_query_stats AS qs
1680+ LEFT JOIN sys .dm_exec_procedure_stats AS ps
1681+ ON qs .plan_handle = ps .plan_handle
1682+ CROSS APPLY sys .dm_exec_plan_attributes (qs .plan_handle ) AS pa
1683+ WHERE pa .attribute = N ' dbid'
1684+ AND pa .value <> 32767 /* Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/
1685+ AND (
1686+ ISNULL (@IgnoreReadableReplicaDBs, 0 ) = 0
1687+ OR NOT EXISTS
1688+ (
1689+ SELECT 1
1690+ FROM #ReadableDBs AS rdb
1691+ WHERE rdb .DatabaseID = CONVERT (INT , pa .value )
1692+ )
1693+ )
16451694OPTION (RECOMPILE );
16461695
1696+ RAISERROR (N ' Checking plan cache age' , 0 , 1 ) WITH NOWAIT ;
1697+ INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID )
1698+ SELECT CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 24 THEN 1 ELSE 0 END ), 0 )
1699+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1700+ CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 4 THEN 1 ELSE 0 END ), 0 )
1701+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1702+ CONVERT (DECIMAL (5 ,2 ), NULLIF (SUM (CASE WHEN DATEDIFF (HOUR, pc .creation_time , SYSDATETIME ()) <= 1 THEN 1 ELSE 0 END ), 0 )
1703+ / (1 . * NULLIF (COUNT_BIG (* ), 0 ))) * 100 ,
1704+ COUNT_BIG (* ),
1705+ @@SPID
1706+ FROM #plan_cache_by_db AS pc
1707+ OPTION (RECOMPILE );
16471708
16481709RAISERROR (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- )
16841710INSERT
16851711 #plan_usage
16861712(
16871713 duplicate_plan_hashes,
1688- percent_duplicate,
1689- single_use_plan_count,
1690- percent_single,
1691- total_plans,
1692- spid
1714+ percent_duplicate,
1715+ single_use_plan_count,
1716+ percent_single,
1717+ total_plans,
1718+ spid
16931719)
16941720SELECT
1695- m .duplicate_plan_hashes ,
1721+ SUM ( x .duplicate_plan_hashes ),
16961722 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 ,
1723+ (
1724+ DECIMAL (5 , 2 ),
1725+ SUM ( x .duplicate_plan_hashes )
1726+ / (1 . * NULLIF (t .total_plans , 0 ))
1727+ ) * 100 .,
1728+ t .single_use_plan_count ,
17031729 CONVERT
1704- (
1705- decimal (5 ,2 ),
1706- s .single_use_plan_count
1707- / (1 . * NULLIF (t .total_plans , 0 ))
1708- ) * 100 . AS percent_single,
1730+ (
1731+ DECIMAL (5 , 2 ),
1732+ t .single_use_plan_count
1733+ / (1 . * NULLIF (t .total_plans , 0 ))
1734+ ) * 100 .,
1735+ t .total_plans ,
1736+ @@SPID
1737+ FROM
1738+ (
1739+ SELECT
1740+ COUNT_BIG (pc .query_plan_hash ) AS duplicate_plan_hashes
1741+ FROM #plan_cache_by_db AS pc
1742+ WHERE pc .query_plan_hash <> 0x0000000000000000
1743+ GROUP BY
1744+ /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */
1745+ pc .query_hash ,
1746+ pc .object_id ,
1747+ pc .database_id
1748+ HAVING COUNT_BIG (pc .query_plan_hash ) > 5
1749+ ) AS x
1750+ CROSS JOIN
1751+ (
1752+ SELECT
1753+ COUNT_BIG (* ) AS total_plans,
1754+ SUM (CASE WHEN pc .execution_count = 1 THEN 1 ELSE 0 END ) AS single_use_plan_count
1755+ FROM #plan_cache_by_db AS pc
1756+ ) AS t
1757+ GROUP BY
17091758 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;
1759+ t .single_use_plan_count
1760+ OPTION (RECOMPILE );
17141761
17151762
17161763/*
@@ -1720,9 +1767,74 @@ Erik Darling:
17201767
17211768UPDATE #plan_usage
17221769 SET percent_duplicate = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END,
1723- percent_single = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END;
1770+ percent_single = CASE WHEN percent_single > 100 THEN 100 ELSE percent_single END;
17241771*/
17251772
1773+ RAISERROR (N ' Checking for per-database single use plans and duplicate plan hashes' , 0 , 1 ) WITH NOWAIT ;
1774+ INSERT
1775+ #plan_usage_by_database
1776+ (
1777+ database_id,
1778+ database_name ,
1779+ plan_count,
1780+ duplicate_plan_hashes,
1781+ percent_duplicate,
1782+ single_use_plan_count,
1783+ percent_single,
1784+ spid
1785+ )
1786+ SELECT
1787+ p .database_id ,
1788+ DB_NAME (p .database_id ),
1789+ p .plan_count ,
1790+ ISNULL (d .duplicate_plan_hashes , 0 ),
1791+ CONVERT
1792+ (
1793+ DECIMAL (9 , 2 ),
1794+ ISNULL (d .duplicate_plan_hashes , 0 )
1795+ / (1 . * NULLIF (p .plan_count , 0 ))
1796+ ) * 100 .,
1797+ p .single_use_plan_count ,
1798+ CONVERT
1799+ (
1800+ DECIMAL (9 , 2 ),
1801+ p .single_use_plan_count
1802+ / (1 . * NULLIF (p .plan_count , 0 ))
1803+ ) * 100 .,
1804+ @@SPID
1805+ FROM
1806+ (
1807+ SELECT
1808+ pc .database_id ,
1809+ COUNT_BIG (* ) AS plan_count,
1810+ SUM (CASE WHEN pc .execution_count = 1 THEN 1 ELSE 0 END ) AS single_use_plan_count
1811+ FROM #plan_cache_by_db AS pc
1812+ GROUP BY pc .database_id
1813+ ) AS p
1814+ LEFT JOIN
1815+ (
1816+ SELECT
1817+ x .database_id ,
1818+ SUM (x .duplicate_plan_hashes ) AS duplicate_plan_hashes
1819+ FROM
1820+ (
1821+ SELECT
1822+ pc .database_id ,
1823+ COUNT_BIG (pc .query_plan_hash ) AS duplicate_plan_hashes
1824+ FROM #plan_cache_by_db AS pc
1825+ WHERE pc .query_plan_hash <> 0x0000000000000000
1826+ GROUP BY
1827+ pc .query_hash ,
1828+ pc .object_id ,
1829+ pc .database_id
1830+ HAVING COUNT_BIG (pc .query_plan_hash ) > 5
1831+ ) AS x
1832+ GROUP BY x .database_id
1833+ ) AS d
1834+ ON p .database_id = d .database_id
1835+ OPTION (RECOMPILE );
1836+
1837+
17261838SET @OnlySqlHandles = LTRIM (RTRIM (@OnlySqlHandles)) ;
17271839SET @OnlyQueryHashes = LTRIM (RTRIM (@OnlyQueryHashes)) ;
17281840SET @IgnoreQueryHashes = LTRIM (RTRIM (@IgnoreQueryHashes)) ;
@@ -6834,6 +6946,58 @@ BEGIN
68346946 + ' To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = '' query hash'' ; '
68356947 FROM #plan_usage AS p ;
68366948
6949+ /* Per-database duplicate plan findings. Addresses #3878 */
6950+ IF EXISTS (SELECT 1 / 0
6951+ FROM #plan_usage_by_database p
6952+ WHERE p .percent_duplicate > 10
6953+ AND p .spid = @@SPID )
6954+ INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
6955+ SELECT p .spid ,
6956+ 1001 ,
6957+ CASE WHEN ISNULL (p .percent_duplicate , 0 ) > 75 THEN 1 ELSE 254 END AS Priority,
6958+ ' Plan Cache Information' ,
6959+ CASE WHEN ISNULL (p .percent_duplicate , 0 ) > 75
6960+ THEN ' Many Duplicate Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6961+ ELSE ' Duplicate Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6962+ END AS Finding,
6963+ ' https://www.brentozar.com/archive/2018/03/why-multiple-plans-for-one-query-are-bad/' ,
6964+ ' Database ' + ISNULL (p .database_name , N ' Unknown' )
6965+ + ' has ' + CONVERT (NVARCHAR (20 ), p .plan_count )
6966+ + ' plans in the cache, and '
6967+ + CONVERT (NVARCHAR (10 ), p .percent_duplicate )
6968+ + ' % are duplicates with more than 5 entries'
6969+ + ' , meaning similar queries in this database are generating the same plan repeatedly.'
6970+ + ' Forced Parameterization may fix the issue.'
6971+ FROM #plan_usage_by_database AS p
6972+ WHERE p .percent_duplicate > 10
6973+ AND p .spid = @@SPID ;
6974+
6975+ /* Per-database single-use plan findings. Addresses #3878 */
6976+ IF EXISTS (SELECT 1 / 0
6977+ FROM #plan_usage_by_database p
6978+ WHERE p .percent_single > 10
6979+ AND p .spid = @@SPID )
6980+ INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
6981+ SELECT p .spid ,
6982+ 1002 ,
6983+ CASE WHEN ISNULL (p .percent_single , 0 ) > 75 THEN 1 ELSE 254 END AS Priority,
6984+ ' Plan Cache Information' ,
6985+ CASE WHEN ISNULL (p .percent_single , 0 ) > 75
6986+ THEN ' Many Single-Use Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6987+ ELSE ' Single-Use Plans In ' + ISNULL (p .database_name , N ' Unknown' )
6988+ END AS Finding,
6989+ ' https://www.brentozar.com/blitz/single-use-plans-procedure-cache/' ,
6990+ ' Database ' + ISNULL (p .database_name , N ' Unknown' )
6991+ + ' has ' + CONVERT (NVARCHAR (20 ), p .plan_count )
6992+ + ' plans in the cache, and '
6993+ + CONVERT (NVARCHAR (10 ), p .percent_single )
6994+ + ' % are single use plans'
6995+ + ' , meaning SQL Server thinks it'' s seeing a lot of "new" queries from this database.'
6996+ + ' Forced Parameterization and/or Optimize For Ad Hoc Workloads may fix the issue.'
6997+ FROM #plan_usage_by_database AS p
6998+ WHERE p .percent_single > 10
6999+ AND p .spid = @@SPID ;
7000+
68377001 IF @is_tokenstore_big = 1
68387002 INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL , Details)
68397003 SELECT @@SPID ,
0 commit comments