Consultas lentas e índices en la base de datos, sin tecnicismos
Por qué una app se pone lenta con datos reales, cómo encontrar las consultas lentas en Postgres o Supabase, qué índices crear y cómo comprobar que sirvieron.
Read in English →
Cuando una app que volaba con datos de prueba se pone lenta con usuarios reales, casi siempre la causa es un puñado de consultas que leen muchas más filas de las necesarias, normalmente porque la columna por la que filtran u ordenan no tiene índice. Un índice es una estructura ordenada, como el índice al final de un libro, que permite a la base de datos ir directo a las filas que busca en vez de recorrer la tabla entera. Encontrar esas pocas consultas y crear los índices correctos suele resolver la lentitud sin tocar el resto de la app.
En esta guía te explicamos, sin dar por hecho que programas, cómo reconocer que el problema es la base de datos, dónde encontrar las consultas lentas en Postgres (incluido Supabase), qué índices agregar y cómo confirmar el arreglo.
¿Cómo sé que el problema es la base de datos?
Hay señales bastante claras:
- Las páginas se fueron poniendo lentas poco a poco a medida que crecían los datos, no de golpe tras un cambio.
- Los listados, búsquedas y paneles van lentos; las páginas estáticas, bien.
- En la pestaña Network (Red) del navegador, la petición a tu API o a Supabase se lleva casi todo el tiempo, mientras que la página en sí carga rápido.
- Les va peor a tus clientes más antiguos o más grandes, que son los que tienen más filas.
Si en cambio todo va lento, incluso páginas sin datos, mira primero el frontend. Nuestra guía de Core Web Vitals en tu app cubre ese lado.
¿Por qué se ponen lentas las consultas?
Sin índice, Postgres responde a WHERE user_id = 42 leyendo cada fila de la tabla y revisándola una por una. A eso se le llama escaneo secuencial. Con 500 filas nadie lo nota. Con cinco millones, cada visita espera.
Las causas típicas en apps hechas con Lovable, Bolt, Replit o Cursor:
- Claves foráneas sin índice. Postgres crea un índice para la clave primaria automáticamente, pero no para las columnas que apuntan a otra tabla, como
pedidos.cliente_idotareas.proyecto_id. - Políticas de Row Level Security sobre columnas sin índice. Una política como
user_id = auth.uid()se ejecuta en cada consulta. Siuser_idno tiene índice, cada petición recorre la tabla. - La misma consulta dentro de un bucle. La página trae 50 proyectos y luego hace una petición por proyecto para contar sus tareas: 51 consultas donde bastaba una (el famoso problema "N+1").
- Traer todo.
select *sobre una tabla con columnas grandes de texto o JSON, o cargar la lista completa en vez de la primera página. - Ordenar resultados grandes por una columna sin índice, por ejemplo "más recientes primero" sobre
created_at.
¿Cómo encuentro las consultas lentas?
En Supabase
- Abre el Performance Advisor dentro de la sección de Advisors de tu proyecto. Señala problemas como claves foráneas sin índice, índices duplicados y políticas de seguridad lentas.
- Con la CLI de Supabase vinculada a tu proyecto, ejecuta
supabase inspect db outlierspara ver las consultas con mayor tiempo de ejecución, ysupabase inspect db seq-scanspara saber qué tablas se leen completas más seguido. - La extensión
index_advisorsugiere índices para una consulta concreta.
En cualquier base Postgres
La extensión pg_stat_statements registra cuántas veces corre cada consulta y cuánto tarda. Ordena por tiempo total, no por la llamada más lenta: una consulta de 40 milisegundos que corre 100.000 veces al día pesa más que una de dos segundos que corre una vez por semana.
Luego toma una consulta y ejecútala con EXPLAIN ANALYZE delante. Si el plan dice Seq Scan sobre una tabla grande y el tiempo real es alto, encontraste una candidata.
explain analyze
select id, titulo, created_at
from tareas
where proyecto_id = 'a1b2c3'
order by created_at desc
limit 20;
¿Qué índices debería crear?
Parte de lo que las consultas lentas filtran, unen y ordenan.
| Patrón de consulta | Índice |
|---|---|
where proyecto_id = ? | create index on tareas (proyecto_id); |
where proyecto_id = ? order by created_at desc | create index on tareas (proyecto_id, created_at desc); |
where estado = 'abierta' sobre pocas filas | create index on tareas (proyecto_id) where estado = 'abierta'; |
Política RLS user_id = auth.uid() | create index on documentos (user_id); |
Algunas reglas prácticas:
- En un índice combinado, el orden de las columnas importa. Primero la columna que filtras por igualdad, después la que ordenas o usas en un rango.
- Los índices parciales (con
where) son más chicos y rápidos cuando las consultas siempre apuntan al mismo subconjunto, siempre que la condición de la consulta coincida con la del índice. - En una tabla de producción con mucho movimiento, usa
create index concurrently. Uncreate indexnormal bloquea las escrituras en la tabla mientras se construye. La versión concurrente tarda más pero deja que la app siga funcionando. No puede ir dentro de una transacción, así que ejecútala sola. - No indexes todo. Cada índice ocupa espacio y vuelve más lentas las inserciones y actualizaciones. Los advisors de Supabase también avisan de índices sin uso o duplicados que puedes quitar después de verificarlo.
Arregla también la política
Supabase recomienda escribir (select auth.uid()) en lugar de auth.uid() dentro de las políticas RLS, para que Postgres lo evalúe una vez por consulta y no una vez por fila. Nuestra guía de Row Level Security de Supabase muestra políticas escritas así.
¿Cómo demuestro que el índice ayudó?
- Ejecuta otra vez el mismo
EXPLAIN ANALYZE. Deberías verIndex ScanoBitmap Index Scanen vez deSeq Scan, y un tiempo de ejecución mucho menor. - Revisa la página en la pestaña Network con la misma cuenta y los mismos datos que antes.
- Mira de nuevo la lista de consultas más pesadas tras un día de tráfico real. La consulta debería haber bajado.
Si el plan sigue mostrando un escaneo secuencial, puede que la consulta no coincida con el índice (otro orden de columnas, una función alrededor de la columna, tipos distintos) o que la tabla sea tan chica que a Postgres le conviene leerla entera. En tablas pequeñas eso es normal.
¿Y si los índices no alcanzan?
A veces hay que cambiar la consulta: reemplazar el bucle por una sola consulta con un join o un count ... group by, paginar, o precalcular un total que cada panel recalcula. Si la base simplemente se queda sin memoria o CPU en las horas pico, un tamaño de cómputo mayor ayuda, pero después de los índices, no en su lugar. Lo que se rompe cuando crece el tráfico lo vemos en escalar de 100 a 10.000 usuarios.
Respuestas rápidas
¿Agregar un índice puede romper mi app? No. Los índices no cambian los resultados, solo la velocidad. El riesgo es bloquear una tabla con mucho uso mientras se crea, y create index concurrently lo evita.
¿Cuántos índices son demasiados? No hay un número fijo. Si un índice nunca aparece en los planes ni en las estadísticas de uso, te está costando escrituras a cambio de nada.
¿Le puedo pedir los índices a la herramienta de IA? Sí, pero dale la consulta lenta exacta y revisa el resultado con EXPLAIN ANALYZE. Los índices creados sin una consulta real muchas veces no coinciden con cómo la app lee los datos.
¿Necesito un plan de base de datos más grande? Quizás más adelante. Sin los índices correctos, cualquier plan parece chico.
¿Quieres que alguien las encuentre por ti?
Si tu app se puso lenta y quieres otra mirada, nuestro equipo de mantenimiento de software puede revisar tus consultas más lentas y crear los índices adecuados sin riesgos, y en proyectos de Supabase la revisión de seguridad de Supabase revisa tus políticas al mismo tiempo. También puedes escribirnos contando qué se siente lento. La guía de optimización de consultas de Supabase es una buena lectura para seguir.

