Question: How do you find outdated statistics?

Answer: Inspect when a statistics object was last updated, its sampling and relevant modifications, then relate them to the query’s estimates. Its age in days alone is not a reliable stale-statistics test.
An attendee asked me during a performance-tuning webcast whether we could find all outdated statistics by their dates. A date is an easy field to sort; deciding whether the information misrepresents your data is the real task. Statistics can become stale, but an old date doesn’t automatically make them wrong, and a recent date doesn’t guarantee good estimates for your query.
Read the Statistics Properties
This inventory returns one row per statistics object, including index statistics, and avoids duplicate rows from joining every partition:
USE AdventureWorks2025;
SELECT SCHEMA_NAME(o.schema_id) AS SchemaName,
o.name AS TableName, s.name AS StatisticsName,
COL_NAME(s.object_id, sc.column_id) AS LeadingColumn,
p.last_updated, p.rows, p.rows_sampled, p.modification_counter,
CAST(100.0 * p.modification_counter / NULLIF(p.rows,0)
AS decimal(12,2)) AS ModificationPct,
s.auto_created, s.user_created, s.no_recompute
FROM sys.stats AS s
JOIN sys.objects AS o ON o.object_id = s.object_id AND o.type = 'U'
JOIN sys.stats_columns AS sc ON sc.object_id = s.object_id
AND sc.stats_id = s.stats_id AND sc.stats_column_id = 1
OUTER APPLY sys.dm_db_stats_properties(s.object_id,s.stats_id) AS p
ORDER BY SchemaName, TableName, StatisticsName;last_updated records the statistics update, not an index’s last rebuild. rows and rows_sampled help interpret the snapshot and sampling. For disk-based tables, modification_counter concerns changes to the leading statistics column, not a count of distinct rows that changed exactly once. ModificationPct is an investigation aid, not a universal update threshold.
An empty or filtered-empty statistic can have NULL properties or no statistics blob. Lack of permissions can also limit visibility. Keep those cases visible and investigate them rather than pretending a missing date means “fresh.” Memory-optimized statistics have different counter behavior.
The Automatic Threshold
You’ll still see the rule “500 rows plus 20 percent” for larger tables. SQL Server 2016 and later use a dynamic threshold at compatibility level 130 or higher. It is the smaller of 500 + 0.20 × rows and the square root of 1000 × rows. These are engine triggers, not promises that every important distribution change waits for that percentage.
Auto-create and auto-update statistics are different settings. Creating missing statistics is not the same operation as refreshing an existing statistic. I usually want both enabled, but that doesn’t make investigation unnecessary. A few new high values, skew, filtered statistics or an important recent load can still deserve attention.
Choose the Update Based on Evidence
The database-wide command is:
EXEC sys.sp_updatestats;That’s a maintenance operation, not part of the read-only inventory. A targeted UPDATE STATISTICS may be more appropriate when you have identified a specific object’s estimates or sampling problem. Updating statistics can change compilation and plans; it doesn’t guarantee faster execution.
Look at estimated versus actual rows where the bad choice begins, inspect the relevant statistic, and measure the query after a justified update. If the problem is an expression, correlation or a missing index, refreshing a histogram alone may leave the real cause untouched.

An old statistic is not automatically a wrong one, it is a candidate, and the query’s estimates tell you which ones matter.
Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.
Discover more from SQL Authority with Pinal Dave
Subscribe to get the latest posts sent to your email.





16 Comments. Leave new
Pinal, can you address the complications of SQL Server in a Sharepoint environment? It’s my understanding that both auto-create and auto-update statistics should be turned OFF.
I also heard the same from other clients as well. Looks like this is Microsoft’s recommendation.
Produces “Invalid object name ‘sys.dm_db_stats_properties’. error on our SQL Server 2008R2
This query works with SQL Server 2012 and onwards.
The query works on SQL Server 2008 R2(SP3)
The logic of 20% you described is valid only until 2014. Since 2016 the threshold depends on the number of rows in the table.
Because the trace flag 2371 has been promoted to default behavior in SQL 2016 and cannot be disabled: with 2008, if you want Sql Server adjusts dynamically the threshold to the number of rows in the table to determine when statistics are old, you have to enable such trace flag and set AUTO_UPDATE_STATISTICS_ASYNC ON on table.
Pinal, This doesn’t appear to work on SQL Server 2016. I’m getting a “invalid object name ‘sys.dm_db_stats_properties’. Do you have a fix for this?
Thanks for everything you do for us!
I have Microsoft SQL Server 2016 (SP1-CU3) (KB4019916) – 13.0.4435.0 (X64) Enterprise Edition (64-bit) and the query works just fine. Besides – as I wrote in a previous reply – it works in 2008 SP3 version too. I suspect you have a RTM version. Paste output of query ‘SELECT @@VERSION’ here to check. bye
What is the history of this database? Any errors in applying any service pack /patch earlier? These objects should be part of resource database.
I have a table with partition by date, this table has around 5B records and 20M records insert every day.
This is good idea to update statistics only for the last 2 partitions? or we should also run update statstistcs on whole table?
Thanks
You can use the Incremental Statistics feature available from 2014 version of SQL server.
I have several databases on Azure on 1 SQL Server and in an Elastic Pool.
By default ‘Auto Create Incremental Statistics’ is ‘False’. Also ‘Auto Update Statistics Async’ is ‘False’.
Should I enable it on my databases?
I can’t find any conclusive anwers anywhere.
This script does not show the statistics that were created when indexes were created on tables or view.
Nome de objeto ‘sys.dm_db_stats_properties’ inválido.
I have Microsoft SQL Server 2016 SP2 (X64) Enterprise Edition (64-bit) and the query works fine too.