Índices: la diferencia entre 3 segundos y 30 milisegundos

Un índice bien puesto puede acelerar mil veces una consulta. Diez índices mal puestos pueden hacer más lenta toda tu base.

Bases de datos · Rendimiento

La misma consulta, la misma máquina, los mismos datos. Sin índice tarda 3.2 segundos y lee cuatro millones de filas para devolver treinta y siete. Con el índice correcto tarda 31 milisegundos y lee cinco páginas. No hay optimización de hardware que produzca esa diferencia; hay una estructura de datos bien elegida.

Y sin embargo, los índices son también una de las formas más eficientes de degradar una base de datos. Cada índice que agregas se paga en cada inserción, cada actualización y cada borrado, para siempre. Una tabla con doce índices y tres consultas distintas está pagando nueve impuestos que no le sirven a nadie.

Esta guía cubre lo que hay que entender para tomar buenas decisiones: cómo leer un plan sin ser especialista, la regla de orden de columnas que decide si un índice sirve o no, cuándo conviene cubrir una consulta, y cómo encontrar los índices que solo cuestan.

Sumario

  1. 01Qué cambia realmente un índice
  2. 02Leer un plan sin ser especialista
  3. 03El orden de las columnas
  4. 04Índices que cubren la consulta
  5. 05Lo que impide usar un índice
  6. 06El costo de escritura
  7. 07Duplicados y redundantes
  8. 08Los que nadie lee

Qué cambia realmente un índice

Sin índice, el motor recorre la tabla completa y descarta lo que no cumple. Con el índice adecuado, salta directo al rango de filas que importan. Esto es lo que se ve en el plan de ejecución de una consulta real sobre una tabla de cuatro millones de pedidos:

Antes y después · el mismo SELECT
SELECT id, total, fecha
FROM   pedidos
WHERE  cliente_id = 8841 AND estatus = 'ABIERTO';
 
─── SIN ÍNDICE ─────────────────────────────────────────
|--Clustered Index Scan (pedidos)
       filas leídas ......... 4,120,000
       filas devueltas ..............  37
       lecturas lógicas ....... 38,410
       tiempo .................. 3,180 ms
 
─── CON IX_pedidos_cliente_estatus ─────────────────────
|--Index Seek (IX_pedidos_cliente_estatus)
       filas leídas ................. 37
       filas devueltas .............. 37
       lecturas lógicas ............... 5
       tiempo ...................... 31 ms
El número que hay que mirar

No el tiempo: las lecturas lógicas. El tiempo varía con la carga del servidor y con lo que ya esté en caché; las lecturas lógicas son estables y comparables entre ejecuciones. Cuando optimices, mide ahí. Si las lecturas bajaron de 38,410 a 5, mejoraste de verdad, aunque el reloj diga otra cosa ese día.

Leer un plan sin ser especialista

No hace falta dominar los treinta operadores. Con reconocer seis se resuelve la enorme mayoría de los casos.

OperadorQué está pasando¿Preocupa?
Index SeekVa directo al rango de filas por el índiceNo. Es lo que buscas
Index ScanRecorre el índice completo, pero al menos es más chico que la tablaA veces. Revisa si falta una columna en el índice
Clustered Index Scan
o Table Scan
Recorre la tabla enteraSí, en tablas grandes. Casi siempre falta un índice
Key Lookup
o RID Lookup
Encontró la fila por el índice y ahora va a la tabla por las columnas que faltanSí, si se repite muchas veces. Se resuelve cubriendo
SortEstá ordenando en memoria porque el índice no venía ordenado como se pidióSí, si es costoso. Un índice con el orden correcto lo elimina
Hash MatchUne conjuntos grandes construyendo una tabla temporalNormal en reportes; sospechoso en consultas que devuelven pocas filas

En SQL Server el plan se pide con SET STATISTICS IO, TIME ON y el plan real desde el cliente; en PostgreSQL con EXPLAIN (ANALYZE, BUFFERS); en MySQL con EXPLAIN ANALYZE. Los nombres cambian, los conceptos son los mismos.

1

El orden de las columnas lo decide todo

Un índice compuesto se usa de izquierda a derecha, como el directorio telefónico: ordenado por apellido y luego por nombre. Puedes buscar por apellido, o por apellido y nombre. Pero buscar solo por nombre te obliga a leerlo completo.

La regla práctica: primero las columnas comparadas por igualdad, después la de rango, y al final las que solo aparecen en el ORDER BY. El índice deja de aprovecharse en cuanto encuentra el primer rango.

La consulta y su índice correcto
SELECT id, total, cliente_id
FROM   pedidos
WHERE  sucursal_id = 12               -- igualdad
  AND  estatus     = 'ABIERTO'        -- igualdad
  AND  fecha      >= '2028-06-01'     -- rango
