¿Qué es el problema de consultas N+1?

Actualizado: agosto de 2026

El problema de consultas N+1 es un patrón donde leer N registros dispara una consulta extra por registro — N+1 idas a la base en lugar de una o dos. Es el bug de performance serio más común en aplicaciones respaldadas por datos, y el mejor camuflado: cada query individual es rápida, el código se lee perfecto y la página funciona — justo hasta que llegan los tamaños de datos reales.

Puntos clave

PreguntaRespuesta
La forma1 query para la lista + N queries por relaciones = N+1 round trips
La causaLazy loading tocado dentro de un loop — queries invisibles por iteración
La firmaMuchas queries idénticas y rápidas — los logs de queries lentas nunca la ven
Las salidasEager loading · joins · IN en batch · loaders por solicitud
El multiplicadorLatencia de round trip × tamaño de página — brutal sobre la red

El bug, a plena vista

// 1 query: trae 100 posts
const posts = await postRepo.findRecent(100);

for (const post of posts) {
  // +1 query POR POST — post.author parece una propiedad,
  // pero corre lazy loading: SELECT * FROM users WHERE id = ?
  render(post.title, (await post.author).name);
}
// Log de queries: 1 + 100 = 101 idas a la base para un solo render de página

El arreglo, como lo expresan los SDKs — declara la relación por adelantado y la plataforma la trae en la misma solicitud:

// JavaScript / Node.js — Back4app JS SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
const query = new Parse.Query('Comment');
query.equalTo('post', post);
query.include('author');                    // eager-load the pointer
const comments = await query.find();        // one round trip, total

comments.forEach((c) =>
  render(c.get('text'), c.get('author').get('username')) // already loaded
);

El costo, multiplicado

La aritmética que los explicadores se saltan — tiempo total ≈ query de lista + (N × latencia de round trip):

Tamaño de página N@1 ms/query@5 ms/queryEn batch (1–2 queries)
10~11 ms~55 ms~10 ms
100~101 ms~505 ms~12 ms
1.000~1,0 s~5,0 s~20 ms

Los arreglos publicados coinciden con la matemática: páginas cayendo de 1,4 s a 0,16 s, endpoints acelerándose 30× y más. Dos corolarios que vale la pena fijar: la paginación acota N — un tamaño de página limitado acota el radio de daño incluso antes del arreglo real; y la versión de red es peor — cuando cada una de las N llamadas es una solicitud HTTP y no una query local, multiplica por decenas de milisegundos en vez de un solo dígito.

Las cuatro salidas

Cascada N+1 frente a lectura en batchEl patrón N+1 emite una consulta de lista seguida de una consulta por registro, en secuencia; el patrón en batch emite la consulta de lista y una sola consulta que trae todos los registros relacionados de una vez.

el arreglo

En batch

Query de lista

UNA query para todas las relaciones
WHERE id IN (…)

Cascada N+1

Query de lista

Query por registro
× N, una tras otra

El patrón N+1 emite una consulta de lista seguida de una consulta por registro, en secuencia; el patrón en batch emite la consulta de lista y una sola consulta que trae todos los registros relacionados de una vez.
ArregloCómoÚsalo cuandoCuidado con
Eager loading / includeDeclara las relaciones en la queryLa pantalla siempre necesita la relaciónIncluir de más infla los payloads
JoinUna sentencia trae ambosMotor relacional, lecturas tipo reporteExplosión de filas en joins anchos
IN en batchJunta las claves, trae una vezCualquier stack, incluso a manoUn round trip extra (aceptable)
Loader por solicitudAuto-batch durante la ejecuciónResolvers de GraphQL, código en capasDebe ser por solicitud, no global

La última fila es el caso famoso de GraphQL: los resolvers se disparan por objeto padre, recreando el loop del lado del servidor, y el patrón loader — juntar las claves en un tick, traer una vez, memoizar por solicitud — es la cura estándar de la industria. Una regla sutil viaja con él: los loaders tienen scope de solicitud; uno global se convierte en una caché vieja con bugs de autorización.

Detección: caza las queries rápidas

La firma del N+1 está invertida respecto del trabajo normal de performance: buscas muchas queries idénticas y rápidas, no una lenta — y por eso los logs de queries lentas, la herramienta habitual, nunca lo ven. Los métodos que sí: el logging de debug del ORM (la misma sentencia, N parámetros distintos, un solo render); las vistas de spans del APM, donde la cascada de spans cortos idénticos es inconfundible; y el más barato de todos — cuenta las queries de una carga de página en desarrollo, con un umbral en la cabeza: una página de lista debería costar queries de un solo dígito, no múltiplos de sus filas. El gemelo del lado de escritura también merece su auditoría: un loop de insert-por-ítem es N+1 de escrituras, arreglado con operaciones bulk.

Casos de uso comunes

  • Vistas de lista con autores, dueños o estados — el hogar canónico: cada feed, bandeja de entrada y tabla que une personas con ítems.
  • APIs GraphQL — los campos de lista anidados son estructuralmente N+1 hasta que existen loaders.
  • Bases de documentos — un find por documento referenciado; se arregla con include, IN en batch o embebiendo, según las reglas de modelado.
  • Fan-outs de microservicios — una lista del servicio A, una llamada HTTP al servicio B por ítem; los endpoints compuestos y los BFFs existen para terminar con esto.
  • Jobs en segundo plano — el loop que procesa 10.000 registros con dos queries cada uno, costando horas en silencio.

Lazy loading vs. eager loading: matriz de decisión

