Uno de los problemas con los que suelen encontrarse los desarrolladores son las N+1 Queries. Por lo general aparecen al desarrollar aplicaciones usando frameworks de persistencia como Hibernate, JPA o Entity Framework. Aunque muchos no le prestan atención, es un problema que ralentiza drásticamente el rendimiento de las aplicaciones. Aquí conocerás qué son las N+1 Queries, cómo identificar este problema y, lo más importante, cómo solucionarlo.
¿Qué son las N+1 Queries?
Las N+1 Queries suceden cuando haces una primera consulta para obtener una lista de elementos (esa es la consulta “1”), y luego ejecutas consultas adicionales por cada uno de esos elementos para obtener más información relacionada. El nombre “N+1” viene de este patrón: la consulta inicial es la “1” y “N” es el número de consultas extra que haces por cada elemento de la lista.
Este problema aparece mucho cuando trabajas con relaciones entre entidades en bases de datos, especialmente en relaciones de uno a muchos. Para que lo entiendas mejor, imagina que tienes una base de datos con Autores y Libros. Esto implicaría que cada autor puede tener varios libros.
Entonces, si haces una consulta para obtener todos los autores, y luego, por cada autor, ejecutas otra consulta para obtener sus libros, estarías haciendo N+1 queries.
De modo que, si tienes 100 autores, primero obtendrías la lista de autores (consulta “1”), pero luego harías 100 consultas más, una por cada autor, para obtener sus libros. Al final habrías ejecutado 101 consultas en total, y este comportamiento puede hacer que tu aplicación se vuelva mucho más lenta.
Ejemplo práctico del problema N+1 Queries
Estás desarrollando una aplicación de gestión de empleados y proyectos. Tienes dos clases relacionadas entre sí: Empleado y Proyecto. Ten en cuenta que cada empleado puede estar asignado a múltiples proyectos, y esa relación se representa en tu base de datos.
Clases de ejemplo
Así es como se verían las clases Empleado y Proyecto usando JPA en Java:
@Entity
public class Empleado {
@Id
private Long id;
private String nombre;
@OneToMany(mappedBy = "empleado")
private List<Proyecto> proyectos = new ArrayList<>();
// Getters y setters...
}
@Entity
public class Proyecto {
@Id
private Long id;
private String nombreProyecto;
@ManyToOne
@JoinColumn(name = "empleado_id")
private Empleado empleado;
// Getters y setters...
}
Como ves, hay una relación de uno a muchos: un empleado puede tener varios proyectos. En caso de que quieras obtener una lista de todos los empleados y sus proyectos, si no tienes cuidado, puedes caer en el problema de las N+1 queries.
Consultas problemáticas
Imagina que ejecutas el siguiente código para obtener todos los empleados:
List<Empleado> empleados = entityManager.createQuery("SELECT e FROM Empleado e", Empleado.class).getResultList();
for (Empleado empleado : empleados) {
System.out.println(empleado.getNombre());
for (Proyecto proyecto : empleado.getProyectos()) {
System.out.println(proyecto.getNombreProyecto());
}
}
En este caso, la primera consulta SELECT e FROM Empleado e obtiene todos los empleados de la base de datos.
Sin embargo, para cada empleado, se ejecuta una consulta adicional para obtener los proyectos asociados.
Entonces, si tienes 50 empleados, el código ejecutará 51 consultas: una para obtener todos los empleados y 50 más para obtener los proyectos de cada empleado.
¿Por qué ralentizan tu aplicación?
Lo que sucede con las N+1 Queries es que incrementan innecesariamente el número de consultas que realiza tu aplicación a la base de datos, provocando cuellos de botella y una gran disminución en el rendimiento. Entonces, si trabajas con muchos datos o tienes muchos usuarios concurrentes accediendo a la aplicación, esto te traerá muchos problemas.
¿Cómo evitar el problema de N+1 Queries?
Por suerte, hay varias formas de evitar este problema dependiendo del framework de persistencia que estés utilizando. Estas son las soluciones más comunes:
Utiliza consultas con “fetch join”
Para que evites las N+1 Queries te recomendamos usar un fetch join en tu consulta. De esta manera, la información de las entidades relacionadas se obtiene en una única consulta, en lugar de realizar consultas separadas.
En el ejemplo anterior, puedes modificar la consulta para incluir un fetch join:
List<Empleado> empleados = entityManager.createQuery(
"SELECT e FROM Empleado e JOIN FETCH e.proyectos", Empleado.class).getResultList();
Ahora puedes ver la gran diferencia, porque obtienes tanto los empleados como sus proyectos en una única operación, eliminando las consultas adicionales.
Configura el “batch fetching”
También puedes configurar el “batch fetching” en tu framework de persistencia. Es así como el framework cargará varias entidades relacionadas en lotes, reduciendo el número de consultas necesarias.
Por ejemplo, en Hibernate puedes configurar el tamaño de los lotes para que cargue varias entidades relacionadas de una vez:
<property name="hibernate.default_batch_fetch_size" value="10"/>
Utiliza la carga “eager” solo cuando sea necesario
En aquellas ocasiones en las que necesites los datos relacionados de inmediato, puedes usar la carga “eager” en vez de la carga diferida (lazy loading). Así reduces la posibilidad de ejecutar consultas adicionales innecesarias.
Monitorea las consultas generadas
Recuerda monitorear las consultas SQL que genera tu framework de persistencia. La mayoría de los frameworks tienen opciones para habilitar el log de las consultas, de modo que puedes ver cuántas consultas se están ejecutando y si estás cayendo en el problema de las N+1 Queries.
Aprende más sobre cómo mejorar el rendimiento de tus aplicaciones y dominar frameworks como Hibernate y JPA en nuestro Bootcamp de Java Full Stack en KeepCoding. En solo unos meses, te prepararás para el mundo del desarrollo de software, con habilidades que te abrirán las puertas al sector IT. Este sector está en constante crecimiento, con alta demanda de profesionales, sueldos competitivos y estabilidad laboral. ¡No pierdas la oportunidad de cambiar tu vida!