Skip to content

Instantly share code, notes, and snippets.

@FlogDonkey
Last active March 5, 2025 21:37
Show Gist options
  • Save FlogDonkey/97b455204c11e65109d70bf1e6a995e1 to your computer and use it in GitHub Desktop.
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.
/* 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