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:
// ❌ 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
Npedidos (N = 100). - 100 consultas independientes obtienen el usuario de cada pedido.
- Total: ¡
100 + 1 = 101consultas 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 = 8ms101 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)
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:
// ✅ 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:
SELECT * FROM orders LIMIT 100;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
Desarrollador Backend Junior 2026
Lo que realmente necesitas saber para conseguir un puesto backend junior en 2026 y lo que puede esperar, filtrando las trampas de palabras de moda de las ofertas reales.
¿SQL o NoSQL?
Responde seis preguntas y te diré qué tipo de base de datos encaja con tu proyecto, y por qué.
Sigue leyendo
- CZESCortando el ruido: ¿qué lenguaje y framework de backend deberías elegir en 2026?
Cortando el ruido: ¿qué lenguaje y framework de backend deberías elegir en 2026?
25 de octubre de 2026 · 4 min de lectura - CZESDentro de PHP CVE-2026-115560: el desajuste entre Session Upload Progress y el serialize handler
Dentro de PHP CVE-2026-115560: el desajuste entre Session Upload Progress y el serialize handler
24 de octubre de 2026 · 4 min de lectura