Escritos

El asesino de bases de datos: qué es el problema de las consultas N+1 y cómo resolverlo con eager loading

Un código que se ejecuta en 5 ms en tu portátil con 3 filas de prueba puede tumbar fácilmente tu servidor PostgreSQL de producción al listar 100 pedidos. Así se produce la trampa de las consultas N+1 en los ORM modernos y cómo solucionarla con una sola línea.

Furkan Özay20 de octubre de 2026 · 3 min de lectura
czCZAY.DEV // EDITORIALES
DATABASE INTERNALS

#Veritabanı · 3 MIN DE LECTURA

FURKAN ÖZAYVOL. 2026 // #2C8A

Todo ingeniero backend ha vivido este misterio al menos una vez: construyes un endpoint de API que lista los pedidos recientes junto con el nombre de cada cliente. En tu máquina local de desarrollo, con 4 registros de prueba, el endpoint responde en unos fulgurantes 6 milisegundos.

Lo despliegas a producción. En cuanto llega el tráfico y la página muestra 100 pedidos por solicitud, el uso de CPU de tu servidor PostgreSQL se dispara al 100 %, los pools de conexiones se agotan y los tiempos de respuesta se arrastran hasta los 3,5 segundos.

¿El culpable? El antipatrón de rendimiento más conocido de la historia de los ORM: el problema de las consultas N+1.

· · ·

1. Anatomía de una consulta N+1

Mira este código aparentemente inocente que obtiene los últimos 100 pedidos y adjunta los datos de cada cliente:

typescript
// ❌ THE N+1 TRAP: 1 query for orders + 100 separate queries inside the loop!
const orders = await db.query('SELECT * FROM orders LIMIT 100'); // -> 1st Query (1)
 
for (const order of orders) {
  // Executes a brand-new network roundtrip to PostgreSQL on EVERY iteration!
  order.user = await db.query('SELECT * FROM users WHERE id = $1', [order.user_id]); // -> 100 Queries (N)
}

Hagamos las cuentas:

  • 1 consulta obtiene la lista de N pedidos (N = 100).
  • 100 consultas independientes obtienen el usuario de cada pedido.
  • Total: ¡100 + 1 = 101 consultas a la base de datos para una sola solicitud HTTP!

Por qué la latencia de red te hunde

Aunque PostgreSQL ejecute cada SELECT * FROM users WHERE id = $1 en 0.2ms gracias a un índice de clave primaria, cada consulta por separado requiere un viaje de ida y vuelta por la red TCP entre tu contenedor de API y tu servidor de base de datos (~3ms a 5ms en las VPC de la nube).

  • 2 consultas × 4ms de sobrecarga de red = 8ms
  • 101 consultas × 4ms de sobrecarga de red = 404ms (¡para un solo usuario que recarga la página!)
· · ·

2. La solución: eager loading (JOIN o WHERE IN)

En lugar de hacerle a la base de datos 100 preguntas separadas una por una, el eager loading obtiene todos los datos necesarios en 1 o 2 consultas en total.

Enfoque A: SQL LEFT JOIN (1 consulta en total)

SQL
SELECT orders.*, users.name, users.email
FROM orders
LEFT JOIN users ON orders.user_id = users.id
LIMIT 100;

Enfoque B: WHERE id IN (...) por lotes (2 consultas en total)

Los ORM modernos como Prisma, Drizzle, Eloquent (Laravel) y Django te permiten activar el eager loading con una sola propiedad:

typescript
// ✅ PRISMA: Uses `include` to batch-fetch all related users in 1 extra query (WHERE id IN (...))
const orders = await prisma.order.findMany({
  take: 100,
  include: {
    user: true, // <- Eliminates the N+1 loop!
  },
});
 
// ✅ DRIZZLE ORM: Relational query with `with`
const orders = await db.query.orders.findMany({
  limit: 100,
  with: {
    user: true,
  },
});

Por debajo, el ORM ejecuta:

  1. SELECT * FROM orders LIMIT 100;
  2. SELECT * FROM users WHERE id IN (12, 15, 19, 44, ...);

¡Y une los objetos en memoria en microsegundos! 101 consultas se reducen a 2.

· · ·

Herramienta de diagnóstico interactiva

¿Quieres probar tus propios patrones de ORM y recorrer paso a paso nuestra lista de comprobación para detectar N+1?

👉 Abre la herramienta interactiva de diagnóstico de consultas N+1

Hoja de ruta y herramienta interactivas

Sigue leyendo