Cómo hacer tus bases de datos más pequeñas y rápidas: encuentra los índices sin uso

Los índices son una de las herramientas de rendimiento más potentes en SQL Server — y una de las fuentes de sobrecarga más ignoradas. Cada índice que creas debe mantenerse en cada operación INSERT, UPDATE y DELETE. Los índices sin uso consumen espacio en disco, tiempo de copia de seguridad, memoria y rendimiento de escritura, sin proporcionar ningún beneficio de lectura. Encontrarlos y eliminarlos es una tarea habitual de DBA con un alto impacto.

Cómo rastrea SQL Server el uso de los índices

SQL Server llena SYS.DM_DB_INDEX_USAGE_STATS con una fila por cada índice la primera vez que se accede a él tras el reinicio de la instancia. Los contadores registran búsquedas, exploraciones, búsquedas de clave y actualizaciones. Implicaciones clave:

  • Los índices que no aparecen en la vista nunca se han utilizado desde el último reinicio.
  • Los índices listados con contadores de búsqueda/exploración/lookup en cero se han mantenido (hubo escrituras) pero nunca se han leído.
  • Los contadores se reinician en cada reinicio de la instancia — ejecuta estas consultas tras un período representativo de carga de trabajo normal, idealmente varias semanas o más.

Importante: consulta siempre los hallazgos con el equipo de desarrollo antes de eliminar cualquier cosa. Algunos índices solo se usan en procesos por lotes, informes de fin de mes o funcionalidades que no están activas durante todo el año.

Consulta 1: Índices nunca utilizados (no presentes en la DMV)

-- Indexes that have never been used since the last instance restart
DECLARE @dbid INT
SELECT @dbid = DB_ID(DB_NAME())

SELECT
    OBJECTNAME = OBJECT_NAME(I.OBJECT_ID),
    INDEXNAME  = I.NAME,
    I.INDEX_ID
FROM SYS.INDEXES I
JOIN SYS.OBJECTS O ON I.OBJECT_ID = O.OBJECT_ID
WHERE OBJECTPROPERTY(O.OBJECT_ID, 'IsUserTable') = 1
  AND I.INDEX_ID NOT IN (
        SELECT S.INDEX_ID
        FROM SYS.DM_DB_INDEX_USAGE_STATS S
        WHERE S.OBJECT_ID = I.OBJECT_ID
          AND I.INDEX_ID  = S.INDEX_ID
          AND DATABASE_ID = @dbid
  )
ORDER BY OBJECTNAME, I.INDEX_ID, INDEXNAME;

Consulta 2: Índices existentes con actividad de lectura nula

-- Indexes present in the DMV but with zero seek/scan/lookup activity
DECLARE @dbid INT
SELECT @dbid = DB_ID(DB_NAME())

SELECT u.*
FROM [sys].[indexes] i
JOIN [sys].[objects] o ON i.OBJECT_ID = o.OBJECT_ID
LEFT JOIN [sys].[dm_db_index_usage_stats] u
    ON  i.OBJECT_ID  = u.OBJECT_ID
    AND i.[index_id] = u.[index_id]
    AND u.[database_id] = @dbid
WHERE o.[type] <> 'S'
  AND i.[type_desc] <> 'HEAP'
  AND u.[user_seeks] + u.[user_scans] + u.[user_lookups] = 0
ORDER BY i.name;

Los candidatos identificados por estas consultas son un punto de partida para el análisis, no una lista de eliminación inmediata. Compáralos con los planes de consulta, las notas de versión de la aplicación y los calendarios de trabajos programados antes de realizar cualquier cambio.