Carga eager cuando…Quédate lazy cuando…
La relación se renderiza en cada filaLa relación está detrás de un clic
El loop es el patrón de accesoEl acceso es de a un registro a la vez
N es del tamaño de una página o másN es garantizado diminuto
La latencia es de cara al usuarioUna tarea de fondo puede permitirse la espera
Acabas de arreglar este bug aquíMediste, en lugar de suponer

Y la regla de auditoría permanente que sobrevive a cualquier matriz: toda relación tocada dentro de un loop es culpable hasta que el log de queries demuestre lo contrario.

Limitaciones y trade-offs

  • El eager loading puede sobrecorregir. Incluir relaciones pesadas en todas partes cambia N+1 por payloads inflados y joins anchos — incluye lo que la pantalla renderiza, no el grafo completo.
  • Los joins tienen su propio precipicio. Los joins uno-a-muchos duplican las filas del padre por cada hijo; con fan-out alto, la lectura en batch de dos queries le gana al join único.
  • Los loaders agregan maquinaria. El scope por solicitud, la invalidación de caché dentro de la solicitud y las ventanas de batching son código real — el precio del batching automático.
  • Los frameworks lo reintroducen en silencio. Los serializers, los helpers de plantillas y los reviews de “solo un campo más” son la forma en que las páginas arregladas recaen; el chequeo de conteo de queries pertenece al CI, no a la memoria.
  • El arreglo es por ruta, no global. N+1 es un bug de patrón de acceso; cada pantalla nueva vuelve a hacer la pregunta.

El N+1 en Back4app

Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Sus SDKs hacen del arreglo el idioma y no la remediación: las relaciones son Pointers tipados, e include() — las pestañas de código de arriba — las trae en la misma solicitud, en batch del lado del servidor. La API GraphQL resuelve queries anidadas sin fan-out por campo, y los defaults de paginación acotan N antes de que saque los dientes. El loop que causa el N+1 no tiene una forma natural de escribirse — que es la mejor clase de arreglo: el que nadie tiene que recordar.

Preguntas frecuentes

¿Qué es el problema de consultas N+1, en términos simples?

Traes una lista de N registros con una consulta, y luego tu código hace una query más por registro para sus datos relacionados — cien posts se vuelven ciento una queries. Cada query es individualmente rápida, y exactamente por eso el problema se esconde: nada es lo bastante lento como para alarmar a nadie, hasta que la página hace cientos de round trips.

¿Qué causa las consultas N+1?

El lazy loading como default. Los ORMs y SDKs te dejan navegar relaciones como propiedades de objeto — post.author — y corren una query de forma transparente cuando tocas una. Pon ese acceso dentro de un loop sobre N resultados y habrás escrito N queries en silencio. La abstracción que hizo agradable el acceso a datos también volvió invisibles las idas a la base.

¿Cómo se arregla el problema N+1?

Cuatro salidas estándar, a elegir por caso: eager loading — dile a la query desde el inicio que incluya la relación; un join que trae ambos en una sola sentencia; batching — junta las N claves foráneas y tráelas en una sola query de contenido-en; y loaders con scope de solicitud que agrupan automáticamente. Las cuatro convierten N+1 round trips en uno o dos.

¿Qué tan lento es N+1 en realidad?

Multiplícalo: cada ida a la base cuesta de uno a cinco milisegundos antes de que la query siquiera corra, así que 100 filas a 5 ms agregan medio segundo frente a una sola query en batch de ~10 ms. Arreglos reales publicados reportan páginas pasando de cerca de 1,4 segundos a 0,16, y endpoints de API acelerándose treinta veces. La matemática empeora linealmente con el tamaño de página — y catastróficamente sobre la red.

¿Por qué el problema N+1 es tan común en GraphQL?

Porque los resolvers corren por campo, por objeto. Una query de posts con sus autores corre el resolver de posts una vez y el de author N veces — el loop del ORM renacido del lado del servidor. La cura canónica es el patrón loader: un batcher con scope de solicitud que junta los IDs de autor durante la ejecución y emite una sola lectura en batch, memoizada para la solicitud.

¿Cómo se detectan las consultas N+1?

Busca muchas queries idénticas y rápidas, no una lenta — esa es la firma. El logging de debug del ORM muestra la sentencia repetida con parámetros distintos; los logs de queries lentas se lo pierden por completo porque cada query es veloz; las herramientas de APM marcan el patrón de spans explícitamente. El hábito que lo atrapa temprano: lee el log de queries de un solo render de página, y cuenta.

¿El problema N+1 ocurre en bases de documentos y APIs REST?

En todos lados donde los datos tienen relaciones. En almacenes de documentos es un find por documento referenciado — arreglado con mecanismos include, queries de contenido-en agrupadas o embebiendo. Sobre REST es un endpoint de lista más una llamada HTTP por ítem — peor que la versión de base de datos, porque la latencia de red empequeñece la latencia de query; los endpoints compuestos, los endpoints batch y los lenguajes de consulta existen en gran parte para matarlo.

¿El lazy loading siempre está mal?

No — está mal dentro de loops. El lazy loading es exactamente correcto cuando los datos relacionados rara vez se necesitan: paga por ellos en el único registro que los necesita, en lugar de pagar eager por los N. La disciplina es saber qué patrón de acceso tiene cada pantalla: las relaciones siempre necesarias se cargan eager, las raramente necesarias lazy, y cualquier cosa dentro de un loop se audita.

Términos relacionados

Compara con

Lecturas recomendadas

¿Listo para construir tu backend?

Empieza tu proyecto en Back4app en minutos — base de datos, autenticación, APIs y Cloud Code incluidos. Sin tarjeta de crédito.

Escrito y revisado por Back4app Engineering, Back4app Engineering · Publicado el 2026-09-01