Cómo una columna sin índice mató nuestra base de datos
Un solo índice faltante convirtió una API de 8 ms en una pesadilla de 8 segundos.
Realizamos una prueba de carga antes de un lanzamiento importante. El desarrollo local funcionaba bien con 100 registros. Luego simulamos 500 usuarios contra 1,5 millones de filas con un volumen de datos de producción.
Los resultados fueron malos:
- Los tiempos de respuesta de la API saltaron de 45 ms a 8.000 ms.
- El uso de CPU de la base de datos alcanzó el 100 %.
- Los pools de conexiones se agotaron, provocando errores de tiempo de espera (timeout).
Revisamos el log de consultas lentas y encontramos un endpoint que recuperaba el historial de pedidos por ID de usuario y estado.
Al ejecutar EXPLAIN ANALYZE en PostgreSQL, se mostró un Sequential Scan. El motor leyó las 1,5 millones de filas en cada solicitud porque no había un índice en user_id.
Con 100 solicitudes concurrentes, escaneó 150 millones de filas a la vez.
La solución tomó cinco minutos.
En lugar de un simple índice en user_id, creamos un índice compuesto en (user_id, status, created_at DESC). Eso permitió que la base de datos:
- Filtrara por
user_id. - Filtrara por
status. - Devolviera las filas más recientes de inmediato.
- Se saltara pasos de ordenamiento adicionales.
Usamos CREATE INDEX CONCURRENTLY para que la tabla permaneciera desbloqueada durante la operación.
Después de la solución:
- El tiempo de consulta bajó de 8.150 ms a 0,14 ms.
- La latencia de la API cayó de 8 segundos a 12 ms.
- El uso de CPU bajó del 100 % a menos del 8 %.
Lecciones aprendidas:
- Las pruebas locales son engañosas; 100 filas no representan a un millón.
- Indexa tus claves foráneas; la mayoría de los ORM omiten esto.
- Ejecuta
EXPLAIN ANALYZEantes de comprar más hardware. - Diseña los índices en función de las consultas que realmente ejecutas.
No escales los servidores primero. Escala las consultas.
