Saltar al contenido >_
Blog Java
DÍA 5 · #100ArchitectureDays
Java Spring Boot Architecture 100ArchitectureDays

Día 5: N+1 Queries — el bug que tu DBA ya sabe que tenés

Alejandro Lafourcade Alejandro Lafourcade
1 abr 2026 6 min read
Día 5: N+1 Queries — el bug que tu DBA ya sabe que tenés

Para mostrar 50 usuarios tu app hace 251 queries a la base de datos. Y vos ni te enteraste.

No falla. No tira excepción. No aparece en ningún log por default. Simplemente tu página tarda 4 segundos en cargar y nadie sabe por qué. Bueno, tu DBA sí sabe. Y te odia.

El supermercado

Imaginá que necesitás comprar 50 cosas. Vas al supermercado, comprás la primera, volvés a tu casa. Vas de nuevo, comprás la segunda, volvés. Y así 50 veces.

Ridículo, ¿no? Nadie haría eso en la vida real.

Bueno, tu ORM lo hace. Cada vez que accedés a una relación lazy, dispara un SELECT nuevo. Un viaje más a la base de datos. Uno por cada entidad. Y vos mirando el endpoint pensando “no sé por qué anda lento”.

Esto se llama el problema N+1 y es, sin exagerar, el bug de performance más común en cualquier aplicación que use un ORM. No importa si es Hibernate, Django, ActiveRecord, Entity Framework o Prisma. Todos los ORMs tienen este problema. Si tu framework carga relaciones de forma lazy por default, estás expuesto.

El ANTES: lazy loading te arruina el día

Tenés un Usuario con una lista de Pedido. Clásico:

@Entity
public class Usuario {
    @Id
    @GeneratedValue
    private Long id;
    private String nombre;

    @OneToMany(mappedBy = "usuario")
    private List<Pedido> pedidos; // LAZY por default
}

Y en tu servicio hacés algo inocente:

List<Usuario> usuarios = usuarioRepository.findAll();

for (Usuario u : usuarios) {
    System.out.println(u.getNombre() + ": " + u.getPedidos().size());
}

Activá spring.jpa.show-sql=true y mirá el horror:

-- Query 1: traer todos los usuarios
SELECT * FROM usuario;

-- Query 2: pedidos del usuario 1
SELECT * FROM pedido WHERE usuario_id = 1;
-- Query 3: pedidos del usuario 2
SELECT * FROM pedido WHERE usuario_id = 2;
-- Query 4: pedidos del usuario 3
SELECT * FROM pedido WHERE usuario_id = 3;
-- ...
-- Query 51: pedidos del usuario 50
SELECT * FROM pedido WHERE usuario_id = 50;

1 query para los usuarios + 50 queries para los pedidos = 51 queries. Si cada usuario tiene 5 pedidos y cada pedido tiene ítems… ya estás en las 251 queries del título. Y eso para una sola request HTTP.

Multiplicá por 100 usuarios concurrentes y entendés por qué tu base de datos está llorando.

El DESPUÉS: un solo viaje al supermercado

La solución es decirle a Hibernate: “traeme todo de una, no seas vago”.

Opción 1: JOIN FETCH en JPQL

@Query("SELECT u FROM Usuario u JOIN FETCH u.pedidos")
List<Usuario> findAllConPedidos();

Opción 2: @EntityGraph (más declarativo)

@EntityGraph(attributePaths = {"pedidos"})
@Query("SELECT u FROM Usuario u")
List<Usuario> findAllConPedidos();

Resultado en la base de datos:

-- UNA sola query
SELECT u.*, p.*
FROM usuario u
LEFT JOIN pedido p ON u.id = p.usuario_id;

251 queries se convierten en 1. Un solo viaje. Todo lo que necesitás, de una sola vez.

Los números

ANTESDESPUÉSMejora
Queries251199.6%
Tiempo~4200ms~85ms98%

No es una mejora marginal. Es la diferencia entre una app que funciona y una que da vergüenza.

Cómo detectar N+1 antes de producción

No esperés a que tu DBA te mande un mensaje pasivo-agresivo. Detectalo vos:

1. Activá el logging de SQL en desarrollo:

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

Si ves el mismo SELECT repitiéndose con diferentes parámetros, tenés un N+1.

2. Usá las estadísticas de Hibernate:

spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true

En el log vas a ver algo como:

Session Metrics {
    1234567 nanoseconds spent executing 251 JDBC statements
}

251 statements para un endpoint que debería hacer 1 o 2 queries. Ahí lo tenés. Evidencia irrefutable.

3. Regla de oro: si un endpoint ejecuta más de 10 queries, algo está mal. No hay excusa.

Cuándo NO usar JOIN FETCH

Antes de que salgas a ponerle JOIN FETCH a todo (sí, te vi), hay un caso donde te va a explotar en la cara:

// NUNCA hagas esto
@Query("SELECT u FROM Usuario u JOIN FETCH u.pedidos JOIN FETCH u.direcciones")
List<Usuario> findAllCompleto();

Si un usuario tiene 5 pedidos y 3 direcciones, Hibernate genera un producto cartesiano: 5 x 3 = 15 filas por usuario. Con 50 usuarios son 750 filas. Y la JVM tiene que deduplicar todo eso en memoria.

La regla: JOIN FETCH con una sola colección a la vez. Si necesitás múltiples colecciones, usá @BatchSize o queries separadas:

@Entity
public class Usuario {
    @OneToMany(mappedBy = "usuario")
    @BatchSize(size = 50) // Carga en lotes de 50 en vez de 1 por 1
    private List<Pedido> pedidos;

    @OneToMany(mappedBy = "usuario")
    @BatchSize(size = 50)
    private List<Direccion> direcciones;
}

@BatchSize convierte 50 queries individuales en 1 query con un IN clause. No es tan eficiente como JOIN FETCH, pero evita el producto cartesiano.

Esto no es solo Java

Si usás Django y hacés User.objects.all() y después accedés a user.orders en un loop, tenés el mismo problema. La solución allá es select_related y prefetch_related.

Si usás Rails, es includes(:orders).

Si usás Entity Framework, es .Include(u => u.Orders).

Si usás Prisma, es el include: { orders: true }.

El patrón es universal. Cambia la sintaxis, el problema es idéntico. Si tu ORM carga relaciones de forma lazy y vos iterás sin pensar, estás haciendo N+1. No importa el lenguaje, no importa el framework.

Esto es el Día 5

Este artículo es parte de #100ArchitectureDays — una serie de problemas reales de arquitectura con soluciones reales. No teoría abstracta. Código que podés correr y medir.

La próxima vez que un endpoint ande lento, antes de escalar horizontalmente, antes de agregar un cache, antes de culpar a la base de datos… activá show-sql y contá las queries.

Te sorprendería la cantidad de problemas de performance que se resuelven con un JOIN FETCH bien puesto.

Seguí la saga completa en #100ArchitectureDays.

Todo el código está en GitHub. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.

No te pierdas el próximo artículo

Suscribite y recibí cada nuevo post directo en tu inbox. Sin spam — solo ingeniería real.

¿Necesitás ayuda con tu proyecto?

Arquitectura, code review, decisiones técnicas — agendá una sesión 1:1.

Agendar Consultoría
Tags: Java Spring Boot Architecture 100ArchitectureDays
Compartir artículo:
// related_posts

También te puede interesar