Was ist das N+1-Abfrageproblem
Das N+1-Abfrageproblem tritt auf, wenn das Datenzugriffssystem N zusätzliche SQL-Anweisungen ausführt, um dieselben Daten abzurufen, die bei der Ausführung der primären SQL-Abfrage hätten abgerufen werden können.
Je größer der Wert von N ist, desto mehr Abfragen werden ausgeführt und desto größer sind die Auswirkungen auf die Leistung. Und im Gegensatz zum Protokoll für langsame Abfragen, das Ihnen helfen kann, langsam laufende Abfragen zu finden, wird das N+1-Problem nicht erkannt, da jede einzelne zusätzliche Abfrage ausreichend schnell läuft, um das Protokoll für langsame Abfragen nicht auszulösen.
Das Problem ist die Ausführung einer großen Anzahl zusätzlicher Abfragen, die insgesamt so viel Zeit in Anspruch nehmen, dass sich die Antwortzeit verlangsamt.
Nehmen wir an, wir haben die folgenden Datenbanktabellen post und post_comments, die eine Eins-zu-Viel-Tabellenbeziehung bilden:
Wir werden die folgenden 4 erstellen post
Reihen:
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)
Und wir werden auch 4 post_comment
Kinderaufzeichnungen:
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 Option post_comments
mit dieser SQL-Abfrage:
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 beschließen Sie, die zugehörigen Daten abzurufen. post
title
für jede 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 der N+1-Abfrage auslösen, weil Sie statt 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'
Die Behebung des Problems der N+1-Abfrage ist sehr einfach. Sie müssen nur alle Daten extrahieren, die Sie in der ursprünglichen SQL-Abfrage benötigen, etwa so:
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
);
}
Diesmal wird nur eine SQL-Abfrage ausgeführt, um alle Daten abzurufen, an denen wir weiter interessiert sind.
N+1-Abfrageproblem mit JPA und Hibernate
Bei der Verwendung von JPA und Hibernate gibt es mehrere Möglichkeiten, das Problem der N+1-Abfrage auszulösen. Daher ist es sehr wichtig zu wissen, wie Sie diese Situationen vermeiden können.
Für die nächsten Beispiele betrachten wir die Abbildung der post
y post_comments
Tabellen zu den folgenden Einrichtungen:
Die JPA-Zuordnungen sehen wie folgt 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
Verwendung von FetchType.EAGER
entweder implizit oder explizit für Ihre JPA-Verknüpfungen zu verwenden, ist eine schlechte Idee, da Sie viel mehr Daten abrufen werden, als Sie benötigen. Außerdem ist die FetchType.EAGER
Strategie ist auch anfällig für N+1-Abfrageprobleme.
Leider ist die @ManyToOne
y @OneToOne
Verbände nutzen FetchType.EAGER
standardmäßig, wenn Ihre Zuordnungen also wie folgt aussehen:
@ManyToOne
private Post post;
Sie verwenden die FetchType.EAGER
Strategie, und jedes Mal, wenn Sie vergessen, die JOIN FETCH
beim Laden einiger PostComment
Entitäten mit einer JPQL- oder Criteria-API-Abfrage:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Sie werden das Problem der N+1-Abfrage 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
Assoziation abgeholt werden muss, bevor die List
von PostComment
Einheiten.
Im Gegensatz zum Standard-Fetchplan, den Sie beim Aufruf der find
Methode der EntityManager
Eine JPQL- oder Criteria-API-Abfrage definiert einen expliziten Plan, den Hibernate nicht durch automatisches Einfügen eines JOIN FETCH ändern kann. Sie müssen dies also manuell tun.
Wenn Sie das nicht bräuchten post
Assoziation überhaupt, haben Sie Pech, wenn Sie FetchType.EAGER
denn es lässt sich nicht vermeiden, dass es abgeholt wird. Deshalb ist es besser, die FetchType.LAZY
standardmäßig.
Wenn Sie jedoch Folgendes verwenden möchten post
Assoziation, dann können Sie JOIN FETCH
um das N+1-Abfrageproblem zu vermeiden:
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 einzige 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'
FetchType.LAZY
Selbst wenn Sie auf die Verwendung von FetchType.LAZY
explizit für alle Verbände, können Sie immer noch auf das N+1 Problem stoßen.
Dieses Mal ist die post
Assoziation wird wie folgt abgebildet:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Wenn Sie nun die PostComment
Einheiten:
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Hibernate wird eine einzelne SQL-Anweisung ausführen:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Wenn Sie aber danach auf die "lazy-loaded post
Verein:
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
Sie werden das Problem der N+1-Abfrage bekommen:
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'
Weil die post
Assoziation "lazy" abgerufen wird, wird beim Zugriff auf die "lazy" Assoziation eine sekundäre SQL-Anweisung ausgeführt, um die Protokollmeldung zu erstellen.
Auch hier besteht die Lösung im Hinzufügen einer JOIN FETCH
Klausel in der JPQL-Abfrage:
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 in der FetchType.EAGER
Beispiel wird diese JPQL-Abfrage eine einzige SQL-Anweisung erzeugen.
Auch wenn Sie die FetchType.LAZY
und verweisen nicht auf die untergeordnete Assoziation einer bidirektionalen @OneToOne
JPA-Beziehung, können Sie immer noch das Problem der N+1-Abfrage auslösen.
Wie wird das Problem der N+1-Abfrage automatisch erkannt?
Wenn Sie N+1-Abfrageprobleme in Ihrer Datenzugriffsschicht automatisch erkennen möchten, können Sie die db-util
Open-Source-Projekt.
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 SQLStatementCountValidator
um die zugrunde liegenden SQL-Anweisungen, die generiert werden, zu bestätigen:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
Falls Sie Folgendes verwenden FetchType.EAGER
und führen Sie den obigen Testfall aus, erhalten Sie den folgenden Fehler:
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!
2 Stimmen
Es gibt einige hilfreiche Beiträge, die sich mit diesem Problem und der möglichen Lösung befassen. Häufige Anwendungsprobleme und deren Behebung: Das Select N + 1 Problem , Die (silberne) Kugel für das N+1 Problem , Langsames Laden - eifriges Laden
0 Stimmen
Für alle, die nach einer Lösung für dieses Problem suchen, habe ich einen Beitrag gefunden, der es beschreibt. stackoverflow.com/questions/32453989/
54 Stimmen
Wenn man die Antworten bedenkt, sollte man dies nicht als 1+N Problem bezeichnen? Da es sich um eine Terminologie zu handeln scheint, frage ich nicht speziell OP.