Last active
March 5, 2025 21:37
-
-
Save FlogDonkey/97b455204c11e65109d70bf1e6a995e1 to your computer and use it in GitHub Desktop.
Finds top N execution plans in cache that took the most time to complete, extracts statistics with low sampling with changed rows (and other filtering), and issues UPDATE STATISTIC commands for each. Meant to be run in high frequency on busy systems. Optionally add NORECOMPUTE to stop SQL Server from auto-updating managed statistics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* To persist statistics and disallow engine rebuilding with default sampling, set @WithNoRecompute = 1 */ | |
DECLARE @WithNoRecompute BIT = 1 | |
,@NoRecomputeSQL VARCHAR(50) = ''; | |
IF @WithNoRecompute = 1 | |
BEGIN | |
SET @NoRecomputeSQL = ',NORECOMPUTE'; | |
END; | |
DROP TABLE IF EXISTS #Temp; | |
DECLARE @Count INT = 1000; | |
DECLARE @plans TABLE | |
( | |
query_text NVARCHAR(MAX) | |
,o_name sysname | |
,execution_plan XML | |
,last_execution_time DATETIME | |
,execution_count BIGINT | |
,total_worker_time BIGINT | |
,total_physical_reads BIGINT | |
,total_logical_reads BIGINT | |
); | |
DECLARE @lookups TABLE | |
( | |
table_name sysname | |
,index_name sysname | |
,index_cols NVARCHAR(MAX) | |
); | |
WITH query_stats AS | |
( | |
SELECT TOP (@Count) | |
dm_exec_query_stats.sql_handle | |
,dm_exec_query_stats.plan_handle | |
,SUM(total_dop) AS total_dop | |
,MAX(dm_exec_query_stats.last_execution_time) AS last_execution_time | |
,SUM(dm_exec_query_stats.execution_count) AS execution_count | |
,SUM(dm_exec_query_stats.total_worker_time) AS total_worker_time | |
,SUM(dm_exec_query_stats.total_physical_reads) AS total_physical_reads | |
,SUM(dm_exec_query_stats.total_logical_reads) AS total_logical_reads | |
FROM sys.dm_exec_query_stats | |
GROUP BY | |
dm_exec_query_stats.sql_handle | |
,dm_exec_query_stats.plan_handle | |
ORDER BY | |
execution_count DESC | |
) | |
SELECT TOP (@Count) | |
sql_text.text | |
,DB_NAME(query_plan.dbid) AS DatabaseName | |
,query_stats.total_dop | |
,CASE | |
WHEN sql_text.objectid IS NOT NULL THEN ISNULL(OBJECT_NAME(sql_text.objectid, sql_text.dbid), 'Unresolved') | |
ELSE CAST('Ad-hoc\Prepared' AS sysname) | |
END AS QueryType | |
,query_plan.query_plan | |
,query_stats.last_execution_time | |
,query_stats.execution_count | |
,query_stats.total_worker_time | |
,query_stats.total_physical_reads | |
,query_stats.total_logical_reads | |
INTO #Temp | |
FROM query_stats | |
CROSS APPLY sys.dm_exec_sql_text(query_stats.sql_handle) AS sql_text | |
CROSS APPLY sys.dm_exec_query_plan(query_stats.plan_handle) AS query_plan | |
WHERE query_plan.query_plan IS NOT NULL | |
ORDER BY | |
query_stats.total_worker_time DESC; | |
DECLARE @sql NVARCHAR(MAX) | |
,@SamplingPercent DECIMAL(5, 2) | |
,@LastUpdated DATETIME | |
,@TableName sysname | |
,@StatName sysname | |
,@PercentDataChange DECIMAL(8, 2) | |
,@RowCount VARCHAR(100) | |
,@Message VARCHAR(150); | |
DECLARE db_cursor CURSOR FAST_FORWARD FOR | |
WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') | |
SELECT SQLStatement | |
,SamplingPercent | |
,MAX(LastUpdate) AS MaxUpdate | |
,TableName | |
,StatisticsName | |
,SUM(PercentDataChange) AS PercentDataChange | |
,TableRows | |
FROM ( | |
SELECT DISTINCT | |
'UPDATE STATISTICS ' + x.value('@Schema', 'sysname') + '.' + x.value('@Table', 'sysname') + ' ' | |
+ x.value('@Statistics', 'sysname') + ' WITH SAMPLE ' | |
+ CASE | |
WHEN TableRows <= 100000 THEN '100' /* 100,000 */ | |
WHEN TableRows <= 1000000 THEN '80' /* 1,000,000 */ | |
WHEN TableRows <= 5000000 THEN '50' /* 5,000,000 */ | |
WHEN TableRows <= 10000000 THEN '30' /* 10,000,000 */ | |
WHEN TableRows <= 100000000 THEN '20' /* 100,000,000 */ | |
WHEN TableRows <= 500000000 THEN '10' /* 500,000,000 */ | |
ELSE '5' /* > 500,000,000 */ | |
END + ' PERCENT' + @NoRecomputeSQL AS SQLStatement | |
,CEILING(((sp.rows_sampled * 1.0) / (rows * 1.0)) * 100) AS SamplingPercent | |
,sp.last_updated AS LastUpdate | |
,x.value('@Table', 'sysname') AS TableName | |
,x.value('@Statistics', 'sysname') AS StatisticsName | |
,CASE | |
WHEN TableRows = 0 THEN 0 | |
ELSE (sp.modification_counter * 1.0) / tr.TableRows | |
END AS PercentDataChange | |
,tr.TableRows | |
FROM #Temp t | |
CROSS APPLY t.query_plan.nodes('//OptimizerStatsUsage/StatisticsInfo') AS p(x) | |
CROSS APPLY ( | |
SELECT SUM(ps.row_count) AS TableRows | |
FROM sys.dm_db_partition_stats ps | |
WHERE ps.object_id = OBJECT_ID(PARSENAME(x.value('@Schema', 'sysname'), 1) + '.' | |
+ PARSENAME(x.value('@Table', 'sysname'), 1) | |
) | |
AND ps.index_id IN (0, 1) | |
) AS tr | |
INNER JOIN sys.stats AS ss ON ss.object_id = OBJECT_ID(PARSENAME(x.value('@Schema', 'sysname'), 1) | |
+ '.' | |
+ PARSENAME(x.value('@Table', 'sysname'), 1) | |
) | |
AND ss.name = PARSENAME(x.value('@Statistics', 'sysname'), 1) | |
CROSS APPLY sys.dm_db_stats_properties(ss.object_id, ss.stats_id) sp | |
WHERE 1 = 1 | |
AND CEILING(((sp.rows_sampled * 1.0) / (rows * 1.0)) * 100) < CASE | |
WHEN TableRows <= 100000 THEN '100' /* 100,000 */ | |
WHEN TableRows <= 1000000 THEN '80' /* 1,000,000 */ | |
WHEN TableRows <= 5000000 THEN '50' /* 5,000,000 */ | |
WHEN TableRows <= 10000000 THEN '30' /* 10,000,000 */ | |
WHEN TableRows <= 100000000 THEN '20' /* 100,000,000 */ | |
WHEN TableRows <= 500000000 THEN '10' /* 500,000,000 */ | |
ELSE '5' /* > 500,000,000 */ | |
END | |
AND ( | |
sp.modification_counter > 10000 | |
OR DATEDIFF(HOUR, sp.last_updated, GETDATE()) > 24 | |
) | |
) t | |
GROUP BY | |
SQLStatement | |
,SamplingPercent | |
,TableName | |
,StatisticsName | |
,TableRows | |
ORDER BY | |
PercentDataChange DESC; | |
OPEN db_cursor; | |
FETCH NEXT FROM db_cursor | |
INTO @sql | |
,@SamplingPercent | |
,@LastUpdated | |
,@TableName | |
,@StatName | |
,@PercentDataChange | |
,@RowCount; | |
WHILE @@FETCH_STATUS = 0 | |
BEGIN | |
DECLARE @Timestamp DATETIME2(3) = SYSDATETIME(); | |
SET @Message = 'Start Updating Stat...' + @TableName + ' ' + @StatName + '. ' + CAST(@SamplingPercent AS VARCHAR) | |
+ ': ' + CONVERT(VARCHAR, @Timestamp, 120); | |
RAISERROR(@Message, 0, 1) WITH NOWAIT; | |
SET @Message = CAST(@SamplingPercent AS VARCHAR) + ' sampling. Last updated ' + CONVERT(VARCHAR, @LastUpdated, 120); | |
RAISERROR(@Message, 0, 1) WITH NOWAIT; | |
BEGIN TRY | |
EXEC sp_executesql @sql; | |
SET @Message = 'Stat Updated ' + @TableName + ' ' + @StatName + '. Seconds Elapsed: ' | |
+ CAST(DATEDIFF(SECOND, @Timestamp, SYSDATETIME()) AS VARCHAR); | |
RAISERROR(@Message, 0, 1) WITH NOWAIT; | |
END TRY | |
BEGIN CATCH | |
SET @Message = 'ERROR updating ' + @TableName + ' ' + @StatName + ': ' + ERROR_MESSAGE(); | |
RAISERROR(@Message, 16, 1) WITH NOWAIT; | |
END CATCH; | |
-- Blank line for readability in output | |
RAISERROR('', 0, 1) WITH NOWAIT; | |
FETCH NEXT FROM db_cursor | |
INTO @sql | |
,@SamplingPercent | |
,@LastUpdated | |
,@TableName | |
,@StatName | |
,@PercentDataChange | |
,@RowCount; | |
END; | |
CLOSE db_cursor; | |
DEALLOCATE db_cursor; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment