Index Usage Query

This is a follow up from an earlier blog "Finding Index usage". In that blog, I described a very simple way to list how indexes are being used. In this blog, the query is much extended so that it now shows your Navision keys, listed by either number of updates, or by their cost divided by their usage, and it shows when an index was last used for reading. The idea is to show a list of indexes that are being maintained, but never or rarely being used.

The query uses SQL Server Dynamic Managament Views (DMW), which means it will only work for for SQL Server 2005 and later.

Feel free to add comments to this blog about how useful (or not) this query is. And about any problems you may find, and suggestions to improve it. All comments will be welcome!


To use it, copy the query below into SQL Server Management Studio. Remember to set the database to your Microsoft Dynamics NAV database (not Master or any other database). Then run it. Depending on the size of your database, it may take a few minutes to run it. First time you run it, I would recommend that you do it when the SQL Server is not otherwise busy, until you konw how long it takes:


-- use NavisionDB

IF OBJECT_ID ('z_IUQ_Temp_Index_Keys', 'U') IS NOT NULL

DROP TABLE z_IUQ_Temp_Index_Keys;

IF OBJECT_ID ('zIUQ_Temp_Index_Usage', 'U') IS NOT NULL

DROP TABLE zIUQ_Temp_Index_Usage

-- Generate list of indexes with key list

create table z_IUQ_Temp_Index_Keys(

[F_Obj_ID] [int] NOT NULL,

[F_Obj_Name] [nvarchar] (128) NULL,

[F_Ind_ID] [int] NOT NULL,

[Index_Column_ID] [int] NOT NULL,

[Index_Key] [nvarchar] (128) NULL,

[Index_Key_List] [nvarchar] (MAX) NULL,







Insert into z_IUQ_Temp_Index_Keys








from sys.index_columns


-- populate key string

declare IndexCursor cursor FOR

select F_Obj_ID, F_Ind_ID from z_IUQ_Temp_Index_Keys

FOR UPDATE of Index_Key_List

declare @ObjID int

declare @IndID int


set @KeyString = NULL

open IndexCursor

set nocount on

fetch next from IndexCursor into @ObjID, @IndID

while @@fetch_status = 0 begin

SET @KeyString = ''

SELECT @KeyString = COALESCE(@KeyString, '') + Index_Key + ', '

FROM z_IUQ_Temp_Index_Keys

where F_Obj_ID = @ObjID and F_Ind_ID = @IndID

ORDER BY F_Ind_ID, Index_Column_ID

SET @KeyString = LEFT(@KeyString,LEN(@KeyString) - 2)

update z_IUQ_Temp_Index_Keys

set Index_Key_List = @KeyString

where current of IndexCursor

fetch next from IndexCursor into @ObjID, @IndID


close IndexCursor;

deallocate IndexCursor;

-- Generate list of Index usage

create table zIUQ_Temp_Index_Usage(

[F_Table_Name] [nvarchar](128) NOT NULL,

[F_Ind_ID] [int] NOT NULL,

[F_Index_Name] [nvarchar](128) NULL,

[No_Of_Updates] [int] NULL,

[User_Reads] [int] NULL,

[Last_Used_For_Reads] [datetime] NULL,

[Index_Type] [nvarchar](56) NOT NULL,

[last_user_seek] [datetime] NULL,

[last_user_scan] [datetime] NULL,

[last_user_lookup] [datetime] NULL,

[Index_Keys] [nvarchar] (255) NULL


insert into zIUQ_Temp_Index_Usage


object_name(US.object_id) Table_Name,

US.index_id Index_ID, Index_Name,

US.user_updates No_Of_Updates,

US.user_seeks + US.user_scans + US.user_lookups User_Reads,


when (ISNULL(US.last_user_seek,'00:00:00.000') >= ISNULL(US.last_user_scan,'00:00:00.000')) and (ISNULL(US.last_user_seek,'00:00:00.000') >= ISNULL(US.last_user_lookup,'00:00:00.000')) then US.last_user_seek

when (ISNULL(US.last_user_scan,'00:00:00.000') >= ISNULL(US.last_user_seek,'00:00:00.000')) and (ISNULL(US.last_user_scan,'00:00:00.000') >= ISNULL(US.last_user_lookup,'00:00:00.000')) then US.last_user_scan

else US.last_user_lookup

end as Last_Used_For_Reads,

SI.type_desc Index_Type,





from sys.dm_db_index_usage_stats US, sys.indexes SI where SI.object_id = US.object_id and SI.index_id = US.index_id

order by No_Of_Updates desc


-- Select and join the two tables.


TIU.F_Table_Name Table_Name,

--TIU.F_Ind_ID Index_ID,

--TIU.F_Index_Name Index_Name,





when TIU.User_Reads = 0 then TIU.No_Of_Updates

else TIU.No_Of_Updates / TIU.User_Reads

end as Cost_Benefit,



from zIUQ_Temp_Index_Usage TIU, z_IUQ_Temp_Index_Keys TIK where TIK.F_Obj_Name = TIU.F_Table_Name and TIK.F_Ind_ID = TIU.F_Ind_ID and TIK.Index_Column_ID = 1

and TIU.F_Table_Name not in ('zIUQ_Temp_Index_Usage','z_IUQ_Temp_Index_Keys')

order by No_Of_Updates desc

--order by Cost_Benefit desc




The query will show you one line for each index in the SQL Database. It shows you the table name, and a list of fields in the index. Note that any non-clustered index also contain the clustered index. For example on SQL Server, the key "Document No." in the "Cus. Ledger Entry table" is "Document No.","Entry No.". Also note that the indexes shown by SQL Server is not always shown in the same order as you have defined them in NAV.

The column "No_Of_Updates" basically shows you the cost of this index, since every update requires a lock as well as a write to the database. The next column, "User_Reads", shows you how often this index has been used, either from the UI, or by C/AL code. Compare these two, and you have way to compare the cost against the benefits of each index, as shown in the column "Cost_Benefit", which is simply "No_Of_Updates" / "User_Reads". The column "Last_Used_For_Reads" shows you when an index was actually used for reading.

The query sorts the indexes by "No_Of_Updates", with the most updated (most costly) index first. At the last line of the query you can change the sorting to "order by Cost_Benefit desc", and you are likely to see a different picture.

Finally, the query shows you whether each index is clustered or non-clustered.


The query will create two new tables called "z_IUQ_Temp_Index_Keys" and "zIUQ_Temp_Index_Usage". Although highly unlikely, if you already have tables with these names in your database, then the query will overwrite those without warnings. These tables collect index usage statistics, so if you need to run the query again, for example because you lost the results, or wat to run it with a different sorting, you don't have to run the whole query. Just run the last part of the query - from the section  "-- Select and join the two tables.", and it will run much faster. Only after you change indexes, or want an updated view of index usage, you need to run the whole query again.


The data shown by the query is reset every time SQL Server restarts. So if you have recently restarted SQL Server, then the query may not show you the most precise picture of how the indexes are being used over time. Also consider that some indexes may only ever be used for example at end of the month / end of fiscal year, etc. So just because the query shows that a certain index was not used since SQL Server was last restarted, then this index may still be required for specific jobs.



Lars Lohndorf-Larsen

Escalation Engineer

These postings are provided "AS IS" with no warranties and confer no rights. You assume all risk for your use.

Comment List