Como esta es una pregunta muy común, escribí
este artículo , en el que se basa esta respuesta.
¿Cuál es el problema de consulta N + 1?
El problema de la consulta N + 1 ocurre cuando el marco de acceso a datos ejecuta N sentencias SQL adicionales para obtener los mismos datos que podrían haberse recuperado al ejecutar la consulta SQL primaria.
Cuanto mayor sea el valor de N, más consultas se ejecutarán, mayor será el impacto en el rendimiento. Y, a diferencia del registro de consulta lento que puede ayudarlo a encontrar consultas de ejecución lenta, el problema de N + 1 no se detectará porque cada consulta adicional individual se ejecuta lo suficientemente rápido como para no activar el registro de consulta lenta.
El problema es ejecutar una gran cantidad de consultas adicionales que, en general, toman suficiente tiempo para ralentizar el tiempo de respuesta.
Consideremos que tenemos las siguientes tablas de base de datos post y post_comments que forman una relación de tabla de uno a muchos :

Vamos a crear las siguientes 4 postfilas:
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)
Y también crearemos 4 post_commentregistros secundarios:
INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)
INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)
INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)
INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)
Problema de consulta N + 1 con SQL simple
Si selecciona el post_commentsuso de esta consulta SQL:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
""", Tuple.class)
.getResultList();
Y, más tarde, decide buscar el asociado post titlepara cada uno post_comment:
for (Tuple comment : comments) {
String review = (String) comment.get("review");
Long postId = ((Number) comment.get("postId")).longValue();
String postTitle = (String) entityManager.createNativeQuery("""
SELECT
p.title
FROM post p
WHERE p.id = :postId
""")
.setParameter("postId", postId)
.getSingleResult();
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
Va a desencadenar el problema de consulta N + 1 porque, en lugar de una consulta SQL, ejecutó 5 (1 + 4):
SELECT
pc.id AS id,
pc.review AS review,
pc.post_id AS postId
FROM post_comment pc
SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Arreglar el problema de consulta N + 1 es muy fácil. Todo lo que necesita hacer es extraer todos los datos que necesita en la consulta SQL original, así:
List<Tuple> comments = entityManager.createNativeQuery("""
SELECT
pc.id AS id,
pc.review AS review,
p.title AS postTitle
FROM post_comment pc
JOIN post p ON pc.post_id = p.id
""", Tuple.class)
.getResultList();
for (Tuple comment : comments) {
String review = (String) comment.get("review");
String postTitle = (String) comment.get("postTitle");
LOGGER.info(
"The Post '{}' got this review '{}'",
postTitle,
review
);
}
Esta vez, solo se ejecuta una consulta SQL para obtener todos los datos que nos interesan más.
Problema de consulta N + 1 con JPA e Hibernate
Al usar JPA e Hibernate, hay varias formas en que puede desencadenar el problema de consulta N + 1, por lo que es muy importante saber cómo puede evitar estas situaciones.
Para los siguientes ejemplos, considere que estamos asignando las tablas posty post_commentsa las siguientes entidades:

Las asignaciones JPA se ven así:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
//Getters and setters omitted for brevity
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
private Long id;
@ManyToOne
private Post post;
private String review;
//Getters and setters omitted for brevity
}
FetchType.EAGER
El uso FetchType.EAGERimplícito o explícito para sus asociaciones JPA es una mala idea porque va a obtener muchos más datos que necesita. Más, elFetchType.EAGERAdemás estrategia también es propensa a problemas de consulta N + 1.
Desafortunadamente, las asociaciones @ManyToOney se @OneToOneusan FetchType.EAGERde manera predeterminada, por lo que si sus asignaciones se ven así:
@ManyToOne
private Post post;
Está utilizando la FetchType.EAGERestrategia y, cada vez que olvida usarla JOIN FETCHal cargar algunas PostCommententidades con una consulta JPQL o Criteria API:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Va a desencadenar el problema de consulta N + 1:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
Observe las instrucciones SELECT adicionales que se ejecutan porque la postasociación debe recuperarse antes de devolver el ListdePostComment las entidades.
A diferencia del plan de recuperación predeterminado, que está utilizando al llamar al findmétodo de la EnrityManager, una consulta de JPQL o Criteria API define un plan explícito que Hibernate no puede cambiar al inyectar un JOIN FETCH automáticamente. Por lo tanto, debe hacerlo manualmente.
Si no necesitaba la postasociación en absoluto, no tiene suerte al usarla FetchType.EAGERporque no hay forma de evitarla. Por eso es mejor usarFetchType.LAZY por defecto.
Pero, si desea utilizar la postasociación, puede utilizar JOIN FETCHpara evitar el problema de consulta N + 1:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Esta vez, Hibernate ejecutará una sola declaración SQL:
SELECT
pc.id as id1_1_0_,
pc.post_id as post_id3_1_0_,
pc.review as review2_1_0_,
p.id as id1_0_1_,
p.title as title2_0_1_
FROM
post_comment pc
INNER JOIN
post p ON pc.post_id = p.id
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Para obtener más detalles sobre por qué debe evitar la FetchType.EAGERestrategia de recuperación, consulte también este artículo .
FetchType.LAZY
Incluso si cambia a usar FetchType.LAZYexplícitamente para todas las asociaciones, aún puede toparse con el problema N + 1.
Esta vez, la postasociación se mapea así:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Ahora, cuando busca las PostCommententidades:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Hibernate ejecutará una sola declaración SQL:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Pero, si después, va a hacer referencia a la postasociación con carga lenta:
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Obtendrá el problema de consulta N + 1:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'
Debido a que la postasociación se obtiene perezosamente, se ejecutará una instrucción SQL secundaria al acceder a la asociación perezosa para generar el mensaje de registro.
Nuevamente, la solución consiste en agregar una JOIN FETCHcláusula a la consulta JPQL:
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
join fetch pc.post p
""", PostComment.class)
.getResultList();
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Y, al igual que en el FetchType.EAGERejemplo, esta consulta JPQL generará una sola declaración SQL.
Incluso si está utilizando FetchType.LAZYy no hace referencia a la asociación secundaria de una @OneToOnerelación JPA bidireccional , aún puede desencadenar el problema de consulta N + 1.
Para obtener más detalles sobre cómo puede superar el problema de consulta N + 1 generado por las @OneToOneasociaciones, consulte este artículo .
Cómo detectar automáticamente el problema de consulta N + 1
Si desea detectar automáticamente un problema de consulta N + 1 en su capa de acceso a datos, este artículo explica cómo puede hacerlo utilizando eldb-util proyecto de código abierto.
Primero, debe agregar la siguiente dependencia de Maven:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
Después, solo tiene que usar la SQLStatementCountValidatorutilidad para afirmar las declaraciones SQL subyacentes que se generan:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
En caso de que esté utilizando FetchType.EAGERy ejecute el caso de prueba anterior, obtendrá el siguiente error de caso de prueba:
SELECT
pc.id as id1_1_,
pc.post_id as post_id3_1_,
pc.review as review2_1_
FROM
post_comment pc
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2
-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!
Para obtener más detalles sobre el db-utilproyecto de código abierto, consulte este artículo .