ORDER BY fecha DESC;
 
-- BIEN: igualdades primero, rango al final
CREATE INDEX IX_pedidos_suc_est_fecha
    ON pedidos (sucursal_id, estatus, fecha);
 
-- MAL: el rango va primero y bloquea el resto
CREATE INDEX IX_malo
    ON pedidos (fecha, sucursal_id, estatus);
Consecuencia útil

Un índice sobre (sucursal_id, estatus, fecha) también sirve para consultas que filtran solo por sucursal_id, o por sucursal_id y estatus. Por eso casi siempre puedes consolidar tres índices en uno bien ordenado, en lugar de crear uno por consulta.

2

Índices que cubren la consulta

El índice encontró las 37 filas, pero la consulta pide columnas que el índice no tiene. El motor tiene entonces que ir a la tabla una vez por cada fila: eso es el Key Lookup. Con 37 filas no se nota; con 40,000 es el cuello de botella entero.

La solución es agregar esas columnas al índice como incluidas: viajan con el índice pero no forman parte de la llave, así que no lo hacen más lento de buscar.

Índice de cobertura
CREATE INDEX IX_pedidos_suc_est_fecha
    ON pedidos (sucursal_id, estatus, fecha)   -- llave: para buscar y ordenar
    INCLUDE (total, cliente_id, vendedor_id);  -- incluidas: para no ir a la tabla
 
-- PostgreSQL 11+ soporta la misma sintaxis INCLUDE.
-- En MySQL/InnoDB no existe INCLUDE: se agregan a la llave,
-- con el costo de tamaño que eso implica.
Hasta dónde cubrir

Es tentador incluir todas las columnas de la tabla y acabar con una copia completa que hay que mantener en cada escritura. Cubre las consultas frecuentes y de pocas columnas. Si una consulta necesita quince columnas, probablemente el Key Lookup no sea el problema real.

3

Lo que impide usar un índice que sí existe

Este es el hallazgo más frustrante y más común: el índice está, es el correcto, y la consulta no lo usa. Casi siempre porque algo en el WHERE impide comparar directamente contra la columna indexada.

Cuatro formas de anular tu propio índice
-- 1. Función sobre la columna
WHERE YEAR(fecha) = 2028                         ✗ recorre todo
WHERE fecha >= '2028-01-01' AND fecha < '2029-01-01'   
 
-- 2. Conversión de tipo
WHERE CONVERT(varchar, cliente_id) = '8841'      
WHERE cliente_id = 8841                          
 
-- 3. Comodín al inicio
WHERE nombre LIKE '%lopez%'                      
WHERE nombre LIKE 'lopez%'                       
 
-- 4. Cálculo del lado de la columna
WHERE total * 1.16 > 5000                        
WHERE total > 5000 / 1.16                        
La trampa silenciosa

La conversión implícita entre varchar y nvarchar, o entre texto y número, no genera error ni advertencia: solo hace que el índice deje de usarse. Es habitual cuando el ORM manda parámetros con un tipo distinto al de la columna. Si un índice «no se usa sin razón», revisa los tipos primero.

Cuando la función es inevitable

Si de verdad necesitas filtrar por una expresión, la mayoría de los motores permiten indexarla: columnas calculadas persistidas e indexadas en SQL Server, índices sobre expresiones en PostgreSQL. Es mejor que reescribir media aplicación.

4

El costo que nadie mira: escribir

Cada índice es una estructura adicional que el motor debe mantener sincronizada. Un INSERT en una tabla con ocho índices no escribe una vez: escribe nueve. Un UPDATE que toca una columna indexada tiene que reacomodar esa entrada, y si la fila cambia de posición, mover más de lo que parece.

Por eso la pregunta correcta antes de crear un índice no es «¿ayuda a esta consulta?» sino «¿ayuda lo suficiente para justificar el impuesto que le cobra a todas las escrituras de esta tabla?».

Referencia práctica

En tablas muy transaccionales, más de cinco o seis índices no agrupados empieza a ser señal de revisión —no una prohibición, una señal—. En tablas de consulta o histórico puedes tener más sin problema, porque casi no se escriben. Y siempre revisa el reporte de escrituras contra lecturas antes de agregar uno más.

5

Duplicados y redundantes

Aparecen solos con el tiempo: alguien agrega un índice para una consulta sin revisar los existentes, y a los tres años la tabla tiene cuatro variantes del mismo. Un índice sobre (cliente_id) es completamente redundante si ya existe uno sobre (cliente_id, fecha), porque el segundo cubre todo lo que hacía el primero.

Listar índices con sus columnas clave
SELECT OBJECT_NAME(i.object_id) AS tabla,
       i.name                    AS indice,
       STUFF((SELECT ', ' + c.name
              FROM   sys.index_columns ic
              JOIN   sys.columns c
                     ON  c.object_id = ic.object_id
                     AND c.column_id = ic.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('')), 1, 2, '')  AS columnas_clave
