Index Bloat in Postgres: Why It Matters, How to Identify, How to Resolve
Index bloat in Postgres can cause problems, but it’s easy to miss.
I’ve written about how vacuum problems can prevent PostgreSQL from …
Read Moreon • 6 min read
SQL Server’s “index usage stats” dynamic management view is incredibly useful– but does it tell you what you THINK it tells you?
I explain the quirks of how sys.dm_db_index_usage_stats works and why the information is so valuable.
Why does the sys.dm_db_index_usage_stats dynamic management view increment the user_updates value even when you have a where clause on a given index that would result in no change to indexed values?
Sincerely,
Going Insane with Index Usage Stats
sys.dm_db_index_usage_stats is a dynamic management view that reports the number of times an index is used by queries for reads or writes.
The “user_updates” column is described in books online as:
Number of updates by user queries. This includes Insert, Delete and Updates representing number of operations done not the actual rows affected. For example, if you delete 1000 rows in one statement, this count will increment by 1
Source: https://msdn.microsoft.com/en-us/library/ms188755.aspx
There are also columns for:
These are all named “user” to indicate that they aren’t behind the scenes system queries, which have their own columns in the view.
Books online said, “For example, if you delete 1000 rows in one statement, [the count in the user_updates column] will increment by 1
“Going Insane” noticed that if your delete query deletes zero rows, it will still increment the user_updates column value by 1.
This is totally true, and it’s easy to reproduce like this:
Sometimes operators in query plans don’t actually get used at runtime.
For example, the nested loop join operator looks up a value in an “inner” table/index for every row that comes back from the “outer” table/index in the join. If zero rows comes back from the “outer” table/index, the SQL Server will never go and access the “inner” table/index.
But user_seeks will still be incremented by 1, even if the index wasn’t actually used at runtime by that query.
When we learn about seeks vs scans, we tend to think of seeks as being super efficient and looking at just a few rows, and scans as reading all the rows in the object.
It turns out this isn’t true. You can have a seek operator that reads all the rows in an index for a query. And you can have a scan operator that reads just a few rows.
Let’s say your query uses a TOP. You may get a scan operator in your plan that feeds into the top. And the query may be so efficient that it quickly finds enough rows to satisfy the TOP requirement, and at that point the scan can just stop.
It may only read a tiny number of pages in a huge index, but still user_scans is incremented by 1.
Let’s say you do run a single query that updates 1,000 rows. Like books online says, user_updates will increase by 1.
If you roll back that transaction, the value stays as is. It shows the same value in user_updates as if the transaction committed successfully.
The misunderstanding here is about what’s doing the “using”. I totally get this, I had the same misunderstanding myself.
Our inclination is to think the DMV answers the question, “Did I use the rows in this index?”
Instead, it answers the question, “Did I run a query with an operator that could do something with this index?”
It’s counting the number of times an operator shows up in a query plan, and categorizing it by type.
It’s not checking if the operator is actually executed on individual runs, if it does a “full” scan, or if the transaction is rolled back.
Although the DMV is higher level than it appears, it still points to the indexes that get used the most
It can also point you to indexes that haven’t been used at all– at least since the last time usage stats were reset for that index
The DMV is really useful for quick, high level insight into what indexes are most popular, and which you should put on a list to monitor, and perhaps eventually drop
Sample uses:
Index usage stats is always reset when a database goes offline (that includes restarting the SQL Server or failing the database over)
Dropping an index or CREATE with DROP_EXISTING will also reset usage stats
In SQL Server 2012, a bug occurred where index rebuilds started resetting index usage stats
The issue is specific to ALTER INDEX REBUILD (not a reorganize command)
That has been fixed in:
More detail: /2016/03/07/sql-server-2016-rc0-fixes-index-usage-stats-bug-missing-indexes-still-broken/
If you want specific data about how many writes occur in an index, you have a few options:
Tracing with SQLTrace or Extended events: more impact on the instance, but more complete info
Looking at sys.dm_exec_query_stats for queries that reference the index: lightweight, but may miss information for queries whose plans aren’t in cache
There is a DMV, sys.dm_db_index_operational_stats that includes more granular information about writes
It’s sensitive to memory pressure, though. Data goes away when metadata for that index is no longer in memory
“How many writes since when?” is super hard to answer with this DMV
If you’re putting in a lot of time on answering the question, you’re better off with sys.dm_exec_query_stats and friends, in my experience
This is worth your time investment! Indexes are critical to performance.
Stick with it and don’t sweat the small stuff.
Copyright (c) 2025, Catalyze SQL, LLC; all rights reserved. Opinions expressed on this site are solely those of Kendra Little of Catalyze SQL, LLC. Content policy: Short excerpts of blog posts (3 sentences) may be republished, but longer excerpts and artwork cannot be shared without explicit permission.