SQL Query to show tables, their indexes and index usage
The SQL query below shows a list of tables sorted by the largest tables first. For each table, the indexes in the table are shown. For each index it shows when the index was last used. The query is designed to collect various pieces of information in one place, and give an overview to help supporting decisions when doing index tuning.
The left hand columns show data for the table (No. of receords, data- and index size) to have a view of the impact of having indexes on the table.
The right hand columns show data for each index, including Updates (costs) and Reads (benefits) and when it was last used sine the last time SQL Server was restarted.
Further comments:
- The query only works on SQL Server 2005 and later.
- The numbers in the query are reset every time SQL Server restarts.
- The query may take up to a few minutes to run.
- The query is provided "as is", with no warranties and confers no rights. You assume all risk for your use.
If you have comments or feedback, please feel free to post them here.
Best regards
Lars Lohndorf-Larsen
Microsoft Dynamics UK
Microsoft Customer Service and Support (CSS) EMEA
--use NavisionDatabase
IF
OBJECT_ID ('z_IUQ2_Temp_Index_Keys', 'U') IS NOT NULL
DROP
TABLE z_IUQ2_Temp_Index_Keys;
-- Generate list of indexes with key list
create
table z_IUQ2_Temp_Index_Keys(
[l1] [int]
NOT NULL,
[F_Obj_ID] [int]
NOT NULL,
[F_Schema_Name] [nvarchar]
(128) NULL,
[F_Table_Name] [nvarchar]
(128) NOT NULL,
[F_Row_Count] [int]
NULL,
[F_Reserved] [int]
NULL,
[F_Data] [int]
NULL,
[F_Index_Size] [int]
NULL,
[F_UnUsed] [int]
NULL,
[F_Index_Name] [nvarchar]
(128) NOT NULL,
[F_Index_ID] [int]
NOT NULL,
[F_Column_Name] [nvarchar]
(128) NOT NULL,
[F_User_Updates] [int]
NULL,
[F_User_Reads] [int]
NULL,
[F_Last_Used] [datetime]
NULL,
[F_Index_Type] [nvarchar]
(128) NOT NULL,
[F_Index_Column_ID] [int]
NOT NULL,
[F_Last_Seek] [datetime]
NULL,
[F_Last_Scan] [datetime]
NULL,
[F_Last_Lookup] [datetime]
NULL,
[Index_Key_List] [nvarchar]
(MAX) NULL
)
go
CREATE
NONCLUSTERED INDEX [Object_ID_Index] ON [dbo].[z_IUQ2_Temp_Index_Keys]
(
[F_Obj_ID] ASC
)
go
CREATE
NONCLUSTERED INDEX [Index_ID_Index] ON [dbo].[z_IUQ2_Temp_Index_Keys]
(
[F_Index_ID] ASC
)
go
CREATE
NONCLUSTERED INDEX [RowCount_ID_Index] ON [dbo].[z_IUQ2_Temp_Index_Keys]
(
[F_Row_Count] ASC
)
go
insert
into
z_IUQ2_Temp_Index_Keys
SELECT
(row_number() over(order by a3.name, a2.name))%2 as l1,
a1
.object_id, ---
a3
.name AS [schemaname],
a2
.name AS [tablename],
a1
.rows as row_count,
(a1.reserved + ISNULL(a4.reserved,0))* 8 AS reserved,
a1
.data * 8 AS data,
(CASE WHEN (a1.used + ISNULL(a4.used,0)) > a1.data THEN (a1.used + ISNULL(a4.used,0)) - a1.data ELSE 0 END) * 8 AS index_size,
(CASE WHEN (a1.reserved + ISNULL(a4.reserved,0)) > a1.used THEN (a1.reserved + ISNULL(a4.reserved,0)) - a1.used ELSE 0 END) * 8 AS unused,
-- Index Description
SI
.name,
SI
.Index_ID,
index_col
(object_name(SIC.object_id),SIC.index_id,SIC.Index_Column_ID),
-- Index Stats
US
.user_updates,
US
.user_seeks + US.user_scans + US.user_lookups User_Reads,
case
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,
SIC
.index_column_id,
US
.last_user_seek,
US
.last_user_scan,
US
.last_user_lookup,
''
FROM
(SELECT
ps
.object_id,
SUM (
CASE
WHEN (ps.index_id < 2) THEN row_count
ELSE 0
END
) AS [rows],
SUM (ps.reserved_page_count) AS reserved,
SUM (
CASE
WHEN (ps.index_id < 2) THEN (ps.in_row_data_page_count + ps.lob_used_page_count + ps.row_overflow_used_page_count)
ELSE (ps.lob_used_page_count + ps.row_overflow_used_page_count)
END
) AS data,
SUM (ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
GROUP BY ps.object_id) AS a1
LEFT
OUTER JOIN
(SELECT
it
.parent_id,
SUM(ps.reserved_page_count) AS reserved,
SUM(ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
INNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id)
WHERE it.internal_type IN (202,204)
GROUP BY it.parent_id) AS a4 ON (a4.parent_id = a1.object_id)
INNER
JOIN sys.all_objects a2 ON ( a1.object_id = a2.object_id )
INNER
JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id)
inner
join sys.indexes SI ON (SI.object_id = a1."object_id") ---
inner
join sys.index_columns SIC ON (SIC.object_id = SI.object_id and SIC.index_id = SI.index_id)
left
outer join sys.dm_db_index_usage_stats US ON (US.object_id = SI.object_id and US.index_id = SI.index_id)
WHERE
a2.type <> N'S' and a2.type <> N'IT'
order
by row_count desc
go
-- Populate key string
declare
IndexCursor cursor for
select
F_Obj_ID, F_Index_ID from z_IUQ2_Temp_Index_Keys
for
update of Index_Key_List
declare
@objID int
declare
@IndID int
declare
@KeyString VARCHAR(MAX)
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,'') + F_Column_Name + ', '
from
z_IUQ2_Temp_Index_Keys
where
F_Obj_ID = @ObjID and F_Index_ID = @IndID
ORDER
BY F_Index_ID, F_Index_Column_ID
set
@KeyString = LEFT(@KeyString,LEN(@KeyString) -2)
update
z_IUQ2_Temp_Index_Keys
set
Index_Key_List = @KeyString
where
current of IndexCursor
fetch
next from IndexCursor into @ObjID, @IndID
end
;
close
IndexCursor
deallocate
IndexCursor
go
-- clean up table to one line per index
delete
from z_IUQ2_Temp_Index_Keys
where
[F_Index_Column_ID] > 1
go
select
[F_Table_Name] TableName
,
[F_Row_Count] No_Of_Records
,
[F_Data] Data_Size
,
[F_Index_Size] Index_Size
,
[F_UnUsed] UnUsed_Space
,
[F_Index_Name] Index_Name
,
[F_User_Updates] Index_Updates
,
[F_User_Reads] Index_Reads
,
[F_Last_Used] Index_Last_Used
,
[F_Index_Type] Index_Type
,
[Index_Key_List] Index_Fields
from
z_IUQ2_Temp_Index_Keys
order
by F_Row_Count desc, F_Table_Name, [F_Index_ID]
Comments
Anonymous
June 10, 2009
Great post - thanks again Lars. However; I tend to get 2 entries for several indexes, where the only differences are these colums: "Index_Updates" and "Index_Reads" and "Index_Last_Used". Do you have any explanation for these entries? (I'll be happy to send you my spreadsheet if you like)Anonymous
June 14, 2009
Thank you! And yes, please send me your spreadsheet - I guess it's nothing to do with tables where DataPerCompany = No? LarsAnonymous
April 04, 2011
There is a problem if you are using "Included Columns". There is also a problem with schema names.