Tu aplicación no va lenta: hace 300 consultas

Una pantalla de tu aplicación tarda cuatro segundos en cargar. Mirás el servidor: el procesador está tranquilo, la memoria sobra. Mirás la base de datos: cada consulta tarda dos milisegundos, ninguna es lenta.
Todo está bien y sin embargo tarda cuatro segundos.
Lo que está pasando, casi seguro, es que esa pantalla hace trescientas consultas de dos milisegundos. Ninguna es lenta. Todas juntas son seiscientos milisegundos de base de datos, más trescientas idas y vueltas por la red, más trescientas veces armar y desarmar el resultado.
Este problema tiene nombre, aparece en todos los lenguajes y en todos los marcos de trabajo, y una vez que sabés reconocerlo lo vas a ver por todos lados.
Cómo se ve el problema
Querés mostrar los últimos veinte artículos con el nombre de su autor. Escribís lo obvio:
const articulos = await db.articulos.buscarUltimos(20);
for (const articulo of articulos) {
articulo.autor = await db.autores.buscarPorId(articulo.autor_id);
}
Se lee bien. Hace lo que dice. Y ejecuta veintiuna consultas: una para traer los artículos, y una más por cada artículo para traer su autor.
Con veinte artículos casi no se nota. El día que la pantalla muestre doscientos, son doscientas una consultas y la página tarda tres segundos.
Se llama el problema N+1: una consulta inicial más una por cada resultado.
Por qué cuesta tanto verlo
Porque el código que lo produce es el más natural de escribir. Nadie escribe un bucle con consultas adentro a propósito: escribe “para cada artículo, traeme su autor”, que es exactamente cómo se piensa el problema.
Y porque muchas veces la consulta no está a la vista. Con una herramienta que mapea objetos a tablas, esto es lo mismo:
for (const articulo of articulos) {
console.log(articulo.autor.nombre); // ← acá hay una consulta
}
Ese .autor parece un campo del objeto. Es una consulta a la base, disfrazada de
acceso a una propiedad. Se llama carga perezosa, y es cómoda hasta que estás en
un bucle.
Cómo detectarlo en dos minutos
No hacen falta herramientas especiales. Contá las consultas.
Casi todas las bibliotecas de acceso a datos permiten registrar cada consulta. Activalo en desarrollo y cargá la pantalla:
// Ejemplo con Prisma; el equivalente existe en todas
const db = new PrismaClient({ log: ['query'] });
Después mirá el número. La pregunta que lo revela: ¿el número de consultas crece cuando hay más datos en la pantalla? Si mostrar veinte artículos hace veintiuna consultas y mostrar cuarenta hace cuarenta y una, ahí está.
Una pantalla sana hace un número de consultas fijo, sin importar cuántos elementos muestre.
Cómo se arregla
Trayendo todo junto. Hay dos formas, según lo que tengas.
Si usás una herramienta de mapeo, casi siempre hay una opción para incluir lo relacionado en la misma consulta:
// Prisma
const articulos = await db.articulo.findMany({
take: 20,
include: { autor: true }, // ← una sola consulta
});
En otras herramientas se llama distinto pero es lo mismo: includes en Rails,
Include en Entity Framework, joinedload en SQLAlchemy, select_related en
Django.
Si escribís SQL a mano, un JOIN:
SELECT a.*, au.nombre AS autor_nombre
FROM articulos a
JOIN autores au ON au.id = a.autor_id
ORDER BY a.publicado_en DESC
LIMIT 20;
De veintiuna consultas a una.
Cuando el JOIN no sirve
Hay un caso donde traer todo junto empeora las cosas: cuando cada artículo tiene
muchos elementos relacionados. Un artículo con cincuenta comentarios, en un
JOIN, se repite cincuenta veces en el resultado. Con veinte artículos son mil
filas para mostrar veinte.
Ahí la solución son dos consultas, no veintiuna:
const articulos = await db.articulos.buscarUltimos(20);
const ids = articulos.map((a) => a.id);
// Una sola consulta para TODOS los comentarios de TODOS los artículos
const comentarios = await db.comentarios.buscarPorArticulos(ids);
// Y se agrupan en memoria, que es rapidísimo
const porArticulo = new Map();
for (const c of comentarios) {
if (!porArticulo.has(c.articulo_id)) porArticulo.set(c.articulo_id, []);
porArticulo.get(c.articulo_id).push(c);
}
Dos consultas para cualquier cantidad de artículos. Este patrón se llama carga por lotes, y es lo que hacen por dentro las herramientas cuando les pedís que incluyan una relación de muchos.
Cómo evitar que vuelva
El problema reaparece siempre, porque el código que lo causa es el natural. Dos costumbres que ayudan:
Al revisar código, buscá consultas dentro de bucles. Es la señal más
confiable. Cualquier await a la base dentro de un for o un map merece una
mirada.
Poné un límite en desarrollo. Muchos marcos permiten avisar cuando una petición supera cierta cantidad de consultas:
if (consultas > 25) {
console.warn(`⚠ ${consultas} consultas en ${ruta}`);
}
Veinticinco es arbitrario. El punto no es el número exacto: es que el problema te avise mientras programás, en vez de que lo descubras cuando un usuario se queje de que la pantalla tarda.
Comentarios
Iniciá sesión para comentar y dar "me gusta".
Todavía no hay comentarios. Sé la primera persona en escribir uno.