---
term: 'El Problema de Consultas N+1'
seoTitle: 'El Problema de Consultas N+1: Causas, Detección y Soluciones'
headline: '¿Qué es el problema de consultas N+1?'
slug: problema-n-mas-1
category: api-realtime
shortDefinition: '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.'
relatedTerms:
  - relational-queries-document-databases
  - graphql-vs-rest
  - overfetching-underfetching
  - api-payload-optimization
  - database-index
contrastsWith:
  - overfetching-underfetching
faq:
  - question: '¿Qué es el problema de consultas N+1, en términos simples?'
    answer: '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.'
  - question: '¿Qué causa las consultas N+1?'
    answer: '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.'
  - question: '¿Cómo se arregla el problema N+1?'
    answer: '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.'
  - question: '¿Qué tan lento es N+1 en realidad?'
    answer: '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.'
  - question: '¿Por qué el problema N+1 es tan común en GraphQL?'
    answer: '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.'
  - question: '¿Cómo se detectan las consultas N+1?'
    answer: '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.'
  - question: '¿El problema N+1 ocurre en bases de documentos y APIs REST?'
    answer: '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.'
  - question: '¿El lazy loading siempre está mal?'
    answer: '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.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'DataLoader pattern — graphql-js documentation'
    url: 'https://www.graphql-js.org/docs/n1-dataloader/'
  - name: 'Solving the N+1 problem for GraphQL through batching — Shopify Engineering'
    url: 'https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching'
  - name: 'N+1 queries — Sentry performance issue documentation'
    url: 'https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/'
  - name: 'SDK query documentation (include)'
    url: 'https://docs.parseplatform.org/js/guide/#relational-data'
cta:
  title: 'Una solicitud donde otros hacen cien'
  text: 'Los SDKs de Back4app hacen del arreglo el idioma por defecto: include() trae las relaciones en la misma solicitud, en batch del lado del servidor, con pointers que mantienen los joins baratos. El loop N+1 simplemente no tiene razón de existir en tu codebase.'
  linkText: 'Empieza gratis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-09-01'
translationKey: n-plus-one-query-problem
---

**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

| Pregunta | Respuesta |
| --- | --- |
| La forma | 1 query para la lista + N queries por relaciones = N+1 round trips |
| La causa | Lazy loading tocado dentro de un loop — queries invisibles por iteración |
| La firma | Muchas queries *idénticas y rápidas* — los logs de queries lentas nunca la ven |
| Las salidas | Eager loading · joins · IN en batch · loaders por solicitud |
| El multiplicador | Latencia de round trip × tamaño de página — brutal sobre la red |

## El bug, a plena vista

```javascript
// 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:**

```javascript
// 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
);
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
final query = QueryBuilder<ParseObject>(ParseObject('Comment'))
  ..whereEqualTo('post', post.toPointer())
  ..includeObject(['author']);              // eager-load the pointer
final response = await query.query();       // one round trip, total
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
let query = Comment.query("post" == post)
  .include("author")                        // eager-load the pointer
query.find { result in
  if case .success(let comments) = result { // one round trip, total
    comments.forEach { render($0.text, $0.author?.username) }
  }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
val query = ParseQuery.getQuery<ParseObject>("Comment")
query.whereEqualTo("post", post)
query.include("author")                     // eager-load the pointer
query.findInBackground { comments, e ->     // one round trip, total
  if (e == null) comments.forEach {
    render(it.getString("text"), it.getParseObject("author")?.getString("username"))
  }
}
```

## 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/query | En 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

```mermaid
flowchart LR
  accTitle: Cascada N+1 frente a lectura en batch
  accDescr: 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.
  subgraph P["Cascada N+1"]
    a["Query de lista"] --> b["Query por registro<br/>× N, una tras otra"]
  end
  subgraph B["En batch"]
    c["Query de lista"] --> d["UNA query para todas las relaciones<br/>WHERE id IN (…)"]
  end
  P -.->|"el arreglo"| B
```

| Arreglo | Cómo | Úsalo cuando | Cuidado con |
| --- | --- | --- | --- |
| Eager loading / include | Declara las relaciones en la query | La pantalla siempre necesita la relación | Incluir de más infla los payloads |
| Join | Una sentencia trae ambos | Motor relacional, lecturas tipo reporte | Explosión de filas en joins anchos |
| IN en batch | Junta las claves, trae una vez | Cualquier stack, incluso a mano | Un round trip extra (aceptable) |
| Loader por solicitud | Auto-batch durante la ejecución | Resolvers de GraphQL, código en capas | Debe 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](https://www.graphql-js.org/docs/n1-dataloader/) — juntar las claves en un tick, traer una vez, memoizar por solicitud — es la [cura estándar de la industria](https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching). 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](https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/). 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](/glossary/es/joins-en-bases-de-documentos/).
- **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 fila | La relación está detrás de un clic |
| El loop es el patrón de acceso | El acceso es de a un registro a la vez |
| N es del tamaño de una página o más | N es garantizado diminuto |
| La latencia es de cara al usuario | Una 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()`](https://docs.parseplatform.org/js/guide/#relational-data) — 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.
