Da dies eine sehr häufige Frage ist, habe ich
diesen Artikel geschrieben , auf dem diese Antwort basiert.
Was ist das N + 1-Abfrageproblem?
Das N + 1-Abfrageproblem tritt auf, wenn das Datenzugriffsframework N zusätzliche SQL-Anweisungen ausführt, um dieselben Daten abzurufen, die beim Ausführen der primären SQL-Abfrage abgerufen werden könnten.
Je größer der Wert von N ist, desto mehr Abfragen werden ausgeführt, desto größer ist die Auswirkung auf die Leistung. Und im Gegensatz zum langsamen Abfrageprotokoll , mit dem Sie langsam laufende Abfragen finden können, ist das N + 1-Problem nicht erkennbar, da jede einzelne zusätzliche Abfrage so schnell ausgeführt wird, dass das langsame Abfrageprotokoll nicht ausgelöst wird.
Das Problem besteht darin, eine große Anzahl zusätzlicher Abfragen auszuführen, die insgesamt ausreichend Zeit benötigen, um die Antwortzeit zu verlangsamen.
Nehmen wir an, wir haben die folgenden Post- und Post_comments-Datenbanktabellen, die eine Eins-zu-Viele-Tabellenbeziehung bilden :
![Die Tabellen <code> post </ code> und <code> post_comments </ code>](https://i.stack.imgur.com/T1uWG.png)
Wir werden die folgenden 4 post
Zeilen erstellen :
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)
Außerdem erstellen wir 4 post_comment
untergeordnete Datensätze:
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)
N + 1-Abfrageproblem mit einfachem SQL
Wenn Sie die post_comments
Verwendung dieser SQL-Abfrage auswählen :
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();
Und später entscheiden Sie sich, die zugehörigen post
title
für jeden abzurufen 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
);
}
Sie werden das Problem mit der N + 1-Abfrage auslösen, da Sie anstelle einer SQL-Abfrage 5 (1 + 4) ausgeführt haben:
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'
Das Beheben des N + 1-Abfrageproblems ist sehr einfach. Sie müssen lediglich alle Daten extrahieren, die Sie in der ursprünglichen SQL-Abfrage benötigen:
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
);
}
Dieses Mal wird nur eine SQL-Abfrage ausgeführt, um alle Daten abzurufen, an deren Verwendung wir weiter interessiert sind.
N + 1-Abfrageproblem mit JPA und Ruhezustand
Bei Verwendung von JPA und Hibernate gibt es verschiedene Möglichkeiten, das N + 1-Abfrageproblem auszulösen. Daher ist es sehr wichtig zu wissen, wie Sie diese Situationen vermeiden können.
Betrachten Sie für die nächsten Beispiele, dass wir die post
und post_comments
-Tabellen den folgenden Entitäten zuordnen:
![<code> Post </ code> und <code> PostComment </ code>](https://i.stack.imgur.com/rZJne.png)
Die JPA-Zuordnungen sehen folgendermaßen aus:
@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
Die FetchType.EAGER
implizite oder explizite Verwendung für Ihre JPA-Zuordnungen ist eine schlechte Idee, da Sie viel mehr Daten abrufen werden, als Sie benötigen. Darüber hinaus ist die FetchType.EAGER
Strategie auch anfällig für N + 1-Abfrageprobleme.
Leider werden die @ManyToOne
und @OneToOne
Assoziationen FetchType.EAGER
standardmäßig verwendet. Wenn Ihre Zuordnungen also folgendermaßen aussehen:
@ManyToOne
private Post post;
Sie verwenden die FetchType.EAGER
Strategie und jedes Mal, JOIN FETCH
wenn Sie vergessen, sie beim Laden einiger PostComment
Entitäten mit einer JPQL- oder Kriterien-API-Abfrage zu verwenden:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Sie werden das N + 1-Abfrageproblem auslösen:
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
Beachten Sie die zusätzlichen SELECT - Anweisungen , die ausgeführt werden , weil die post
Vereinigung der auf die Rückkehr geholt , bevor sein muss List
von PostComment
Einheiten.
Im Gegensatz zum Standardabrufplan, den Sie beim Aufrufen der find
Methode von verwenden EnrityManager
, definiert eine JPQL- oder Kriterien-API-Abfrage einen expliziten Plan, den Hibernate nicht durch automatisches Einfügen eines JOIN FETCH ändern kann. Sie müssen dies also manuell tun.
Wenn Sie die post
Zuordnung überhaupt nicht benötigt haben , haben Sie bei der Verwendung FetchType.EAGER
kein Glück, da es nicht möglich ist, das Abrufen zu vermeiden. Deshalb ist es besser, FetchType.LAZY
standardmäßig zu verwenden.
Wenn Sie jedoch die post
Zuordnung verwenden möchten, können Sie JOIN FETCH
das N + 1-Abfrageproblem umgehen:
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()
);
}
Dieses Mal führt Hibernate eine einzelne SQL-Anweisung aus:
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'
Weitere Informationen darüber, warum Sie die FetchType.EAGER
Abrufstrategie vermeiden sollten , finden Sie auch in diesem Artikel .
FetchType.LAZY
Selbst wenn Sie FetchType.LAZY
explizit für alle Zuordnungen verwenden, können Sie dennoch auf das Problem N + 1 stoßen.
Diesmal wird die post
Zuordnung folgendermaßen zugeordnet:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Wenn Sie nun die PostComment
Entitäten abrufen:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Der Ruhezustand führt eine einzelne SQL-Anweisung aus:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Wenn Sie sich danach darauf beziehen, verweisen Sie auf die faul geladene post
Zuordnung:
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Sie erhalten das Problem mit der N + 1-Abfrage:
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'
Da die post
Zuordnung träge abgerufen wird, wird beim Zugriff auf die verzögerte Zuordnung eine sekundäre SQL-Anweisung ausgeführt, um die Protokollnachricht zu erstellen.
Das Update besteht wiederum darin JOIN FETCH
, der JPQL-Abfrage eine Klausel hinzuzufügen :
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()
);
}
Und genau wie im FetchType.EAGER
Beispiel generiert diese JPQL-Abfrage eine einzelne SQL-Anweisung.
Selbst wenn Sie FetchType.LAZY
die untergeordnete Zuordnung einer bidirektionalen @OneToOne
JPA-Beziehung verwenden und nicht darauf verweisen , können Sie dennoch das N + 1-Abfrageproblem auslösen.
Weitere Informationen dazu, wie Sie das durch @OneToOne
Assoziationen verursachte N + 1-Abfrageproblem lösen können , finden Sie in diesem Artikel .
So erkennen Sie das N + 1-Abfrageproblem automatisch
Wenn Sie ein N + 1-Abfrageproblem in Ihrer Datenzugriffsebene automatisch erkennen möchten, wird in diesem Artikel erläutert, wie Sie dies mithilfe des db-util
Open-Source-Projekts tun können .
Zunächst müssen Sie die folgende Maven-Abhängigkeit hinzufügen:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
Danach müssen Sie nur noch das SQLStatementCountValidator
Dienstprogramm verwenden, um die zugrunde liegenden SQL-Anweisungen zu aktivieren, die generiert werden:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
Wenn Sie FetchType.EAGER
den obigen Testfall verwenden und ausführen, wird der folgende Testfallfehler angezeigt:
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!
Weitere Informationen zum db-util
Open Source-Projekt finden Sie in diesem Artikel .