SQL Server

Get index size and usage

Had a question about the usage of some indexes. Ended up with this script to document my findings.

Cl

Claus Munch

Jan 28, 2026 ยท 1 min read

6 views

I had a team reach out to me, asking if we could clean-up some indexes on a table.
This is the script I ended up with, to get and document the usage of the indexes, making it visible to the team what indexes could be removed or should be kept.

-- Index usage statistics with size information
SELECT 
    OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName,
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    
    -- Size metrics
    ps.[row_count] AS [RowCount],
    (ps.used_page_count * 8.0) / 1048576.0 AS IndexSizeGB,  -- 8KB pages -> GB
    (ps.reserved_page_count * 8.0) / 1048576.0 AS ReservedSizeGB,
    
    -- Usage stats
    ISNULL(ius.user_seeks, 0) AS UserSeeks,
    ISNULL(ius.user_scans, 0) AS UserScans,
    ISNULL(ius.user_lookups, 0) AS UserLookups,
    ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0) + ISNULL(ius.user_lookups, 0) AS TotalReads,
    ISNULL(ius.user_updates, 0) AS UserUpdates,
    
    -- Read/Update Ratio
    CASE 
        WHEN ISNULL(ius.user_updates, 0) = 0 THEN NULL
        ELSE CAST((ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0) + ISNULL(ius.user_lookups, 0)) AS DECIMAL(18,6)) 
             / CAST(ius.user_updates AS DECIMAL(18,6))
    END AS ReadUpdateRatio,
    
    ius.last_user_seek AS LastSeek,
    ius.last_user_scan AS LastScan,
    
    -- Index columns
    STUFF((
        SELECT ', ' + c.name + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END
        FROM sys.index_columns ic
        INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
        WHERE ic.object_id = i.object_id 
          AND ic.index_id = i.index_id
          AND ic.is_included_column = 0
        ORDER BY ic.key_ordinal
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS KeyColumns,
    
    -- Included columns
    STUFF((
        SELECT ', ' + c.name
        FROM sys.index_columns ic
        INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
        WHERE ic.object_id = i.object_id 
          AND ic.index_id = i.index_id
          AND ic.is_included_column = 1
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS IncludedColumns

FROM sys.indexes i
INNER JOIN sys.dm_db_partition_stats ps 
    ON i.object_id = ps.object_id 
    AND i.index_id = ps.index_id
LEFT JOIN sys.dm_db_index_usage_stats ius 
    ON i.object_id = ius.object_id 
    AND i.index_id = ius.index_id
    AND ius.database_id = DB_ID()
INNER JOIN sys.objects o
    ON i.object_id = o.object_id AND o.type = 'U'
--WHERE i.object_id = OBJECT_ID('dbo.Posts')
--  AND i.name IN (
--      'IX_Creation_date',
--  )
ORDER BY 
    OBJECT_SCHEMA_NAME(i.object_id), 
    OBJECT_NAME(i.object_id), 
    (ps.used_page_count * 8) / 1024.0 DESC;

Ended up picking some of the datapoints and send an email, with a table much like this:

Index

Total Reads

Read/Update Ratio

Assessment

Size (GB)

IDX_TransferActivities_Internal

10,652,331

1.00

Heavily used

309

IX_Creation_date

51

0.000005

Rarely used

240

IDX_TransferActivities_ExternalTransactionIdentifier

23

0.000002

Rarely used

148

Making it easy for the team to have an idea of the index usage.

This could be extended to be gathered over time (eg. with RedGate or a homemade setup) to more accurately depict the usage, as this is just since the last database startup.

Share this article:

Related Articles