FROM   sys.indexes i
WHERE  i.type_desc = 'NONCLUSTERED'
  AND  OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY tabla, columnas_clave;
Qué buscar en el resultado

Ordenado así, los redundantes quedan juntos y saltan a la vista: cliente_id junto a cliente_id, fecha junto a cliente_id, fecha, estatus. El de más columnas suele absorber a los otros dos. Antes de eliminarlos, verifica que ninguno sea único ni respalde una restricción.

6

Los índices que nadie lee

El motor lleva la cuenta de cuántas veces se usó cada índice para leer y cuántas veces hubo que actualizarlo. Un índice con cero lecturas y doscientas mil escrituras es puro costo.

Índices que solo cuestan
SELECT OBJECT_NAME(i.object_id)  AS tabla,
       i.name                     AS indice,
       ISNULL(us.user_seeks,0) + ISNULL(us.user_scans,0)
       + ISNULL(us.user_lookups,0) AS lecturas,
       ISNULL(us.user_updates,0)   AS escrituras
FROM   sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats us
       ON  i.object_id = us.object_id
       AND i.index_id  = us.index_id
       AND us.database_id = DB_ID()
WHERE  i.type_desc = 'NONCLUSTERED'
  AND  OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY lecturas ASC, escrituras DESC;
Antes de borrar, verifica el periodo

Estas estadísticas se reinician cuando reinicia el servicio. Si el servidor se reinició la semana pasada, un índice con cero lecturas puede ser el que usa el cierre mensual. Confirma desde cuándo se acumulan y, ante la duda, deshabilita el índice en lugar de eliminarlo: si algo se rompe, se rehabilita en segundos; si lo borraste, hay que reconstruirlo y en tablas grandes eso duele.

Método para trabajar una tabla

Revisión de índices, tabla por tabla

Imprimir o guardar como PDF
  • Identificar las 5 consultas más frecuentes o costosas contra esa tabla sys.dm_exec_query_stats ordenado por costo total
  • Capturar el plan de cada una y anotar las lecturas lógicas SET STATISTICS IO ON · es tu línea base
  • Marcar los recorridos completos, los Key Lookup repetidos y los Sort costosos Son los tres síntomas que un índice resuelve
  • Listar los índices existentes con sus columnas clave, ordenados alfabéticamente Los redundantes quedan juntos y se ven solos
  • Diseñar el conjunto mínimo: igualdades, luego rango, luego orden Consolidar, no agregar uno por consulta
  • Agregar columnas incluidas solo donde el Key Lookup sea realmente costoso Cubrir de más se paga en cada escritura
  • Deshabilitar —no borrar— los índices sin lecturas Y esperar un ciclo completo de negocio, incluido el cierre mensual
  • Volver a medir las mismas consultas y comparar lecturas lógicas Sin el antes y el después, no sabes si mejoraste
  • Verificar que las escrituras no se degradaron Tiempo de los INSERT y UPDATE más frecuentes de esa tabla

Trabaja una tabla a la vez y en un ambiente de pruebas con volumen realista. Optimizar contra una tabla de mil filas produce conclusiones que no se sostienen en producción.

El error de aplicar las sugerencias tal cual

Todos los motores sugieren índices, y la tentación de aplicar la lista completa es fuerte. No lo hagas. Esas sugerencias se generan consulta por consulta, sin considerar las demás, sin ver los índices ya existentes y sin contar el costo de escritura. Aplicarlas todas es la forma más rápida de terminar con veinte índices donde cabían cinco.

Úsalas como lo que son: una pista de qué columnas le hicieron falta al optimizador. El diseño lo haces tú, consolidando.

Si estás llegando aquí porque una instancia va lenta y todavía no sabes si el problema son los índices, empieza por el diagnóstico de los ocho puntos: te dice en veinte minutos hacia dónde mirar.

Un índice es una apuesta: pagas en cada escritura para cobrar en cada lectura. Vale la pena solo si sabes cuántas veces vas a cobrar.

CS
Consultoría SysAdmin

¿Tienes una tabla con doce índices y nadie sabe cuáles sirven?

Hacemos la revisión completa: capturamos las consultas reales, medimos la línea base, consolidamos el conjunto de índices al mínimo necesario y verificamos que las escrituras no se degradaron. Te entregamos el antes y el después en lecturas lógicas, que es la única medida que no miente.

  • Optimización de índices y consultas
  • Diagnóstico de rendimiento en SQL Server y PostgreSQL
  • Mantenimiento gestionado de bases de datos
  • Observabilidad para aplicaciones .NET
Diagnóstico inicial sin costo sobre una de tus tablas críticas. Te entregamos los hallazgos aunque no nos contrates.