top of page

When Statistics Stay Empty Forever: A Hidden Edge Case with Empty Tables

Many SQL Server DBAs rely on automated statistics maintenance solutions such as Ola Hallengren’s maintenance scripts. These scripts typically update statistics only when the modification counter exceeds a threshold.


But there is a corner case that can cause statistics to remain empty forever, and many DBAs are not aware of it.


I certainly wasn’t. And I would like to thank Nirit Leibovitch for teaching me this lesson.


When Statistics Stay Empty Forever


The Scenario


Consider the following simple sequence:


  1. Create a table.

  2. Create an index on it while the table is empty.

  3. Insert data later.

  4. Disable AUTO_UPDATE_STATISTICS and rely on a scheduled maintenance job (like Ola’s).


Example:

CREATE TABLE
	dbo.TestTable
(
	Id		INT	NOT NULL ,
	Value	INT	NOT NULL
);

CREATE INDEX
	IX_TestTable_Value
ON
	dbo.TestTable (Value ASC);

Since the table is empty, SQL Server creates the statistics object, but the histogram is empty.


This is expected.


Now insert data:

INSERT INTO
	dbo.TestTable
SELECT
	Id		= ROW_NUMBER () OVER (ORDER BY (SELECT NULL) ASC) ,
	Value	= ABS (CHECKSUM (NEWID ()))
FROM
	GENERATE_SERIES (1 , 100000 , 1);

Now inspect the statistics metadata:

SELECT
	StatsId				= Stats.stats_id ,
	StatsName			= Stats.name ,
	LastUpdated			= StatsProperties.last_updated ,
	NumberOfRows		= StatsProperties.rows ,
	ModificationCounter	= StatsProperties.modification_counter
FROM
	sys.stats AS Stats
CROSS APPLY
	sys.dm_db_stats_properties (Stats.object_id , Stats.stats_id) AS StatsProperties
WHERE
	Stats.object_id = OBJECT_ID(N'dbo.TestTable');

You might expect to see a modification counter reflecting the inserted rows.

Instead, you will see something surprising:

LastUpdated           NULL
NumberOfRows          NULL
ModificationCounter   NULL


The Problem


Because the table was empty when the statistics were created, SQL Server did not create a statistics blob (the internal structure that stores the histogram and density vector).


Without this blob:


  • last_updated is NULL

  • rows is NULL

  • modification_counter is also NULL


And here is the critical point:


The modification counter will remain NULL no matter how many rows are inserted or modified later.


Even if you insert millions or billions of rows, the statistics metadata stays NULL.


The statistics only become initialized after the first statistics update.


For example:

UPDATE STATISTICS
	dbo.TestTable IX_TestTable_Value;

After this command, the DMV suddenly reports valid metadata and a histogram is created.



Why This Matters


Many DBAs disable AUTO_UPDATE_STATISTICS and instead rely on scheduled maintenance jobs.


A very common example is Ola Hallengren’s IndexOptimize procedure, which updates statistics based on the modification counter.


Internally, it relies on:

sys.dm_db_stats_properties.modification_counter

But since the value is NULL, the script effectively treats it as 0.


Which means:

modification_counter < threshold

So the statistics never qualify for an update.


The result:


  • The statistics object exists

  • The histogram does not

  • The metadata remains NULL

  • The maintenance job never updates it


And the statistics can remain in this state indefinitely.



Why Auto-Update Statistics Usually Masks the Problem


If AUTO_UPDATE_STATISTICS is enabled, SQL Server will eventually create the histogram when a query compilation detects stale statistics.


This means the issue usually resolves itself automatically.


However, if a DBA disables auto-update statistics and relies solely on scheduled maintenance based on the modification counter, the statistics may never be initialized.



Detecting the Problem


You can detect such statistics easily:

SELECT
	SchemaName	= SCHEMA_NAME (Tables.schema_id) ,
	TableName	= Tables.name ,
	StatsName	= Stats.name ,
	LastUpdated	= StatsProperties.last_updated
FROM
	sys.tables AS Tables
INNER JOIN
	sys.stats AS Stats
ON
	Tables.object_id = Stats.object_id
OUTER APPLY
	sys.dm_db_stats_properties (Stats.object_id , Stats.stats_id) AS StatsProperties
WHERE
	StatsProperties.last_updated IS NULL;

These statistics objects exist but have never been built.



Possible Solutions


1. Treat NULL modification counters as "needs update"


Maintenance scripts should treat NULL as unknown, not zero.


2. Force an initial statistics update


Run:

UPDATE STATISTICS <table>;

after the first data load.


3. Initialize stats during deployment


If tables are created empty but expected to grow later, a post-deployment step can initialize statistics.



Final Thoughts


This behavior is not widely known, but it can lead to statistics remaining uninitialized forever in environments where:


  • AUTO_UPDATE_STATISTICS is disabled

  • Statistics maintenance relies on modification counters

  • Tables and indexes are created empty and populated later


It’s a small edge case, but one that can quietly affect query optimization until someone notices that the histogram simply does not exist.

Comments


STAY IN TOUCH

Get New posts delivered straight to your inbox

Thank you for subscribing!

bottom of page