Como essa é uma pergunta muito comum, escrevi
este artigo , no qual essa resposta se baseia.
Qual é o problema de consulta N + 1
O problema de consulta N + 1 ocorre quando a estrutura de acesso a dados executa N instruções SQL adicionais para buscar os mesmos dados que poderiam ter sido recuperados ao executar a consulta SQL primária.
Quanto maior o valor de N, mais consultas serão executadas, maior o impacto no desempenho. E, diferentemente do log de consultas lentas que pode ajudá-lo a encontrar consultas de execução lenta, o problema N + 1 não será pontual, pois cada consulta adicional individual é executada com rapidez suficiente para não acionar o log de consultas lentas.
O problema está executando um grande número de consultas adicionais que, em geral, levam tempo suficiente para diminuir o tempo de resposta.
Vamos considerar que temos as seguintes tabelas de banco de dados post e post_comments que formam um relacionamento de tabela um para muitos :
Vamos criar as 4 post
linhas a seguir :
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)
E também criaremos 4 post_comment
registros filhos:
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 com SQL simples
Se você selecionar post_comments
usando 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();
E, mais tarde, você decide buscar o associado post
title
para cada um 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
);
}
Você acionará o problema de consulta N + 1 porque, em vez de uma consulta SQL, você executou 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'
Corrigir o problema de consulta N + 1 é muito fácil. Tudo o que você precisa fazer é extrair todos os dados necessários na consulta SQL original, assim:
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
);
}
Desta vez, apenas uma consulta SQL é executada para buscar todos os dados que estamos mais interessados em usar.
Problema de consulta N + 1 com JPA e Hibernate
Ao usar o JPA e o Hibernate, há várias maneiras de disparar o problema de consulta N + 1; portanto, é muito importante saber como evitar essas situações.
Para os próximos exemplos, considere que estamos mapeando as tabelas post
e post_comments
para as seguintes entidades:
Os mapeamentos JPA são assim:
@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
Usar de FetchType.EAGER
forma implícita ou explícita para suas associações JPA é uma péssima idéia, pois você irá buscar muito mais dados necessários. FetchType.EAGER
Além disso , a estratégia também é propensa a problemas de consulta N + 1.
Infelizmente, as associações @ManyToOne
e @OneToOne
usam FetchType.EAGER
por padrão, portanto, se seus mapeamentos se parecerem com isso:
@ManyToOne
private Post post;
Você está usando a FetchType.EAGER
estratégia e, toda vez que se esquecer de usar JOIN FETCH
ao carregar algumas PostComment
entidades com uma consulta à API JPQL ou Critérios:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Você vai acionar o 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 as instruções SELECT adicionais que são executadas porque a post
associação precisa ser buscada antes do retorno List
das PostComment
entidades.
Diferentemente do plano de busca padrão, que você está usando ao chamar o find
método da EnrityManager
, uma consulta à API JPQL ou Critérios define um plano explícito que o Hibernate não pode alterar injetando um JOIN FETCH automaticamente. Então, você precisa fazer isso manualmente.
Se você não precisou da post
associação, está sem sorte ao usá-la FetchType.EAGER
porque não há como evitar a busca. É por isso que é melhor usar FetchType.LAZY
por padrão.
Mas, se você quiser usar a post
associação, poderá usar JOIN FETCH
para evitar o 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()
);
}
Desta vez, o Hibernate executará uma única instrução 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 mais detalhes sobre por que você deve evitar a FetchType.EAGER
estratégia de busca, consulte este artigo também.
FetchType.LAZY
Mesmo se você mudar para o uso FetchType.LAZY
explícito em todas as associações, ainda poderá encontrar o problema N + 1.
Desta vez, a post
associação é mapeada da seguinte maneira:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Agora, quando você busca as PostComment
entidades:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
O Hibernate irá executar uma única instrução SQL:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Mas, se depois, você fará referência à post
associação preguiçosa :
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Você receberá o 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'
Como a post
associação é buscada preguiçosamente, uma instrução SQL secundária será executada ao acessar a associação preguiçosa para criar a mensagem de log.
Novamente, a correção consiste em adicionar uma JOIN FETCH
cláusula à 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()
);
}
E, como no FetchType.EAGER
exemplo, essa consulta JPQL gerará uma única instrução SQL.
Mesmo se você estiver usando FetchType.LAZY
e não fizer referência à associação filho de um @OneToOne
relacionamento JPA bidirecional , ainda poderá disparar o problema de consulta N + 1.
Para obter mais detalhes sobre como você pode superar o problema de consulta N + 1 gerado por @OneToOne
associações, consulte este artigo .
Como detectar automaticamente o problema de consulta N + 1
Se você deseja detectar automaticamente o problema de consulta N + 1 em sua camada de acesso a dados, este artigo explica como fazer isso usando o db-util
projeto de código aberto.
Primeiro, você precisa adicionar a seguinte dependência do Maven:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
Depois, você só precisa usar o SQLStatementCountValidator
utilitário para afirmar as instruções SQL subjacentes que são geradas:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
Caso você esteja usando FetchType.EAGER
e execute o caso de teste acima, você receberá a seguinte falha do caso de teste:
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 mais detalhes sobre o db-util
projeto de código aberto, consulte este artigo .