In customer meetings, I am often asked about the formula that Microsoft SQL Server uses to automatically update statistics. I always have to look it up myself because this formula depends on a number of factors. For this reason, I decided to develop my own user-defined inline function, which I install in the customer databases. This UDF then gives me the threshold for all statistics when these statistics are automatically updated. I hope that this function is useful for you too.

When is an automatic update performed?

When determining automatic update of statistics follows a calculation formula that depends on three factors:

SQL Server BEFORE 2016 and no TraceFlag 2371 enabled

500 + (rows * 0.2)

SQL Server BEFORE 2016 and TraceFlag 2371 enabled

MIN(500 + (rows * 0.2), SQRT(1000 * rows)

If the old formula returns a smaller number than the new formula than this value is the threshold for SQL Server for the automatic update.

If the new formula returns a smaller number than the new formula than this value is the threshold for SQL Server for the automatic update.

SQL Server >= 2016

Same calculation as above!

Code

CREATE OR ALTER FUNCTION dbo.statistics_update
(
	@object_id		INT,
	@stats_id		INT,
	@tf2371_enabled	BIT = 1
)
RETURNS TABLE
AS
RETURN
(
	SELECT	s.name							AS	schema_name,
			t.name							AS	table_name,
			st.name							AS	statistics_name,
			st.no_recompute					AS	no_recompute,
			CASE WHEN db.is_auto_update_stats_on = 1
					THEN
						CASE WHEN st.no_recompute = 1
							 THEN 'no automatic update'
							 ELSE 'updated automatic '
						END +
						CASE WHEN db.is_auto_update_stats_async_on = 1
							 THEN 
								CASE WHEN st.no_recompute = 0
									 THEN 'asynchronously'
									 ELSE 'synchronous'
								END
						ELSE ''
					END
				 ELSE 'no automatic update'
			END								AS	automatic_update_status,
			dsp.stats_id,
			dsp.last_updated,
			dsp.rows,
			dsp.rows_sampled,
			dsp.steps,
			dsp.unfiltered_rows,
			dsp.modification_counter,
			CAST
			(
				/* The TF is obsolet since Microsoft SQL Server 2016 */
				CASE WHEN db.sql_server_version < 13 AND @tf2371_enabled = 0
					 THEN 500 + (0.20 * dsp.rows)
					 ELSE CASE WHEN 500 + (0.20 * dsp.rows) < SQRT(1000 * dsp.rows)
							   THEN 500 + (0.20 * dsp.rows)
							   ELSE SQRT(1000 * dsp.rows)
						  END
				END AS BIGINT
			)								AS	required_update_rows,
			FORMAT
			(
				dsp.modification_counter /
				CASE WHEN db.sql_server_version < 13 AND @tf2371_enabled = 0
						THEN 500 + (0.20 * dsp.rows)
						ELSE CASE WHEN 500 + (0.20 * dsp.rows) < SQRT(1000 * dsp.rows)
								THEN 500 + (0.20 * dsp.rows)
								ELSE SQRT(1000 * dsp.rows)
							END
				END,
				'#0.00%'
			)			AS update_counter_percentage
	FROM	sys.schemas AS s INNER JOIN sys.tables AS t
			ON (s.schema_id = t.schema_id)
			INNER JOIN sys.stats AS st
			ON (t.object_id = st.object_id)
			CROSS APPLY sys.dm_db_stats_properties(st.object_id, st.stats_id) AS dsp
			CROSS APPLY
			(
				SELECT	CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS sql_server_version,
						is_auto_update_stats_on,
						is_auto_update_stats_async_on
				FROM	sys.databases
				WHERE	name = DB_NAME()
			) AS db
	WHERE	(
				@object_id IS NULL
				OR st.object_id = @object_id
			)
			AND
			(
				@stats_id IS NULL
				OR st.stats_id = @stats_id
			)
);
GO

Notes

  • The function must be installed in the database in which the statistics are to be analyzed
  • It is advisable to import the function into the model database so that it is automatically available in all new databases
  • Unfortunately, activated trace flags cannot be read. For this reason, there is a parameter in the function that specifies whether trace flag 2371 is activated.
  • This parameter is ignored if the version of Microsoft SQL Server is >= 2016.
  • The default value for the parameter is 1

Example

SELECT	su.schema_name,
        su.table_name,
        su.statistics_name,
        su.no_recompute,
        su.automatic_update_status,
        su.stats_id,
        su.last_updated,
        su.rows,
        su.rows_sampled,
        su.steps,
        su.unfiltered_rows,
        su.modification_counter,
        su.required_update_rows,
        su.update_counter_percentage
FROM	sys.tables AS t
		INNER JOIN sys.stats AS st
		ON (t.object_id = st.object_id)
		CROSS APPLY info.statistics_update(st.object_id, st.stats_id, DEFAULT) AS su
ORDER BY
		su.schema_name,
		su.table_name,
		su.stats_id;

To make the result easier to read, I have divided the presentation into two figures.

First part of the row set
Second part of the row set
  • If [no_recompute] is active the [automatic_update_status] will be „no automatic update“
  • If the database option „Auto Update Statistics“ is false the [automatic_update_status] will be „no automatic update“
  • If the database option „“Auto Update Statistics Asynchronously“ is true, the [automatic_update_status] will be [automatic update asynchronously“
  • The [modification_counter] returns the actual number of changes for the statistics object
  • The [required_update_rows] returns the threshold value when automatic update of statistics will start.
  • in [update_counter_percentage] you the the actual percentage to reach the threshold

Thank you for reading!