Skip to content

Commit 1bc395e

Browse files
authored
Merge pull request #3879 from erikdarlingdata/3878-per-database-plan-cache-findings
sp_BlitzCache: per-database plan cache findings (#3878)
2 parents 908beb0 + 335b294 commit 1bc395e

1 file changed

Lines changed: 237 additions & 73 deletions

File tree

sp_BlitzCache.sql

Lines changed: 237 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,10 +1326,11 @@ DROP TABLE IF EXISTS #missing_index_usage;
13261326
DROP TABLE IF EXISTS #missing_index_detail;
13271327
DROP TABLE IF EXISTS #missing_index_pretty;
13281328
DROP TABLE IF EXISTS #index_spool_ugly;
1329-
13301329
DROP TABLE IF EXISTS #ReadableDBs;
13311330
DROP 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+
13331334
CREATE 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+
16221636
IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states')
16231637
BEGIN
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
16281642
END
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+
)
16451694
OPTION (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

16481709
RAISERROR(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-
)
16841710
INSERT
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
)
16941720
SELECT
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
17211768
UPDATE #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+
17261838
SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ;
17271839
SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ;
17281840
SET @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

Comments
 (0)