Priority-based statistics maintenance for SQL Server 2016+
Updates worst stats first. Stops when you tell it to. Tells you if it got killed.
| Problem | Fix |
|---|---|
| Alphabetical ordering | @SortOrder = 'MODIFICATION_COUNTER' - worst first |
| 10-hour jobs killed at 5 AM | @TimeLimit - stops gracefully, logs what's left |
| "Did it finish or get killed?" | START/END markers in CommandLog |
| NORECOMPUTE orphans | @TargetNorecompute = 'Y' - finds and refreshes them |
| Large stats that never finish | @LongRunningThresholdMinutes - auto-reduce sample rate |
| Query Store knows what's hot | @QueryStorePriority = 'Y' - prioritize by CPU/reads |
| Cascading failures | @MaxConsecutiveFailures - stops after N failures |
| AG secondary falls behind | @MaxAGRedoQueueMB - pauses when redo queue is deep |
| tempdb pressure during FULLSCAN | @MinTempdbFreeMB - checks before each stat update |
| Azure DTU/vCore concerns | Auto-detects Azure SQL DB vs MI, platform-specific warnings |
| Indexed view stats ignored | @IncludeIndexedViews = 'Y' - discovers view statistics |
| No audit trail for skipped stats | @LogSkippedToCommandLog = 'Y' - TOCTOU skip logging |
-- 1. Install prerequisites (Ola Hallengren's CommandLog table)
-- Download from: https://ola.hallengren.com/scripts/CommandLog.sql
-- 2. Install sp_StatUpdate
-- Run sp_StatUpdate.sql in your maintenance database
-- 3. Run statistics maintenance
EXEC dbo.sp_StatUpdate
@Databases = N'YourDatabase',
@TargetNorecompute = N'BOTH', -- All stats (NORECOMPUTE + regular)
@TimeLimit = 3600; -- 1 hour limit
-- Defaults: @TieredThresholds=1, @ModificationThreshold=5000, @LogToTable='Y'DROP-IN COMPATIBLE with Ola Hallengren's SQL Server Maintenance Solution.
| Requirement | Details |
|---|---|
| SQL Server | 2016+ (uses STRING_SPLIT). 2016 SP2+ recommended for MAXDOP support |
| Azure SQL | Database (EngineEdition 5), Managed Instance (8), and Edge (9) supported |
| dbo.CommandLog | CommandLog.sql or set @LogToTable = 'N' |
| dbo.Queue | Queue.sql - only for @StatsInParallel = 'Y' |
Note: dbo.QueueStatistic is auto-created on first parallel run. dbo.CommandExecute is NOT required.
-- Nightly maintenance (1hr, tiered thresholds, balanced)
EXEC dbo.sp_StatUpdate @Preset = 'NIGHTLY_MAINTENANCE', @Databases = 'USER_DATABASES';
-- Weekly comprehensive (4hr, lower thresholds)
EXEC dbo.sp_StatUpdate @Preset = 'WEEKLY_FULL', @Databases = 'USER_DATABASES';
-- OLTP with minimal impact (30min, high thresholds, delays)
EXEC dbo.sp_StatUpdate @Preset = 'OLTP_LIGHT', @Databases = 'MyOLTPDatabase';
-- Data warehouse full refresh (no limit, FULLSCAN)
EXEC dbo.sp_StatUpdate @Preset = 'WAREHOUSE_AGGRESSIVE', @Databases = 'MyDW';-- Nightly job: 11 PM - 4 AM window (5 hours)
EXEC dbo.sp_StatUpdate
@Databases = N'USER_DATABASES, -DevDB, -ReportingDB',
@TimeLimit = 18000,
@SortOrder = N'MODIFICATION_COUNTER'; -- Worst stats first-- Let Query Store tell you what matters
EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@QueryStorePriority = N'Y',
@QueryStoreMetric = N'CPU', -- Or DURATION, READS
@SortOrder = N'QUERY_STORE',
@TimeLimit = 3600;-- Find and refresh forgotten NORECOMPUTE stats
EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@TargetNorecompute = N'Y',
@ModificationThreshold = 50000,
@TimeLimit = 1800;-- Pause if any secondary falls behind by 500 MB redo
EXEC dbo.sp_StatUpdate
@Databases = N'USER_DATABASES',
@MaxAGRedoQueueMB = 500,
@MaxAGWaitMinutes = 10, -- Wait up to 10 min for drain
@TimeLimit = 3600;-- Stats that historically took >2 hours get 5% sample
EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@LongRunningThresholdMinutes = 120,
@LongRunningSamplePercent = 5,
@TimeLimit = 14400;EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@Execute = N'N',
@WhatIfOutputTable = N'#Preview',
@Debug = 1;
SELECT * FROM #Preview ORDER BY SequenceNum;-- Downstream ETL waits for this table to have a row
EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@TimeLimit = 3600,
@CompletionNotifyTable = N'dbo.StatUpdateNotify';
-- ETL checks: SELECT * FROM dbo.StatUpdateNotify WHERE RunLabel = ...Run EXEC sp_StatUpdate @Help = 1 for complete documentation including operational notes.
| Parameter | Default | Description |
|---|---|---|
@Databases |
Current DB | USER_DATABASES, SYSTEM_DATABASES, ALL_DATABASES, AVAILABILITY_GROUP_DATABASES, wildcards (%Prod%), exclusions (-DevDB) |
@Tables |
All | Table filter (comma-separated Schema.Table) |
@ExcludeTables |
NULL |
Exclude tables by LIKE pattern (%Archive%) |
@ExcludeStatistics |
NULL |
Exclude stats by LIKE pattern (_WA_Sys%) |
@Statistics |
NULL |
Direct stat references (Schema.Table.Stat, comma-separated) |
@StatisticsFromTable |
NULL |
Table containing stat references (#MyStats, dbo.StatsQueue) |
@IncludeSystemObjects |
'N' |
Include stats on system objects |
@IncludeIndexedViews |
'N' |
Include statistics on indexed views (v2.8+) |
| Parameter | Default | Description |
|---|---|---|
@TargetNorecompute |
'BOTH' |
'Y'=NORECOMPUTE only, 'N'=regular only, 'BOTH'=all |
@ModificationThreshold |
5000 |
Minimum modifications to qualify |
@ModificationPercent |
NULL |
Alternative: min mod % of rows (SQRT-based) |
@TieredThresholds |
1 |
Use Tiger Toolbox 5-tier adaptive formula |
@ThresholdLogic |
'OR' |
'OR'=any threshold, 'AND'=all must be met |
@DaysStaleThreshold |
NULL |
Minimum days since last update |
@HoursStaleThreshold |
NULL |
Alternative: minimum hours since last update |
@MinPageCount |
0 |
Minimum pages (125 = ~1MB, 125000 = ~1GB) |
| Parameter | Default | Description |
|---|---|---|
@TimeLimit |
3600 |
Max seconds (1 hour). NULL = unlimited |
@StopByTime |
NULL |
Absolute wall-clock stop time ('04:00' = 4 AM) |
@BatchLimit |
NULL |
Max stats per run |
@MaxConsecutiveFailures |
NULL |
Stop after N consecutive failures |
@LockTimeout |
NULL |
Seconds to wait for schema locks per stat |
@DelayBetweenStats |
NULL |
Seconds to pause between stats |
@SortOrder |
'MODIFICATION_COUNTER' |
Priority order (see below) |
@Execute |
'Y' |
'N' for dry run |
@FailFast |
0 |
1 = abort on first error |
| Value | Description |
|---|---|
MODIFICATION_COUNTER |
Most modifications first (default) |
DAYS_STALE |
Oldest stats first |
PAGE_COUNT |
Largest tables first |
QUERY_STORE |
Highest Query Store metric first |
FILTERED_DRIFT |
Filtered stats with drift first |
AUTO_CREATED |
User-created stats before auto-created |
RANDOM |
Random order |
| Parameter | Default | Description |
|---|---|---|
@MaxAGRedoQueueMB |
NULL |
Pause when AG secondary redo queue exceeds this MB |
@MaxAGWaitMinutes |
10 |
Max minutes to wait for redo queue to drain |
@MinTempdbFreeMB |
NULL |
Min tempdb free space (MB). @FailFast=1 aborts, else warns |
| Parameter | Default | Description |
|---|---|---|
@QueryStorePriority |
'N' |
Prioritize stats used by Query Store plans |
@QueryStoreMetric |
'CPU' |
CPU, DURATION, READS, EXECUTIONS, or AVG_CPU |
@QueryStoreMinExecutions |
100 |
Minimum plan executions to boost |
@QueryStoreRecentHours |
168 |
Only consider plans from last N hours |
@GroupByJoinPattern |
'Y' |
Update joined tables together (prevents optimization cliffs) |
| Parameter | Default | Description |
|---|---|---|
@LongRunningThresholdMinutes |
NULL |
Stats that took longer get forced sample rate |
@LongRunningSamplePercent |
10 |
Sample percent for long-running stats |
@StatisticsSample |
NULL |
NULL=SQL Server decides, 100=FULLSCAN |
@PersistSamplePercent |
'Y' |
PERSIST_SAMPLE_PERCENT (SQL 2016 SP1 CU4+) |
@MaxDOP |
NULL |
MAXDOP for UPDATE STATISTICS (SQL 2016 SP2+) |
| Parameter | Default | Description |
|---|---|---|
@LogToTable |
'Y' |
Log to dbo.CommandLog |
@LogSkippedToCommandLog |
'N' |
Log TOCTOU-skipped stats for audit trail (v2.8+) |
@ProgressLogInterval |
NULL |
Log progress every N stats |
@ReturnDetailedResults |
0 |
1 = return per-statistic detail result set (v2.8+) |
@CompletionNotifyTable |
NULL |
Table for completion notification row (v2.8+) |
@WhatIfOutputTable |
NULL |
Table for dry-run commands (@Execute = 'N' required) |
@Debug |
0 |
1 = verbose diagnostic output |
| Parameter | Description |
|---|---|
@Version |
Procedure version string |
@VersionDate |
Procedure version date |
@StatsFoundOut |
Total qualifying stats discovered |
@StatsProcessedOut |
Stats attempted (succeeded + failed) |
@StatsSucceededOut |
Stats updated successfully |
@StatsFailedOut |
Stats that failed to update |
@StatsRemainingOut |
Stats not processed (time/batch limit) |
@DurationSecondsOut |
Total run duration in seconds |
@WarningsOut |
Collected warnings (see below) |
@StopReasonOut |
Why execution stopped (see below) |
COMPLETED, TIME_LIMIT, BATCH_LIMIT, FAIL_FAST, CONSECUTIVE_FAILURES, AG_REDO_QUEUE, TEMPDB_PRESSURE, NO_QUALIFYING_STATS, KILLED
LOW_UPTIME, BACKUP_RUNNING, AZURE_SQL, AZURE_MI, RESOURCE_GOVERNOR, AG_REDO_ELEVATED, TEMPDB_LOW, RLS_DETECTED, COLUMNSTORE_CONTEXT, QS_FORCED_PLANS, LOG_SPACE_HIGH, WIDE_STATS, FILTER_MISMATCH
Debug mode (@Debug = 1) automatically reports:
- SQL Server version and build number
- Cardinality Estimator version per database (Legacy CE 70 vs New CE 120+)
- Trace flags affecting statistics (2371, 9481, 2389/2390, 4139)
- DB-scoped configs (LEGACY_CARDINALITY_ESTIMATION)
- Hardware context (CPU count, memory, NUMA nodes, uptime)
- Azure platform (SQL DB vs Managed Instance vs Edge, with platform-specific guidance)
- AG primary status and redo queue depth (v2.7+)
- tempdb free space (v2.7+)
- Resource Governor active resource pools (v2.5+)
When @Debug = 1, after discovery the proc checks each database for:
- Row-Level Security policies that may bias histograms
- Wide statistics (>8 columns) that increase tempdb/memory pressure
- Filtered index mismatches where stat filter differs from index filter
- Columnstore indexes where
modification_counterunderreports - Non-persisted computed columns with evaluation cost during stat updates
- Stretch Database tables (auto-skipped, deprecated feature)
- Query Store forced plans on updated tables (post-update check, automatic)
- Transaction log space >90% full during FULLSCAN operations
Every run returns a summary row:
Status StatusMessage StatsFound ...
-------------- -------------------------------------------------- ---------- ---
SUCCESS All 142 stat(s) updated successfully 142 ...
WARNING Incomplete: 47 stat(s) remaining (TIME_LIMIT) 189 ...
ERROR Failed: 3 stat(s), 47 remaining (FAIL_FAST) 150 ...
-- Recent runs: did they finish or get killed?
SELECT
CASE WHEN e.ID IS NOT NULL THEN 'Completed' ELSE 'KILLED' END AS Status,
s.StartTime,
e.ExtendedInfo.value('(/Summary/StopReason)[1]', 'nvarchar(50)') AS StopReason,
e.ExtendedInfo.value('(/Summary/StatsProcessed)[1]', 'int') AS Processed,
e.ExtendedInfo.value('(/Summary/StatsRemaining)[1]', 'int') AS Remaining
FROM dbo.CommandLog s
LEFT JOIN dbo.CommandLog e
ON e.CommandType = 'SP_STATUPDATE_END'
AND e.ExtendedInfo.value('(/Summary/RunLabel)[1]', 'nvarchar(100)') =
s.ExtendedInfo.value('(/Parameters/RunLabel)[1]', 'nvarchar(100)')
WHERE s.CommandType = 'SP_STATUPDATE_START'
ORDER BY s.StartTime DESC;DECLARE @Found int, @Processed int, @Failed int, @Remaining int,
@StopReason nvarchar(50), @Warnings nvarchar(max);
EXEC dbo.sp_StatUpdate
@Databases = N'Production',
@TimeLimit = 3600,
@StatsFoundOut = @Found OUTPUT,
@StatsProcessedOut = @Processed OUTPUT,
@StatsFailedOut = @Failed OUTPUT,
@StatsRemainingOut = @Remaining OUTPUT,
@StopReasonOut = @StopReason OUTPUT,
@WarningsOut = @Warnings OUTPUT;
-- Use outputs for alerting, logging, or conditional logic
IF @Failed > 0 OR @StopReason = 'CONSECUTIVE_FAILURES'
RAISERROR(N'Alert: Statistics maintenance had failures', 10, 1) WITH NOWAIT;
IF @Warnings LIKE '%AG_REDO_ELEVATED%'
RAISERROR(N'Note: AG redo queue was elevated during maintenance', 10, 1) WITH NOWAIT;-- Secure option: log to CommandLog every 50 stats
EXEC sp_StatUpdate @Databases = 'USER_DATABASES', @ProgressLogInterval = 50;
-- Opt-in global temp table (visible to all sessions - security consideration)
EXEC sp_StatUpdate @Databases = 'USER_DATABASES', @ExposeProgressToAllSessions = 'Y';
-- Query from another session: SELECT * FROM ##sp_StatUpdate_Progress;sp_StatUpdate_Diag analyzes CommandLog history and produces actionable recommendations.
-- Basic diagnostic (last 30 days)
EXEC dbo.sp_StatUpdate_Diag;
-- Obfuscated for external sharing (hashes names)
EXEC dbo.sp_StatUpdate_Diag @Obfuscate = 1;.\Invoke-StatUpdateDiag.ps1 `
-Servers "Server1", "Server2,2500", "Server3" `
-CommandLogDatabase "Maintenance" `
-OutputPath ".\diag_output"| Severity | Checks |
|---|---|
| CRITICAL | Killed runs, repeated stat failures, time limit exhaustion, degrading throughput |
| WARNING | Suboptimal parameters, long-running stats, stale-stats backlog, overlapping runs |
| INFO | Run health trends, parameter history, top tables by cost, version history |
An XE session is included for runtime troubleshooting:
-- Create and start (see sp_StatUpdate_XE_Session.sql)
ALTER EVENT SESSION [sp_StatUpdate_Monitor] ON SERVER STATE = START;Captures UPDATE STATISTICS commands, errors, lock waits, lock escalation, and long-running statements.
- 2.8.2026.0302 - Comprehensive issue sweep (31 issues resolved). New params: @IncludeIndexedViews, @LogSkippedToCommandLog, @ReturnDetailedResults, @CompletionNotifyTable. Detection: QS forced plan warning, RLS, wide stats, columnstore context, filtered index mismatch, computed columns, Stretch DB skip, log space check. Azure MI vs DB distinction. 12-topic @Help operational notes.
- 2.7.2026.0302 - AG redo queue pause (@MaxAGRedoQueueMB, @MaxAGWaitMinutes). tempdb pressure check (@MinTempdbFreeMB). New StopReasons: AG_REDO_QUEUE, TEMPDB_PRESSURE.
- 2.6.2026.0302 - Multi-database direct mode: @Statistics and @StatisticsFromTable respect @Databases.
- 2.5.2026.0302 - CommandLog index advisory, Resource Governor detection, @LockTimeout docs.
- 2.4.2026.0302 - Region markers for LLM navigation. Collation-aware comparisons (COLLATE DATABASE_DEFAULT). Per-phase timing in debug mode.
- 2.3.2026.0302 - Bug fixes: applock release on CATCH, @@TRANCOUNT check, SET ANSI_WARNINGS/ARITHABORT, HAS_DBACCESS() filter, MAXRECURSION unlimited, midnight @StopByTime crossing, @CheckPermissionsOnly, CommandLog schema check.
- 2.1.2026.0219 - Extended Awareness. Azure SQL detection, hardware context, RCSI awareness, Replication/CDC/Temporal detection. New: @MaxConsecutiveFailures, @WarningsOut, @StopReasonOut.
- 2.0.2026.0212 - Environment Intelligence. CE version/trace flag/DB-scoped config detection. Staged discovery auto-fallback. Truncated partition handling. XE session overhaul. Diagnostic tool (sp_StatUpdate_Diag).
- 1.9.2026.0206 - Status/StatusMessage columns for Agent alerting. Batch QS enrichment (O(n) to O(1)).
- 1.9.2026.0128 - @Preset parameter, @GroupByJoinPattern, ##sp_StatUpdate_Progress, @CleanupOrphanedRuns default Y.
- 1.8.2026.0128 - Code review fixes, XE troubleshooting session.
- 1.7.2026.0127 - BREAKING: @ModificationThreshold default 1000 to 5000.
- 1.6.2026.0128 - Staged discovery, adaptive sampling, @ExcludeTables, @WhatIfOutputTable.
- 1.5.2026.0120 - CRITICAL: Fixed @ExcludeStatistics filter, incremental partition targeting.
- 1.4.2026.0119 - Query Store prioritization, filtered stats handling.
- 1.3.2026.0119 - Multi-database support, OUTPUT parameters, return codes.
- 1.2.2026.0117 - Tiger Toolbox tiered thresholds, AND/OR logic, PERSIST_SAMPLE_PERCENT.
- 1.1.2026.0117 - Style refactor, @Help, parallel mode.
- 1.0.2026.0117 - Initial public release.
IndexOptimize is battle-tested and handles indexes + stats together. Use it for general maintenance.
sp_StatUpdate is for when you need:
- Priority ordering (worst stats first)
- Time-limited runs with graceful stops
- NORECOMPUTE targeting
- Query Store-driven prioritization
- Adaptive sampling for problematic stats
- AG-safe maintenance with redo queue awareness
- Programmatic access to results via OUTPUT parameters
MIT License - see LICENSE for details.
Based on patterns from Ola Hallengren's SQL Server Maintenance Solution (MIT License).
- Ola Hallengren - sp_StatUpdate wouldn't exist without his SQL Server Maintenance Solution. We use his CommandLog table, Queue patterns, and database selection syntax. If you're not already using his tools, start there.
- Brent Ozar - years of emphasizing stats over index rebuilds, First Responder Kit, and community education.
- Erik Darling - T-SQL coding style and performance insights. His diagnostic tools are excellent - I'm particularly fond of sp_LogHunter and sp_QuickieStore.
- Tiger Team's AdaptiveIndexDefrag - the 5-tier adaptive threshold formula
- Colleen Morrow - parallel statistics maintenance concept